Files
yetone 0bf9d77d3f agent tests: TestDriftBypassed's request reaching the gateway is kept to its run, so -count=2 passes
The test stood for a request with usage.Saw("codex"). Saw's record lasts
the process, which the gateway relies on (LastSeen answers from it before
a request is written to the log), so a second run started with a request
seconds old, newer than its prompt five minutes back, and found nothing
bypassed ("bypassed: <nil>", also on origin/main).

Drift reads LastSeen through a package variable the test replaces with its
own clock; production still reads usage.LastSeen. That Saw is LastSeen's
answer at once moves to usage's own TestSawIsLastSeen, which removes its
record afterwards.

-run 'TestDrift|TestSawIsLastSeen' -race -count=5 passes; before, -count=2
failed.
2026-10-07 03:03:06 +08:00

829 lines
27 KiB
Go

package agent
import (
"bufio"
"encoding/json"
"fmt"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
"sync"
"time"
"github.com/yetone/magpie/internal/edit"
"github.com/yetone/magpie/internal/gateway"
"github.com/yetone/magpie/internal/provider"
"github.com/yetone/magpie/internal/usage"
)
// An agent's config is a file anyone can write: another switcher, an
// installer, the agent's own setup. One that takes magpie out leaves the
// row showing a magpie model while the agent asks its own vendor for it, and
// the user blames magpie. So magpie remembers what it last set on each agent
// (applied.json, beside the stash) and says when that no longer holds —
// Drift — with the way to set it again.
var appliedMu sync.Mutex
// applied is what magpie last set on one agent, and when.
type applied struct {
At time.Time `json:"at"`
Fields map[string]string `json:"fields"` // field key → value
}
func appliedPath() string { return filepath.Join(filepath.Dir(provider.Path()), "applied.json") }
// appliedLoad is agent id → what magpie set on it.
func appliedLoad() map[string]applied {
out := map[string]applied{}
if b, err := os.ReadFile(appliedPath()); err == nil {
json.Unmarshal(b, &out)
}
return out
}
func appliedSave(m map[string]applied) {
b, _ := json.MarshalIndent(m, "", " ")
os.MkdirAll(filepath.Dir(appliedPath()), 0o755)
os.WriteFile(appliedPath(), b, 0o600)
}
func appliedOf(id string) applied {
appliedMu.Lock()
defer appliedMu.Unlock()
return appliedLoad()[id]
}
// record keeps what a field reads after magpie set it; a field put back to
// the agent's default is magpie's no longer.
func record(id, key, v string) {
appliedMu.Lock()
defer appliedMu.Unlock()
m := appliedLoad()
a := m[id]
if a.Fields == nil {
a.Fields = map[string]string{}
}
if v == "" {
delete(a.Fields, key)
} else {
a.Fields[key] = v
}
a.At = time.Now()
m[id] = a
appliedSave(m)
}
// Apply sets one of the agent's fields and remembers it as magpie's, so it
// can be told apart from what something else writes there later. Every
// setting of a field by the user goes through here. A field magpie set
// before that this one moves too is magpie's doing as well: Claude Code's
// Default takes its tiers and subagent model out with magpie's endpoint,
// which read as changed outside magpie, and the drift kept the row up
// among the connected ones while it said Not connected (#834).
func (a *Agent) Apply(key, v string) error {
if a.Native != nil {
return a.Native.Apply(key, v)
}
f := a.Field(key)
if f == nil {
return nil
}
was := a.Values()
if err := f.Set(v); err != nil {
return err
}
now := a.Values()
for k := range appliedOf(a.ID).Fields {
if k != f.Key && now[k] != was[k] {
record(a.ID, k, now[k])
}
}
record(a.ID, f.Key, f.Get())
return nil
}
// Drift is how an agent differs from what magpie set on it.
type Drift struct {
// Kind says what is off:
// "unwired" the model is magpie's but the config no longer sends it
// through magpie (Check);
// "replaced" a magpie model magpie set was replaced by the agent's own;
// "bypassed" the config is right, yet the agent was used since and
// nothing of it reached the gateway — it runs on an old
// config, or something outside the file overrides it;
// "unreachable" the config is right, but the address off loopback
// it names the gateway at doesn't answer (#1013).
Kind string `json:"kind"`
Field string `json:"field"` // the field it shows on
Now string `json:"now,omitempty"` // what that field says now
Want string `json:"want"` // what setting it again sets
Detail string `json:"detail"` // what exactly is off, for a tooltip
// Addr is where an unreachable agent is pointed; Move, when set, the
// address WSL reaches Windows at now, which Reapply points it at.
Addr string `json:"addr,omitempty"`
Move string `json:"move,omitempty"`
}
// started is when this process — and the gateway in it — came up: before
// then, a request the gateway missed says nothing about the agent.
var started = time.Now()
// Drift says what, if anything, keeps the agent off what magpie set: its
// config first (wiring, then each field against magpie's record), then —
// the config being right — whether its latest use actually came through.
// A field moved from one magpie model to another (the agent's own picker)
// isn't drift; one moved off magpie is.
func (a *Agent) Drift() *Drift {
if a.Native != nil {
s := a.Native.Read()
if s.Provider == "invalid" {
return &Drift{Kind: "unwired", Field: "model", Detail: s.Detail}
}
return nil
}
if len(a.Fields) == 0 || a.theInstalled() {
return nil
}
vals := a.Values()
// the field on one of magpie's models: where drift shows, and what
// setting it again sets
on, onMagpie := a.Fields[0], false
for _, f := range a.Fields {
if magpieValue(a, f, vals[f.Key], vals) {
on, onMagpie = f, true
break
}
}
if a.Check != nil {
if d := a.Check(); d != "" {
return &Drift{Kind: "unwired", Field: on.Key, Now: vals[on.Key], Want: vals[on.Key], Detail: d}
}
}
rec := appliedOf(a.ID)
// still joined, a model off magpie is one picked in the agent, beside
// magpie's in its list (dsh's /model), not a change from outside
joined := a.Joined != nil && a.Joined()
for _, f := range a.Fields {
want, ok := rec.Fields[f.Key]
// a field that reads empty while it follows another (Claude Code's
// tiers and subagents on its main model) runs on that one's model:
// the main model moved onto the one magpie set the field to reads
// as following it, not as the field put back to the agent's own
// default (#1050)
now := vals[f.Key]
if now == "" && f.Follows != "" {
now = vals[f.Follows]
}
if !ok || joined || now == want || !magpieValue(a, f, want, vals) || magpieValue(a, f, now, vals) || sameGroup(want, now) {
continue
}
return &Drift{Kind: "replaced", Field: f.Key, Now: vals[f.Key], Want: want,
Detail: a.Name + "'s config was changed outside magpie: " + f.Label + " is " + orDefault(vals[f.Key]) + ", not " + want + " as magpie set it"}
}
if onMagpie || joined {
if d := a.unreachable(on, vals[on.Key]); d != nil {
return d
}
}
if a.Reached != nil && onMagpie {
if at, to, refused := a.Reached(rec.At); !at.IsZero() {
switch {
case !sameHost(to, gateway.URL()):
return &Drift{Kind: "bypassed", Field: on.Key, Now: vals[on.Key], Want: vals[on.Key],
Detail: a.Name + "'s last request (" + at.Format("15:04") + ") went to " + hostOf(to) + ", not magpie — it was started before magpie set it up and still runs on its old config: quit and reopen it"}
case refused:
return &Drift{Kind: "bypassed", Field: on.Key, Now: vals[on.Key], Want: vals[on.Key],
Detail: a.Name + " couldn't reach magpie at " + at.Format("15:04") + " — magpie wasn't running then, so it has none of magpie's models: quit and reopen it"}
}
}
}
if a.LastUsed != nil && onMagpie {
if used := a.LastUsed(); bypassed(used, rec.At, lastSeen(a.ID)) {
return &Drift{Kind: "bypassed", Field: on.Key, Now: vals[on.Key], Want: vals[on.Key],
Detail: a.Name + " was used at " + used.Format("15:04") + " but none of its requests reached magpie — one started before magpie set it up still runs on its old config: restart it"}
}
}
return nil
}
// installedURL is the gateway of the magpie people install, on its own
// port, the one its Settings have; a magpie MAGPIE_ADDR puts on another
// (magpie-dev, a sandbox) is run beside it.
var installedURL = gateway.SavedURL
// elsewhere: this magpie is not on the installed one's port.
func elsewhere() bool { return !sameHost(gateway.URL(), installedURL()) }
// theInstalled: this magpie runs beside the installed one, and the agent is
// wired to that one's gateway, not this one's — the installed magpie set it
// up, so it is that one's to check, not drift here. Otherwise the two would
// take turns flagging each other's wiring.
func (a *Agent) theInstalled() bool {
if !elsewhere() || a.Path == "" {
return false
}
b, err := edit.Read(a.Path)
if err != nil {
return false
}
return strings.Contains(string(b), hostOf(installedURL())) && !strings.Contains(string(b), hostOf(gateway.URL()))
}
// bypassed: the agent was used — while this gateway was up and after magpie
// last set it — and no request of it arrived since. A request leaves within
// moments of the prompt; a little grace keeps one in flight from counting.
// lastSeen is when the agent's last request reached this process's gateway
// (usage.LastSeen). What usage.Saw records lasts as long as the process, so
// a test that stands for a request gives its own here.
var lastSeen = usage.LastSeen
func bypassed(used, applied, seen time.Time) bool {
const grace = 30 * time.Second
return !used.IsZero() && used.After(started) && used.After(applied) &&
time.Since(used) > grace && seen.Before(used.Add(-2*time.Second))
}
// magpieValue: the value is one of magpie's models as this agent spells it,
// its suffix after the model (SplitSuffix) aside; a list of models is the
// user's own.
func magpieValue(a *Agent, f Field, v string, vals map[string]string) bool {
if v == "" {
return false
}
v, _, one := a.split(v)
if !one || a.Spelled != nil && !a.Spelled(v) {
return false
}
if isMagpie(v) {
return true
}
// spelled as magpie's provider in the agent's own config (OpenCode's
// magpie/…): magpie's even when its list there doesn't have the model
if ref, ok := strings.CutPrefix(v, magpieID+"/"); ok && isMagpie(ref) {
return true
}
if f.Options == nil {
return false
}
for _, o := range f.Options(vals) {
if o.Value == v {
return o.Ref != ""
}
}
return false
}
// groupNamed is the routing group ("group/<id>") a model value reaches at
// the gateway: a group's own id, or a model's id without a provider in it
// that the gateway takes as a group's (provider.GroupFor: gpt-6.1-sol is
// group/auto-gpt-6-1-sol); "" for any other.
func groupNamed(v string) string {
// Claude Code's [1m] mark rides on the group's id as it does on a
// model's, and one of the two values here comes from the agent's own
// settings: without it off, a group read back marked is not the one
// magpie set (GroupFor takes it off; so does GroupFinder)
v = strings.TrimSuffix(strings.TrimSpace(v), "[1m]")
v = strings.TrimPrefix(v, magpieID+"/")
if strings.HasPrefix(v, provider.GroupPrefix) {
return v
}
g, _ := provider.GroupFor(v)
return g
}
// sameGroup: two values of a field reach the same routing group, one by
// the group's id and the other by its model's name (#750: Codex's config
// read gpt-6.1-sol where magpie had set group/auto-gpt-6-1-sol). Asked for
// either, the gateway serves the group, so the one is not the other
// replaced.
func sameGroup(a, b string) bool {
g := groupNamed(a)
return g != "" && g == groupNamed(b)
}
// setAsGroup: magpie set the agent's field to a routing group, and v names
// that group by its model's name (sameGroup) — magpie's model still, for an
// agent whose config sends v to magpie's gateway.
func setAsGroup(id, key, v string) bool {
if v == "" || isMagpie(v) {
return false
}
want, ok := appliedOf(id).Fields[key]
return ok && sameGroup(want, v)
}
func orDefault(v string) string {
if v == "" {
return "the agent's default"
}
return v
}
// Reapply sets again what magpie set on the agent: what drifted, else its
// fields as they read now — for a config taken off magpie in a way no check
// catches. A replaced field brings back the others magpie set with it.
// Either way the record is renewed, so a use before now no longer counts.
func (a *Agent) Reapply() error {
if a.Native != nil {
return a.Native.Connect()
}
d := a.Drift()
if d != nil && d.Kind == "unreachable" && d.Move != "" && a.move != nil {
return a.move(d.Addr, d.Move)
}
if d != nil && d.Kind == "replaced" {
rec := appliedOf(a.ID)
// the model first: the others (an effort) are checked against it
if err := a.Apply(d.Field, d.Want); err != nil {
return err
}
for _, f := range a.Fields {
if v, ok := rec.Fields[f.Key]; ok && f.Key != d.Field && f.Get() != v {
if err := a.Apply(f.Key, v); err != nil {
return err
}
}
}
return nil
}
vals := a.Values()
for _, f := range a.Fields {
if v := vals[f.Key]; v != "" && (d != nil && f.Key == d.Field || magpieValue(a, f, v, vals)) {
if err := a.Apply(f.Key, v); err != nil {
return err
}
}
}
return nil
}
// Keep takes the agent's config as it is now: what magpie set before is
// forgotten, and no longer said to have been changed.
func (a *Agent) Keep() {
appliedMu.Lock()
defer appliedMu.Unlock()
m := appliedLoad()
if _, ok := m[a.ID]; !ok {
return
}
delete(m, a.ID)
appliedSave(m)
}
// Wired reports whether magpie is in the agent's config: a field on one of
// magpie's models, or on magpie itself (an app whose one setting is magpie
// as its provider).
func (a *Agent) Wired() bool {
if a.Native != nil {
return a.Native.Read().Provider == "connected"
}
if a.Joined != nil && a.Joined() || a.Beside != nil && a.Beside() {
return true
}
vals := a.Values()
for _, f := range a.Fields {
if v := vals[f.Key]; v == magpieID || magpieValue(a, f, v, vals) || a.Routed != nil && setAsGroup(a.ID, f.Key, v) && a.Routed() {
return true
}
}
return false
}
// Pick sets a field from the Agents page's model picker. One of magpie's
// models picked on an agent not connected yet connects it first, as its
// switch does, then starts it on that model (the owner: the switch, and a
// model picked in magpie in one click, both): the agent gets magpie's whole
// list, and Disconnect puts back what it had before.
func (a *Agent) Pick(key, v string) error {
if a.Native != nil {
value, err := a.Spell(key, v)
if err != nil {
return err
}
return a.Apply(key, value)
}
if f := a.Field(key); f != nil && !a.Wired() {
vals := a.Values()
vals[key] = v
if v == magpieID || magpieValue(a, *f, v, vals) {
if _, err := a.connect(); err != nil {
return err
}
}
}
return a.Apply(key, v)
}
// Connect puts magpie into the agent's config, the Agents page's switch
// (the owner: an agent is connected to magpie, and its models are picked in
// the agent): the agent's main field goes to one of magpie's models — the
// one serving the model it is on now, else the first — or to magpie itself
// for an app whose one setting is magpie as its provider. Setting it writes
// the provider and the whole catalog in, so the agent's own model list has
// every one of magpie's models. An agent that can keep the model it is on
// (Join) keeps it. An agent already connected is left as it is.
// NoModelsError is Connect's answer while magpie has no models to give
// the agent: no provider or subscription has been added yet. The GUI says
// it in the reader's language by its code (no_models).
type NoModelsError struct{ Agent string }
func (e *NoModelsError) Error() string {
return "Add a provider or subscription in magpie first, then connect " + e.Agent
}
func (a *Agent) Connect() error {
_, err := a.ConnectHow()
return err
}
// Connection is how Connect chose the model an agent starts on, for the
// Agents page to say it (#726: Codex showed its last pick, Claude Code a
// Sonnet through magpie and Antigravity some model, with nothing saying
// why). How is one of:
//
// - "kept": it was connected already, left as it was
// - "again": the models it was on when it was switched off, put back
// - "joined": the model it is on stays its own (Codex signed in with
// ChatGPT), magpie's models join its list
// - "magpie": an app whose one setting is magpie as its provider
// - "same": the model it is on now, through magpie
// - "alike": the model its alias (opus) stands for, through magpie
// - "default": on its own default with no model set: the first of its
// own models in its list (the newest, for Claude Code), through magpie
// - "first": magpie has none of the model it is on: the first on the
// account it is signed in to, else a subscription's, else the first
//
// Field and Value are the field set and what it was set to, where Connect
// picked one model ("" for kept, again and joined).
type Connection struct {
How string `json:"how"`
Field string `json:"field,omitempty"`
Value string `json:"value,omitempty"`
}
// ConnectHow is Connect, saying how the model was chosen.
func (a *Agent) ConnectHow() (Connection, error) {
if a.Native != nil {
return Connection{How: "joined"}, a.Native.Connect()
}
if len(a.Fields) == 0 || a.Wired() {
return Connection{How: "kept"}, nil
}
return a.connect()
}
func (a *Agent) connect() (Connection, error) {
// what it is on now, for Disconnect to put back what it can't
// otherwise (Goose's own model)
now := a.Values()
b, _ := json.Marshal(now)
stash(map[string]string{a.ID + ".connect.was": string(b)})
// switched off and on again: the models it was on, as they were
if ok, err := a.reconnect(now); ok || err != nil {
return Connection{How: "again"}, err
}
// the model it is on stays, where the agent can have magpie's models
// beside it (the owner: Codex keeps its own last pick)
if a.Join != nil {
if ok, err := a.Join(); ok || err != nil {
return Connection{How: "joined"}, err
}
}
// the field magpie is picked in: the one listing magpie's models (for
// Gemini CLI its model, its first field being how it signs in), else one
// whose value is magpie itself
vals := a.Values()
var f *Field
var opts []Option
for pass := 0; pass < 2 && f == nil; pass++ {
for i := range a.Fields {
if a.Fields[i].Options == nil {
continue
}
list := a.Fields[i].Options(vals)
for _, o := range list {
if pass == 0 && o.Ref != "" || pass == 1 && o.Value == magpieID {
f, opts = &a.Fields[i], list
break
}
}
if f != nil {
break
}
}
}
if f == nil {
// nothing to connect it to yet: no provider or subscription added,
// which said only that it can't be connected (Tystem on Discord)
if shown, hidden := provider.CatalogFor(a.ListsFor()); len(shown)+len(hidden) == 0 {
return Connection{}, &NoModelsError{Agent: a.Name}
}
return Connection{}, fmt.Errorf("%s can't be connected to magpie", a.Name)
}
cur := connectWas(vals[f.Key], opts)
if cur == "" && f.Key != a.Fields[0].Key {
cur = connectWas(a.Values()[a.Fields[0].Key], nil)
}
onDefault := cur == ""
if cur == "" {
// on its default: the first of its own models
for _, o := range opts {
if o.Ref == "" && o.Group == a.Name && o.Value != magpieID {
cur = connectWas(o.Value, opts)
break
}
}
}
// the model it is on now, through magpie (or of its family, for an
// alias): on the account it is signed in to (its vendor's) first, then
// on a subscription, before a key's; else its own vendor's; else a
// subscription's; else the first
var pick, same, alike, own, sub, group string
sameRank, alikeRank := -1, -1
for _, o := range opts {
if o.Value == magpieID {
return Connection{How: "magpie", Field: f.Key, Value: magpieID}, a.Apply(f.Key, magpieID)
}
if o.Ref != "" && o.Group == RoutingGroups && group == "" {
group = o.Value
}
if o.Ref == "" || o.Group == RoutingGroups {
continue
}
rank := 0
if o.own || o.Group == a.Name {
rank = 2
} else if o.sub {
rank = 1
}
if pick == "" {
pick = o.Value
}
id := o.Ref[strings.LastIndex(o.Ref, "/")+1:]
if cur != "" && (id == cur || strings.TrimSuffix(id, "[1m]") == cur) && rank > sameRank {
same, sameRank = o.Value, rank
}
// an alias (opus) no label read: the model of that family
if cur != "" && !strings.Contains(cur, "-") && strings.Contains(id+"-", "-"+cur+"-") && rank > alikeRank {
alike, alikeRank = o.Value, rank
}
if own == "" && (o.own || o.Group == a.Name) {
own = o.Value
}
if sub == "" && o.sub {
sub = o.Value
}
}
// the model it is on, of its own, as magpie serves it on a sign-in of
// the user's, which its field lists as its own (Codex's on a ChatGPT
// account it can't join, #940)
if same == "" && alike == "" && cur != "" && a.OwnVia != nil {
same = a.OwnVia(cur)
}
how := "first"
switch {
case same != "":
pick, how = same, "same"
if onDefault {
how = "default"
}
case alike != "":
pick, how = alike, "alike"
case own != "":
pick = own
case sub != "":
pick = sub
case pick == "" && group != "":
// only routing groups are shown it (its models hidden, or its own
// account's alone beside them): the first group (#939)
pick = group
}
if pick == "" {
return Connection{}, fmt.Errorf("magpie has no models %s can use: add a subscription or a provider first", a.Name)
}
return Connection{How: how, Field: f.Key, Value: pick}, a.Apply(f.Key, pick)
}
// connectWas is the model an agent is on before it is connected, as a
// model id: its own option's alias read through the label that names the
// model ("opus · claude-opus-5-5"), a provider's prefix taken off.
func connectWas(v string, opts []Option) string {
for _, o := range opts {
if o.Value == v && o.Ref == "" {
if _, id, ok := strings.Cut(o.Label, " · "); ok {
v = strings.TrimSpace(id)
}
break
}
}
if i := strings.LastIndex(v, "/"); i >= 0 {
v = v[i+1:]
}
return strings.TrimSpace(v)
}
// Disconnect takes magpie out of the agent's config and puts back what the
// user had, the Agents page's "Disconnect from magpie" (Fate on Discord:
// picking the agent's default did it, but nothing said so). Unwire first,
// for an agent whose default would leave it as installed; then each field
// still on magpie goes to its default, as picking it does, and so does
// each one magpie set that reads as magpie set it (an effort). What magpie
// remembered setting is forgotten.
func (a *Agent) Disconnect() error {
if a.Native != nil {
plan, err := a.Native.Disconnect()
if err != nil {
return err
}
return a.Native.Execute(plan)
}
if !a.Wired() {
return nil
}
before, rec := a.Values(), appliedOf(a.ID)
if a.Unwire != nil {
if err := a.Unwire(); err != nil {
return err
}
}
for _, f := range a.Fields {
vals := a.Values()
v := vals[f.Key]
// set by magpie: as it set it, or its group by the model's name
set := v != "" && (rec.Fields[f.Key] == v || sameGroup(rec.Fields[f.Key], v)) && before[f.Key] == v
if v == "" || !set && v != magpieID && !magpieValue(a, f, v, vals) {
continue
}
if err := f.Set(""); err != nil {
return fmt.Errorf("%s: %w", f.Label, err)
}
}
// a field its own Set left empty goes back to what it was on before
// Connect (Goose's own provider and model)
var was map[string]string
json.Unmarshal([]byte(unstash(a.ID+".connect.was")), &was)
after := a.Values()
for _, f := range a.Fields {
if w := was[f.Key]; w != "" && after[f.Key] == "" && w != magpieID && !magpieValue(a, f, w, was) {
if err := f.Set(w); err != nil {
return fmt.Errorf("%s: %w", f.Label, err)
}
}
}
// what it was on through magpie, for switching it on again
back := reconnection{Magpie: map[string]string{}, Left: a.Values()}
for _, f := range a.Fields {
if v := before[f.Key]; v != back.Left[f.Key] {
back.Magpie[f.Key] = v
if v == magpieID || magpieValue(a, f, v, before) {
back.Routed = append(back.Routed, f.Key)
}
}
}
b, _ := json.Marshal(back)
stash(map[string]string{a.ID + ".reconnect": string(b)})
a.Keep()
return nil
}
// DisconnectOffline restores a native agent's saved files only after an
// explicit offline choice. Legacy callers of Disconnect keep live semantics.
func (a *Agent) DisconnectOffline() error {
if a.Native == nil || a.Native.ExecuteOffline == nil {
return fmt.Errorf("%s does not support offline disconnect", a.Name)
}
plan, err := a.Native.Disconnect()
if err != nil {
return err
}
return a.Native.ExecuteOffline(plan)
}
// reconnection is what Disconnect took an agent off: the values of its
// fields it changed (Routed: those that were magpie's) and what it left
// every field at.
type reconnection struct {
Magpie map[string]string `json:"magpie"`
Routed []string `json:"routed,omitempty"`
Left map[string]string `json:"left"`
}
// reconnect puts back the models an agent was on when it was disconnected
// (the owner: an agent remembers its last pick), if nothing has changed
// it since and magpie still routes them; false when it doesn't apply.
func (a *Agent) reconnect(now map[string]string) (bool, error) {
var rec reconnection
if json.Unmarshal([]byte(unstash(a.ID+".reconnect")), &rec) != nil || len(rec.Routed) == 0 {
return false, nil
}
for k, v := range rec.Left {
if now[k] != v {
return false, nil
}
}
for _, k := range rec.Routed {
f := a.Field(k)
if f == nil || rec.Magpie[k] != magpieID && !magpieValue(a, *f, rec.Magpie[k], rec.Magpie) {
return false, nil
}
}
// the models on magpie first, as Connect sets them (setting Gemini
// CLI's model signs it in through magpie, which its sign-in field
// can't), then the rest that aren't so by then
var keys []string
for _, k := range rec.Routed {
if rec.Magpie[k] != magpieID {
keys = append(keys, k)
}
}
for _, f := range a.Fields {
if _, ok := rec.Magpie[f.Key]; ok && !slices.Contains(keys, f.Key) {
keys = append(keys, f.Key)
}
}
for _, k := range keys {
v := rec.Magpie[k]
if v != a.Values()[k] {
if err := a.Apply(k, v); err != nil {
return true, err
}
} else {
// set along with the model (Codex's effort): magpie's as before,
// for Disconnect to take out again
record(a.ID, k, v)
}
}
return a.Wired(), nil
}
// lastJSONLTime reads the newest Unix timestamp (seconds or milliseconds)
// under key in the last lines of a prompt log, passing over the lines whose
// text (under textKey) is a slash command: those an agent answers itself.
// Zero if there is none.
func lastJSONLTime(path, key, textKey string) time.Time {
f, err := os.Open(path)
if err != nil {
return time.Time{}
}
defer f.Close()
const tail = 64 << 10
if st, err := f.Stat(); err == nil && st.Size() > tail {
f.Seek(st.Size()-tail, 0)
}
var last int64
sc := bufio.NewScanner(f)
sc.Buffer(make([]byte, 64<<10), 4<<20)
for sc.Scan() {
var m map[string]json.RawMessage
if json.Unmarshal(sc.Bytes(), &m) != nil {
continue
}
var text string
if json.Unmarshal(m[textKey], &text) == nil && strings.HasPrefix(strings.TrimSpace(text), "/") {
continue
}
n, err := strconv.ParseInt(string(m[key]), 10, 64)
if err != nil {
continue
}
if n > 1e12 {
n /= 1000
}
last = max(last, n)
}
if last == 0 {
return time.Time{}
}
return time.Unix(last, 0)
}
// sameHost: two URLs name the same server, whatever their scheme (Codex
// asks magpie's http gateway over ws) or path.
func sameHost(a, b string) bool { return hostOf(a) == hostOf(b) }
// wiringOff checks what magpie wrote into an agent's config to reach the
// gateway — pairs of key and value, read with get — and says which no longer
// holds, "" when all do. A URL is quoted; a key's value never is.
func wiringOff(name, file string, get func(string) (string, bool), kvs ...string) string {
for i := 0; i+1 < len(kvs); i += 2 {
k, want := kvs[i], kvs[i+1]
v, _ := get(k)
if v == want {
continue
}
where := name + "'s " + k + " (" + filepath.Base(file) + ")"
switch {
case v == "":
return where + " is gone, so it no longer reaches magpie"
case strings.Contains(strings.ToLower(k), "url"):
return where + " is " + v + ", not magpie's gateway at " + want
default:
return where + " was changed, so magpie's gateway won't take its requests"
}
}
return ""
}