Skip to content

accounts: do not restore a forgotten CLI identity after a failed status check - #136

Open
MumuTW wants to merge 3316 commits into
yetone:mainfrom
MumuTW:fix/cli-identity-unanswered
Open

MumuTW wants to merge 3316 commits into
yetone:mainfrom
MumuTW:fix/cli-identity-unanswered

Conversation

@MumuTW

@MumuTW MumuTW commented Sep 28, 2026 •

Copy link
Copy Markdown

An explicit account change invalidates the CLI identity, but if the next ask fails, forget() still serves the previous user/plan/ok. Its disk entry can also restore that account on restart. This update clears the served fields and removes only that CLI's persisted identity; the other CLI's identity stays intact. An ordinary failed background refresh continues to preserve the last answer.

Refreshed against upstream a15e576564cb90d4d027e8b8cb7821472de0a3fc. The earlier unanswered-ask handling, including interpreting Devin output before its exit status, is already on current main (askDevinStatusAt). The update keeps that implementation and adds explicit signed-in/signed-out nonzero-exit coverage instead of reintroducing the old parser. The remaining production change is cliIdentity.forget, shared by the built-in Cursor and Devin identity caches; moved plugin accounts use their own identity paths.

Regression evidence: on unmodified current-main production code, TestCLIIdentityForgetThenUnanswered fails with forget followed by failed ask served old account: "old@example.com" "Pro" true; it passes after the fix. It also checks restart behavior and preservation of another CLI's disk entry. TestCLIIdentityForgetDropsInflightAnswer checks that a pre-invalidation ask cannot restore the old identity. The documented failure behavior is updated in docs/subsystems/providers-accounts.md.

The first complete suite failed only in internal/agent.TestPrimeAgent: it expects no detected agent before writing config, but Agent.Detected correctly finds an installed prime-agent on PATH. This was independently reproduced on untouched upstream a15e5765. A separate test-only commit isolates PATH for that config-detection fixture; the test then passes with -race. No Prime Agent production behavior changes.

Validation on the final tree, using Go 1.26.8:

  • go test -tags nogui ./... -timeout=45m: PASS (exit 0, 892.1s).
  • CLI identity / Devin status tests with -race: PASS.
  • Isolated TestPrimeAgent with -race: PASS.
  • go vet ./... (macOS), GOOS=linux go vet -tags nogui ./..., GOOS=windows go vet ./...: PASS. After the test-only isolation change, the affected agent package was vetted again for all three platforms: PASS.
  • macOS, Linux nogui and Windows builds: PASS.
  • Changed Go files formatted; git diff --check: clean.

The initial short-timeout full run was intentionally stopped, not counted as a pass. The next full run exposed the Prime Agent fixture issue above; the final complete run after its fix passed. Tests use an isolated HOME with Go caches pinned. No real vendor account or GUI test was run; there is no UI change. Cross-platform builds/vet do not establish native Linux or Windows runtime behavior.


🎬 界面预览

CI 用这个 PR 的代码构建并真实运行 magpie(沙盒环境,配置了真实的 DeepSeek key),按改动自动操作、截图和录屏 · 5c80760 · 运行记录

这次没录成(skipped)。详见运行记录。

@yetone

yetone commented Sep 28, 2026

Copy link
Copy Markdown
Owner

@MumuTW Thanks — this is a real bug and the approach (an ask that didn't answer leaves what was served, retried a minute later) is right. I reviewed 0aaae4c every line and ran it: go vet clean, the provider tests pass (the -race failure in TestKiroAccount is on main too), and in a sandbox with a stand-in devin on PATH, main vs this PR:

scenario main PR
fresh start, devin hangs keeps {"ok":false}, later providers shows no Devin nothing kept, Devin shows
kept signed in, then hang overwritten ok:false kept
kept signed in, then Not logged in. exit 0 ok:false ok:false ✓
kept signed in, then Not logged in. exit 1 ok:false kept signed in

Load: 40 concurrent requests with a failing devin spawn it once, once more ~60s later — no pile-up, never blocks.

Two things before I merge:

  1. A sign-out that exits non-zero keeps the account forever. askDevinIdentity returns the error before looking at the output, so devin auth status printing Not logged in. with a non-zero exit (or failing because the token was revoked) never counts as an answer: the account stays shown and on disk while it can't be used. The exit code for signed-out isn't something we can rely on. Please read the output first — Not logged in → signed out, Logged in → parse — and only treat it as "didn't answer" when neither appears; a missing DevinCredentialsPath() file can also count as signed out whatever the exit code.
  2. forget() then a failed ask serves the old account. forget() doesn't clear user/plan/ok, so after a sign-in/out a failed ask leaves the previous account served for another minute (and kept). Reproduced: keep old@example.com → forget() → ask returns an error → get() gives old@example.com ok=true (main: ok=false). Clearing them in forget() (or not keeping across a failed first ask after it) fixes it — a test for it would be good.

Not blocking, just so the description is accurate: cursor-agent's about returns userEmail: null with exit 0 when its own network call fails, so a signed-in Cursor offline is still recorded as signed out. If you want to cover it, null email plus a token from readCursorToken() could count as no answer.

Push an update and I'll run it again.

JasonBuildAI and others added 29 commits October 15, 2026 03:34
… kept without saying so (yetone#2230, JasonBuildAI)

A vendor that keeps the prompt cached by itself and reports nothing of it —
OpenAI's automatic prefix cache, Gemini — says neither a read nor a write of
the turn that built the cache. affine judged that turn's stick by what the
reply read and wrote (0 + 0), found it under cacheWorth and moved the
conversation's second turn as "no-cache": the whole prompt went afresh to
another key or account and was paid for in full, on every turn after the
first, for anyone with more than one key or account and Affinity auto (the
default).

7f968b95b named this as not covered: "a vendor that caches without reporting
a write (OpenAI's automatic prefix cache, Gemini) still reports CacheWrite 0
on a first turn, so its second turn may still move".

What such a vendor holds of the conversation after a turn is the prompt that
turn was sent, which is what its cache now has. answered takes the reply's
usage rather than its two cache counts, and where the reply reports neither
a read nor a write, the prompt it was sent (Input) stands for the write: the
second turn stays to read it, as it does for a turn that wrote its cache. A
reply that reports no usage at all (Input 0) counts as no cache, as before,
and a prompt under cacheWorth (1024) still moves. A read or a write the
vendor does report is used as it was, and resting, spent, cold, strict, turn
and rotate are judged before this, so nothing else changes.

The observable change: with Affinity auto, a conversation whose last reply
reported neither a cache read nor a cache write, and was sent at least 1024
tokens, is kept where it was (Why "cache", Kept true) rather than moved
(Why "no-cache"). Its stick, affinity.json and the trace's
Affinity.CacheWrite carry the prompt sent, and Affinity.CacheRead stays 0,
which is what the Routing page shows as read and written; the page itself is
unchanged.

Tests:
- affinity_silent_cache_test.go (new): TestAffinityKeepsACacheTheVendorKept
  Silently. Two keys, least-used routing, and a Chat vendor reporting
  prompt_tokens 3000 with cached_tokens 0 — what OpenAI and Gemini send of a
  first turn — keeps the second turn on k1 as "cache", with the trace
  telling CacheRead 0 and CacheWrite 3000; a vendor reporting no usage at
  all still moves to k2 as "no-cache".
- TestAffinityKeepsACacheJustWritten, its "too little either way" (300/300,
  no-cache) case included, passes as it did.

Run:
- go test -tags nogui -count=1 -run TestAffinity ./internal/gateway: ok,
  80.982s (TestAffinity 39.36s, TestAffinityKeepsACacheJustWritten 26.54s,
  TestAffinityKeepsACacheTheVendorKeptSilently 9.68s).
- go test -tags nogui -count=1 -timeout 45m ./...: every package passes
  except 21 tests red on this Windows machine — agent 4
  (TestInstallWithoutNodeParses, TestWSLClaudeFollowerOnTheMainModelIsNoDrift,
  TestCopilotFolderFromAnIDEPluginIsNotTheCLI,
  TestQoderSettingsFromAnIDEPluginIsNotTheCLI), gui 4
  (TestGatewayHeldByAContainer, TestGatewayFixQuitsOnlyAMagpie,
  TestFXStateDoesNotWait, TestUpdateThatWontStart), library 3
  (TestAgentsSkillsFolders, TestRemoveFoundSkill,
  TestHooksCodexTrustMovesWithTheGroups), gateway 2 (TestClaudeAsRootEnv,
  TestSweepBridgeProjectsTakesOnlyTheBridgesFolders) and one each in backup,
  davsync, pluginrt, provider, sessions, update and usage. A fresh worktree
  at the base e98440a, without this change, fails the same 20 of them. The
  21st, gui's TestPurposeAssetMatchesUsage, fails only in this checkout,
  whose internal/gui/assets/purposes.js is CRLF (git says i/lf w/crlf) and so
  keeps the "\r" its TrimSuffix(";\n") doesn't cut; the file is not in this
  diff and the test passes in the base worktree.
- gofmt -l on the changed files: nothing. go vet ./..., GOOS=linux go vet
  -tags nogui ./..., GOOS=windows go vet ./...: nothing. go build,
  GOOS=linux go build -tags nogui, GOOS=windows go build: ok.
- Broken by hand: with the substitution removed the new test fails —
  "silent cache: affinity &{... CacheRead:0 CacheWrite:0 ... Why:no-cache},
  want cache". At the base commit the case fails the same way, the second
  turn going to k2.

Not tried: the Playwright suite (no GUI asset changed; the Routing page
reads the same JSON) and a live OpenAI or Gemini key.
…a restart, and a prompt too small still moves (yetone#2230)

JasonBuildAI's test covers OpenAI's Chat shape and a reply with no usage.
TestAffinitySilentCacheGeminiRestartAndSmall adds the cases the same
substitution has to hold for: Gemini's usageMetadata (promptTokenCount,
cachedContentTokenCount 0 on a first turn), the stick read back from
affinity.json after the in-memory one is dropped, and a 600-token prompt,
under cacheWorth, which still moves as "no-cache". Each case also checks
the trace tells CacheRead 0 and CacheWrite equal to the prompt sent.

Run:
- With the substitution in answered taken out, both this test and
  TestAffinityKeepsACacheTheVendorKeptSilently fail "Why:no-cache ...
  want cache" (the second turn went to k2). With it: PASS.
- go test -tags nogui -run TestAffinity ./internal/gateway: ok.
- go test -tags nogui ./... on macOS with this series (at 1b96dcc96 before the
  rebase): every package ok. gofmt -l, go vet ./... on darwin, linux
  (nogui) and windows, and the three builds: clean.
…effort of their own

After yetone#2081 gave Desktop a subagent model, its effort still came from the
request. Add subagent_effort beside that model, using the existing effort
slider. The live setting DesktopSubagentEffort belongs to magpie; no
Claude client file or binary is changed.

Empty preserves the request's effort, including disabled thinking. A pick
applies only to Desktop requests with an agent id and tools, independently
of model replacement: unset, equal and unavailable model picks all work.
Reuse the attempt-level fitting; fixed group-member efforts and reasonless
models keep their existing precedence. count_tokens applies the pick after
unmasked-body restoration. A Codex SubagentPick is not evidence of a
Desktop tool turn.

Route.Effort keeps the incoming level, SubagentEffort records the configured
pick, and tries/usage keep the actual level. Routing explains the pick
even without a model replacement. Options follow the target model; a
saved level survives a model change. POST /api/settings preserves it.
Disconnected preferences, Disconnect/reconnect and restored settings work;
a Live pick gives restart advice only when it actually connects the agent.
Codex's own settings and default-effort explanation are unchanged.

Verification on b9c589e plus this change, with temp HOME and pinned Go
caches:
- go test -tags nogui ./...: 53 packages pass.
- Gateway effort tests under -race -count=20: pass.
- gofmt -l: empty; darwin, linux nogui and windows vet/build: exit 0.
- 23 intentional Go behavior breaks produce real FAILs, then pass
  restored: application, scope, model independence, Default/none,
  disabled thinking, fitting/member priority, counts, next request,
  trace, saving/validation, Live notices and model-dependent options.
- Three UI breaks fail in both engines at 1100/420: default explanation,
  missing effort story and missing translation. Restored: Desktop's new
  controls/story matrix passes 20/20 across five languages. Desktop,
  Codex, tier/effort and translation regressions pass 97/97.
- Desktop-bundled Claude Code 2.1.295 runs real Agent calls through the
  built gateway, temporary HOME and loopback-only sandbox, all replies
  local fixtures. Low/high/Default sends its child low/high/xhigh on m2;
  its lead remains xhigh on m1. All three runs exit 0.

The expanded Routing run is not green: Chromium 510/525, WebKit 485/525.
These failures reproduce on untouched b9c589e, including targeted reruns.
The day/row anchor assertions conflict with yetone#2134's at-top rule: undoing
that one guard makes all 12 tests pass. A narrow request lies below the
viewport; user wheel input makes the failed click pass. WorkBuddy's wheel
pointer/hidden-button failures overlap yetone#2115; moving the wheel to the
view gutter fixes 3/4 selected cases, one WebKit 420 case remains.
The held-member clipping assertion also reproduces on base; its timing
cause is not established. This baseline evidence is reported separately
in the follow-up feature issue, not counted as passed or fixed here.

Not run: the complete GUI suite, the Electron window/Cowork, native
Linux/Windows or WebKitGTK. Cross-builds are not runtime checks.

Reported-by: Koohoko
… back

A pick on the Agents page redraws its square at once and writes behind
it, queued (commit, and the effort slider's onchange). The test read the
fixture's last write as soon as the square changed, so a slow write raced
it: in a broad Routing run, six files at a time, Chromium's en 1100 case
read "high" for the subagent effort just set back to Default.

Each pick now waits for its /api/set answer, which the fixture sends
after noting the write. The app is unchanged.

Checked with the fixture's write held 300 ms (a probe, not committed):
the test before this fails 4 of 4 (Chromium and WebKit, 1100 and 420,
en) at "Desktop pick posts the catalog id", the model pick yetone#2081 brought;
with this change it passes 4 of 4. Unheld, the race is rare: six runs of
the whole file at once pass 120/120 before this and after it. The file
alone: 20/20. The three UI breaks of the subagent effort still fail 4 of
4 (zh), and pass restored.
…it does Codex's

Codex names a subagent's request (x-openai-subagent: collab_spawn), and
the Routing page tags its row Subagent, with why it went where it did.
Claude Code names no kind, so Claude Desktop's subagent turns, which
magpie now moves to their own model and effort, read as the lead's: a
Desktop session's rows on one routing group could not be told apart.

Claude Code 2.1.295 sends x-claude-code-agent-id from every agent context
but the main one. Its x-claude-code-request-class and
x-claude-code-agent-type hints go only to a first-party base URL or with
CLAUDE_CODE_GATEWAY_HINT_HEADERS, so magpie doesn't get them.

- subagentTurnOf keeps the id in Route.SubagentID for a request that
  offers tools and names no kind: the turn desktopSubagentPick and
  desktopSubagentEffort take. Desktop's and terminal Claude Code's turns
  are kept, moved or not. The lead's turns, a subagent's tool-less helper
  calls (yetone#2081: Claude Code's housekeeping) and a blank id are not. A
  kind wins over the id.
- A Desktop pick or effort that found the turn is trusted, so the body
  isn't decoded again; a request without the id isn't decoded at all.
  Only a subagent's turn with neither setting reads its tools once.
- Kind stays empty (yetone#2081), so the ledger, the Usage page and the
  Purpose filter keep the turn unmarked. The ledger's Subagent already
  keeps the same id, on the helper calls too.
- routing.js tags it through tagKind in the request list (by request and
  by session), the story, the replay and the tray panel, with Codex's
  words and reasons (kindWhy's collab_spawn: moved or kept, within a
  routing group, on the model asked for). No new strings.
- The day's history keeps the id. A route kept before it has no tag, and
  its moved/kept story reads as before.

Verified at this head, on b9c589e, under a temporary HOME with Go's
caches pinned:
- Each new test fails with its guarantee broken by hand (a real FAIL, not
  a build error), then passes restored. Go, 10: the id never set; tools,
  kind or the trusted pick ignored; tools decoded before the header (the
  lead's request then allocates 10 times, 1 for its headers); the route
  not given the id; Desktop only (terminal fails); every request trusted
  (the helpers fail); the JSON key renamed; the field left out of the
  history. GUI, 8, each 0 pass / 4 fail (Chromium and WebKit, 1100 and
  420, zh) at its own assertion: tagKind ignoring the id; the list, the
  story, the replay or the panel testing Kind alone; kindWhy's
  collab_spawn reasons on Kind alone; the id over a kind; the Purpose
  filter following the tag.
- go test -tags nogui ./...: 53 packages ok.
- go test -race -count=20 -run
  '^(TestSubagentTurn|TestHistoryKeepsSubagentID|TestDesktopSubagent)'
  ./internal/gateway: ok.
- gofmt -l: nothing. go vet ./..., GOOS=linux go vet -tags nogui ./...,
  GOOS=windows go vet ./..., go build, GOOS=linux go build -tags nogui,
  GOOS=windows go build: exit 0.
- claude-subagent-tag.test.cjs: 20/20 (Chromium and WebKit; en, zh,
  zh-TW, ja, de; 1100 and 420), three runs in a row.
- Desktop-bundled Claude Code 2.1.295 through the built gateway, a
  temporary HOME, a loopback-only sandbox and local fixture replies, its
  Agent tool spawning a real subagent: with nothing set, with a subagent
  model, and as terminal Claude Code, each run kept 4 routes; the
  subagent's one turn has the id the client sent upstream, the lead's 3
  requests none, Kind empty; the model pick moved it to m2. No tool-less
  helper call came in these runs; the Go tests cover them.
- The Routing GUI tests around it, 91 files (every routing-*, the tray
  panel's two, purpose-filter, click-scroll, the Codex and Desktop
  subagent files, subagent-effort, search-instead, search-mcp, vision-mcp,
  remote-group-served, reasoning-speed, usage-ledger, sessions-overview
  and the new one), six files at a time: Chromium 602/617, WebKit
  572/617. Each failure fails without this commit too. On b9c589e
  untouched (checked for the effort commit): routing-days-fit,
  routing-failed-open and routing-side-calls in both engines (their
  position checks against yetone#2134's at-top rule), routing-wb-restricted
  (yetone#2115's wheel), and at 420 in WebKit routing-agent-gone,
  routing-agent-hue-not-red, routing-rest-off and routing-held-member.
  On 6926b7c: search-instead in WebKit, 5 of 5 alone, and
  routing-context-fold's picked request in WebKit, a row out of view, 2
  of 5 runs alone there and 3 of 5 here.

Not run: the whole GUI suite, a real Claude Desktop window or Cowork,
Linux and Windows machines (GOOS vet and build only), WebKitGTK.
…ch is a plugin now (yetone#2232)

Koohoko's patch was written before Routing moved into plugins/routing
(eb7199e): its one new Routing string went into the core i18n.js. It is
used only by plugins/routing/client.js, so its zh, zh-TW, ja and de go in
plugins/routing/i18n.js, beside the page's other words, and the core
scripts' size budget (91b9065) isn't spent on it.

Run: claude-subagent-tag, desktop-subagent-model, subagent-effort,
codex-subagent-model, routing-kind, purpose-filter, routing-purpose-state,
i18n-keys and agent-notice-lang (Chromium and WebKit, 1100 and 420, the
five languages where each test covers them): 111 pass, 0 fail.
…nside it, a long model's name ellipsized, not the chips cut off (碳碳双键 on Discord)

碳碳双键's screenshot (zh, ~770px): in a group's editor the 模型 rows ran
past the editor's right edge, 跟随分组推理 / 标准速度 cut off, and the 模式
row didn't line up with them. .fallback and its .fbl are grids with an
auto track, which grows to the widest row's min-content: one long model
id (nvidia/nemotron-3-super-120b-a12b:free) widened the whole column.

The track is minmax(0, 1fr) now, so the column is the editor's width. A
member row's name and its provider shrink and ellipsize (the provider
first), with the whole name as a title on hover; when the chips still
don't fit they wrap to a second line, right-aligned. The rules group
(.rt-rules) and the phone's own row layout (body.web, which wraps and
starts at the left) are left as they were; the new rules sit in :where()
so the phone's still win.

New test group-editor-fits.test.cjs: Chromium and WebKit, the desktop
window and the web page, 1100, 770 and 420 wide, en, zh, zh-TW, ja, de.
It checks that nothing in the members or patterns box passes the
editor's edges, no chip is cut, the page doesn't scroll sideways, the
pattern row's edges are the members box's, and the long name is a title.
It fails on origin/main 0050140 in both engines (desktop 1100 en
already: past the editor's edge) and passes here. Screenshots at zh 770
and de 770 desktop and zh 420 web compared with origin's by eye.

Run: the group-* and routing rules/weight/columns/shape files, 258 of 258;
the full GUI suite (node --test, concurrency 6, 9679 tests): 34 red,
in quota-count-wrap, quota-name-translated, refusal-retries-setting,
routing-held-member, usage-agents-hide, usage-fold (red alone the same
on the unchanged base 8e8316e; filed as yetone#2228, yetone#2163, yetone#2252, yetone#2211 and
the new yetone#2258) and four that pass alone (deprecation-notice,
login-import, name-suffix-no-names, remote-magpie).
…awn_agent task_name, its nickname shows apart, and Usage requests say which calls were compactions (yetone#1591, Hogeexxl)

Hogeexxl's change (his fork branch codex/codex-session-details, f7c176c),
cherry-picked with his authorship:

- A Codex child thread whose title is still empty after the existing
  precedence takes the task_name of the parent's spawn_agent call, linked by
  the call id and the SubAgentActivity "started" item that names the child's
  agent_thread_id. Conflicting links leave the title empty.
- session_meta.agent_nickname is exposed as `nickname` on the list, one,
  manage and overview responses, beside the title, not in place of it.
- GET /api/usage/requests and /v1/magpie/usage/requests carry `compaction`
  (true/false) for Codex calls with native evidence, and leave it out
  otherwise. A gateway row matched by a unique response_id takes the marker
  and keeps its own model, usage, cost and kind.
- Flags bits 1024 (rowCompactionKnown) and 2048 (rowCompaction) are new.
  TestRowFlagsApart covers them, and they don't overlap 1..512. The sessions
  cache goes to version 21 and the calls cache to calls-v15.

Review:
- Rollout format checked against Codex's source (openai/codex 669449b):
  - v2 spawn_agent args carry task_name.
  - A paginated rollout persists item_completed with a SubAgentActivity
    item: kind "started", id = the call id, agent_thread_id.
  - SessionMeta has a top-level agent_nickname, which Codex's own list.rs
    and metadata.rs read too.
  - compaction_response_id lives in token_usage_record.
  Gaps in legacy rollouts and in calls without a response_id are fixed in
  the next two commits.
- Privacy: only a title string the user's own Codex wrote and a boolean
  are added to local API responses. Nothing new goes to a vendor or to
  telemetry. The ingest POST parses fields one by one, so it is unchanged.
- No .github, dependency or network changes.

What was run:
- His tests pass: go test -tags nogui ./internal/sessions ./internal/usage,
  and the two new internal/gui API tests.
- Broken by hand, each caught by his tests:
  - the nickname read removed;
  - the conflict checks removed;
  - mergeCodexCompactions broken;
  - the gateway pair's setCodexCompaction removed;
  - the flag read forced to true.
- Not caught:
  - the parent check on the title link (covered by a test in the next
    commit);
  - `copy.codexCompactions = nil` in cloneCodexChunks removed. That line is
    defensive only: the shared chunks never hold the map.
- The gofmt, vet and full-suite record for the three commits is in the
  last one.
… task_name too, and a forked child's copy of its parent's spawns no longer erases its siblings' titles (yetone#1591)

Two gaps in the previous commit's title link:

- Legacy rollouts. Codex's ThreadHistoryMode is Legacy by default. The local
  thread store has only defaulted to Paginated since openai/codex#47900
  (2026-09-24). A legacy rollout doesn't persist item_completed for a started
  SubAgentActivity. It persists an event_msg instead:
  {"type":"sub_agent_activity", "event_id": <call id>, "agent_thread_id",
  "kind":"started", ...}. Its children got no title. That event now links
  the same way, through one helper (codexAgentStarted) shared with the
  item_completed path.
- Forked children. A forked child's rollout repeats its parent's history,
  including the spawns of its siblings. The same call id then linked from
  two files, which counted as a conflict and erased the sibling's title.
  Titles are now keyed by {child, parent}, and only the file of the child's
  own parent_session names it or takes a name away from it.

What was run:
- New tests. On the previous commit each fails with:
  - TestCodexSubagentTaskTitleFromLegacyActivity: `legacy activity title
    for child-started = "", want "Legacy started task"`.
  - TestCodexSubagentTaskTitleComesFromItsOwnParent: `a fork's copy of the
    parent's spawn erased the child's title: ""`.
- Broken by hand: with the parent dropped from the key, the second test
  fails with `a spawn its parent never made named the child: "Only in the
  fork's copy"`.
- go test -tags nogui ./internal/sessions ./internal/usage: ok.
- The event's field names come from Codex's source (protocol
  SubAgentActivityEvent and the rollout persistence policy, openai/codex
  669449b). Not tried: a real legacy-history Codex subagent rollout.
…age requests instead of saying false (yetone#1591)

Codex added token_usage_record and compaction_response_id together, in
openai/codex#41912 (2026-08-31). A call that magpie reads from a
token_count event alone, as in older rollouts, has no response_id. It
can't carry the compaction marker either way. The previous commit
reported such calls as `compaction: false`, which claims they were not
compactions. codexCompactionOf now returns no state for them, so the
ledger and the requests page leave the field out, as they do for any call
with no native evidence.

What was run:
- New test TestCodexCallWithoutResponseIDHasNoCompactionState. On the
  previous commit it fails with `ledger: a call with no response_id claimed
  a compaction state: {..."compaction":false...}`, and with the same message
  for the requests page. It passes now.
- go test -tags nogui ./internal/sessions ./internal/usage: ok.

For all three commits on origin/main 1ad6044:
- gofmt -l on the changed files: clean.
- go vet ./..., GOOS=linux go vet -tags nogui ./..., GOOS=windows go vet
  ./...: ok.
- go build for darwin, linux (nogui) and windows: ok.
- Full suite: go test -tags nogui -timeout 40m ./... (pipefail, sandbox
  HOME): 53 packages ok, exit 0.
- No GUI asset changed, so no Playwright run.
- Not tried: a real Codex subagent rollout (legacy or paginated). The
  formats are from Codex's source, and Hogeexxl checked two real paginated
  subagents.
- Rebased onto origin/main f255a3e (12 commits newer, including yetone#2233 and
  yetone#2234 in internal/sessions; no cache-version clash). Rerun there: go
  test -tags nogui -count=1 ./internal/sessions ./internal/usage
  ./internal/gui, gofmt and vet on both packages: ok.
… while it opens is closed upstream and dashboard usage is collected per account (plugins yetone#130, yetone#131)

0.2.13 (magpie-community/plugins 3351e47) carries two community fixes by
nicegui. yetone#130: a cancel landing between the HTTP/2 head and runOnce's abort
listener was never seen, so the Run kept streaming upstream with its
heartbeat for nobody; it is now closed at each hand-off. yetone#131: the usage
ledger's claimed/owed/flights were keyed by conversation alone, so a
conversation moved to a second Cursor account had that account's own
dashboard event taken as the first account's debt; they are now keyed by
account and conversation. The built-in doesn't share this code, so
un-moved users are unchanged. The cursor mover's min goes to 0.2.13 so
keepMovedCurrent updates every moved user.

Run: npm view @magpie-community/opencode-cursor-auth@0.2.13 version
--prefer-online gives 0.2.13. Plugin: full bun test 1292 pass, 0 fail;
own repros fail on 0.2.12 and pass on 0.2.13. Here: gofmt -l; go vet
./internal/provider for darwin, GOOS=linux -tags nogui, GOOS=windows;
go test -timeout 40m ./... in a sandbox HOME: exit 0. Not run: a real
Cursor account, nor GUI tests (no GUI change).
…idle wait over the body only, not over getting the headers (yetone#2248)

The 400ms context deadline covered the whole request, so on a loaded
machine the gateway's slow response headers ended the test at
http.DefaultClient.Do ("context deadline exceeded") before it reached
what it checks. The 400ms now starts once Do returns (time.AfterFunc
cancel), standing for the client's idle timeout over the body.

Run:
- Reproduced by injecting a 600ms sleep before the fake vendor's first
  byte: old test FAIL keepalive_test.go:202 "context deadline exceeded",
  the issue's error; fixed test passes 3/3 with the delay in.
- Broke the guarded behaviour by hand (a 5ms ticker calling alive() in
  readSSEAlive, so silence is covered), delay still in: FAIL "a silent
  provider was covered for: 30 in_progress events".
- go test -tags nogui -race -count=20 -run of this test and the
  TestQueuedStreamKeptAlive* tests: ok (358s). gofmt -l clean,
  go vet -tags nogui ./internal/gateway (darwin) clean.

Not run: the full go test ./..., GOOS=linux/windows vet. Stopped
because the disk dropped below 10G free.
… the vendor's arrivals by a sequence, not a clock that ticks every ~0.5ms on Windows (yetone#2249)

TestQueuedStreamKeptAliveForASlot checked heard.Before(asked) on
wall-clock times. On Windows time.Now() advances in ~0.5ms ticks, and
the keepalive, the slot being freed and the vendor being asked fit in
one, so heard == asked and the test failed with the right order.
Loosening it to !asked.Before(heard) would pass a broken keepalive: with
queued keepalives off, the first write (the reply) lands in the same ms
as the ask, seen below.

A shared counter (step) now gives each gateway write a place in order,
taken before the write so nothing the agent does on hearing it can come
earlier, and each vendor arrival and answer one too. The sibling checks
use it: TestQueuedStreamKeptAliveForTheMinute's same heard-before-asked
and TestQueuedStreamKeptAliveOnceSentToItsVendor's writeLog.within (its
"kept alive before the vendor was asked" is the same strict compare on
the one write that frees the slot).

Run:
- Reproduced by truncating both clocks to 100ms (forced equal ticks):
  old ForASlot FAIL 5/5 at queue_keepalive_test.go:387 with equal
  times, the issue's message; OnceSentToItsVendor FAIL 5/5 "the agent
  wasn't kept alive before the vendor was asked". With the fix and the
  same truncation, all TestQueuedStream* pass 3/3.
- Broke the guarded behaviour by hand (queuedAlive := func() {} in
  gateway.go): all three tests FAIL at the body check; with the body's
  ": keepalive" check also dropped, each FAILs at the new order check
  (ForASlot :433, ForTheMinute :304, OnceSentToItsVendor :368).
- go test -tags nogui -race -count=20 -run of these and
  TestTranslatedSilenceStillTimesOut: ok (358s). gofmt -l clean,
  go vet -tags nogui ./internal/gateway (darwin) clean.

Not run: the full go test ./..., GOOS=linux/windows vet, the Windows
box. Stopped because the disk dropped below 10G free.
…ning none alone and asks no classifier, so its pick and what went out agree (yetone#2247, sryimnoob123)

The classifier's pick was recorded but never applied when the agent asked for none, costing a classifier call per turn. The outer effort in the routing record is what the agent asked; the effort sent is tries[].effort on the try marked picked:true, which the groups help now says. Thanks to ggbdpq for the reproduction.

Run at this head (on 6485056e1, with the yetone#2245/yetone#2246/yetone#2247 fixes together): gofmt -l clean; go vet ./... for darwin and windows, and linux -tags nogui; the full go test ./... in a sandbox HOME, all green. The new test fails on the old code and passes with the fix.
…er group's (yetone#2246, Gaudi-XIX)

Each candidate now carries the firstToken of the innermost group that sets one; a candidate with none falls back to the requested group's value. TestNestedGroupFirstToken.

Run at this head (on 6485056e1, with the yetone#2245/yetone#2246/yetone#2247 fixes together): gofmt -l clean; go vet ./... for darwin and windows, and linux -tags nogui; the full go test ./... in a sandbox HOME, all green. The new test fails on the old code and passes with the fix.
… Settings › Codex subagents on any model, or to start a new thread when it is on (yetone#2245, qikairo7)

TestSealedTaskNamesSubagentsOnAnyModel.

Run at this head (on 6485056e1, with the yetone#2245/yetone#2246/yetone#2247 fixes together): gofmt -l clean; go vet ./... for darwin and windows, and linux -tags nogui; the full go test ./... in a sandbox HOME, all green. The new test fails on the old code and passes with the fix.
…ot on 2026-10-15, so main is green in UTC (yetone#1591)

3c4b4dc's TestCodexSubagentDetailsReachSessionAPIs held usage's clock at
2026-10-15 12:00 local and wrote the rollouts then, but
/api/sessions/overview reads sessions.StatsFor, whose 7-day window ends at
the real today. On CI (UTC, still 10-14) the fixture day was tomorrow and
the child was left out: "overview token top omitted the active Codex
child", red on macOS and ubuntu at 84f14e5 and aae49d0. In UTC+8 it
passed, and from 10-23 it would have failed everywhere. The test now holds
the clock at time.Now().

What was run, at origin/main 8d2a4a7:
- before: TZ=UTC go test -run TestCodexSubagentDetailsReachSessionAPIs
  ./internal/gui fails as on CI; Asia/Shanghai passes.
- after: the test passes under TZ UTC, Asia/Shanghai, America/Los_Angeles
  and Pacific/Kiritimati; go test -tags nogui ./internal/gui (local TZ and
  UTC) and ./internal/sessions (UTC): ok. go vet ./internal/gui: clean.
- Siblings: no other internal/gui test holds a fixed date and reads
  /api/sessions.
Not run: the rest of ./... (test-only change in one file).
…e open issue, not only in magpie-releases' log (yetone#2122, KaiZe on Discord)

magpie-releases writes the Chinese notes with a DeepSeek call. Its balance
ran out, so v0.1.1233 to v0.1.1242 shipped English only, and Chinese apps
showed English notes until a user reported it. The failure was only a
warning in a log nobody reads.

mirror-release now checks notes.md for the <!-- lang:zh --> marker. When it
is missing, the job comments on the open "Release notes without Chinese"
issue, or files that issue if none is open. If gh fails, the step only
warns, so the release itself never fails here.

What was run: the step's script against a fake gh in four cases (Chinese
present, no issue open, issue already open, gh failing); the real
`gh issue list` query, read-only; a YAML parse of the workflow. Not run:
a real release (the next tag will run it). No Go code changed.
…ith a gateway key, as Origin null does (yetone#2256, ggbdpq)

What changed
- corsGuard refuses (403) an http(s) Origin at a subdomain of localhost
  unless the call carries an enabled gateway key (localhostSubdomain),
  the rule Origin "null" got in yetone#2242. foreignPage still passes it, so a
  page there that holds a key keeps working; neither gets CORS headers.
- Why: Chrome and Firefox resolve *.localhost to loopback themselves,
  but Safari and other WebKit views hand the name to the system
  resolver, which before macOS 26 asks the network's DNS (WebKit bug
  160504, closed MOVED: "implemented in OS frameworks below WebKit",
  2025-09-19; fails on macOS 15.7). A captive portal's or hostile
  network's resolver serves such a page from anywhere, and its "simple"
  text/plain POST spent subscriptions with no key.
- tauri.localhost and wails.localhost stay keyless on Windows only,
  where Tauri's and Wails' webviews serve themselves at those names. On
  macOS and Linux those apps use tauri:// and wails://, so the two names
  are any page's there, and a hostile resolver answers them as readily
  as any other (the residual the report left open). webviewOS
  (runtime.GOOS) lets the test be each system.
- The same page also reached the gateway rebound: at evil.localhost:3425
  once the name turns to 127.0.0.1, its GET carries no Origin, and
  lanGuard's ownHost counted every *.localhost Host as this computer, so
  GET /v1/models and /v1/magpie/quotas answered 200. ownHost no longer
  counts a subdomain of localhost (localhost itself and
  docker.for.mac.localhost still count); a browser at one needs a key.
- TestForeignPagesRefused's tauri.localhost row now sends a key, and
  TestReboundPageRefused expects wails.localhost refused.
- docs/subsystems/gateway-routing.md says both.

Ran
- TestLocalhostSubdomainNeedsAKey (darwin, linux and windows as
  webviewOS; six routes; cross-origin and rebound; the fake vendor never
  reached): fails on origin/main 0050140 (76 errors, e.g. "POST
  /v1/chat/completions from http://evil.localhost with no key: 200"),
  passes with the change.
- Broken by hand, each red: ownHost counting *.localhost again (the
  rebound GETs 200); localhostSubdomain always false; the Windows-only
  exception dropped (tauri.localhost answered keyless on darwin).
- The cors, rebind, LAN, usage-ingest and allow-list tests: ok.
- gofmt -l clean; go vet ./..., GOOS=windows go vet ./...,
  GOOS=linux go vet -tags nogui ./... clean.
- go test -p 4 ./... (macOS): ok in every package but internal/agent and internal/gateway, which hit the 10m package timeout under load ~30 with no test hung (the running test 1-2s old); rerun with -timeout 40m: gateway ok 551s, agent ok 426s.

Not tried: a real Safari on macOS 15 against a resolver that answers
*.localhost; Tauri or Wails apps on Windows.

Co-authored-by: ggbdpq <ggbdpq@gmail.com>
…own Sink, not only the group the agent named (yetone#2255, ggbdpq)

What changed
- planLevel applies a nested group's own Sink (sinkOwn) to its own
  candidates and their trace rows at both places it plans one: the
  weighed units (Smart, usage, pace, weight) and the ordered walk. A
  key or account of that group rate limited with quota left goes behind
  its group's others, within that group's place, as it plans by its own
  routing (yetone#576) and rules. The group the agent asked for still sinks
  the whole after, as before. sinks() still leaves out rotate, weight
  and manual.
- Why: only planGroup read Sink, for the group asked for; an inner
  group's switch did nothing, and its rate-limited account went back
  to the front as soon as its rest ended.
- provider/group.go's comment and gateway-routing.md's Sink row say so.

Ran
- TestNestedGroupSinksItsOwnOrder (outer ordered, smart and by weight;
  three levels deep with the middle group's Sink and with the
  innermost's; inner Sink off; the outer's Sink alone): fails on
  origin/main 0050140 ("order [a b x], want b then a"; "mid's Sink:
  order [a b c y], want [b c a y]"), passes with the change.
- Broken by hand, each red: sinkOwn never sinking (all subtests); only
  the ordered path left unsunk (outer ordered and deeper).
- TestGroupRateLimitedSinks, TestRateLimitedSinksToTheBack (-count=3),
  and -run 'Sink|Nested|OwnRouting|Weighted|Group': ok.
- gofmt -l clean; go vet ./..., GOOS=windows go vet ./...,
  GOOS=linux go vet -tags nogui ./... clean.
- go test -p 4 ./... (macOS): ok in every package but internal/agent and internal/gateway, which hit the 10m package timeout under load ~30 with no test hung (the running test 1-2s old); rerun with -timeout 40m: gateway ok 551s, agent ok 426s.

Not tried: a real nested pool with live accounts. The report's sibling,
a nested group's Affinity read only from the outer group, is left for
its own issue.

Co-authored-by: ggbdpq <ggbdpq@gmail.com>
… repo's dist branch, so a reviewed plugin no longer waits unmerged for an owner's npm 2FA (magpie-community/plugins#84)

npm's Trusted Publishing can't create a package, so a new plugin's first
version needs an owner's 2FA, and reviewed PRs sat unmerged for it. A
trust made after 2026-09-03 also allows stage publish only, which is why
npm refused zed 0.1.7 and zen-free 0.1.7.

The plugins repo now packs every merged version onto its `dist` branch
(scripts/dist.mjs, magpie-community/plugins f5308cd): tgz/<file> and an
index.json with each version's sha512, manifest and README, with no
credentials. magpie still asks npm first. Only when `bun add` fails for a
@magpie-community package at latest or an exact version, and not because
of the user's minimumReleaseAge (that hold is kept), does installDist
fetch the tarball. It refuses the tarball unless its sha512 is the index's,
keeps it in plugins/dist and runs `bun add ./dist/<file>`. A later npm
install replaces it, and pruneDist removes tarballs package.json no longer
names. npmAsk (market version, Update, the hourly check) takes dist's
latest when npm errors or is behind, and Readme falls back to dist's. The
branch is read through jsDelivr when the China mirror switch is on, else
raw.githubusercontent.com. installMu is now held for the whole install,
fallback included.

Run at this head (on origin/main 8d2a4a7):
- go test ./... -count=1: all ok but internal/gateway, which hit the
  600s default timeout inside a file rename. The disk was at 5.3 GB free
  and the load around 22. Rerun alone with -timeout 30m: ok in 559.7s.
- gofmt -l clean. GOOS=windows go vet ./... ok. GOOS=linux go vet ok on
  every magpie package that doesn't import Wails (its Linux files need
  cgo, which can't be cross-compiled from macOS).
- New tests, with real Bun 1.4.2 against a fake npm and a fake dist:
  TestDistInstallsAPackageNPMDoesntHave, TestDistServesAVersionNPMRefused,
  TestDistRefusesATarballNotItsIndexs and TestDistWanted.
  TestUpdateBunHoldsBackSaysWhy now has dist holding the held version too.
- Broken by hand, each went red: no fallback, no sha512 check, no prune,
  no dist version in npmAsk, a loose file regexp, and no minAge guard.
- The real dist branch serves index.json and zed's 0.1.7 tarball from
  raw (200). jsDelivr's @dist answered 200, cold in ~20s.

- Real use, sandbox HOME, live npm and live dist: Info said zed 0.1.7
  (npm has 0.1.6), and Add@0.1.7 installed it from dist, package.json
  naming ./dist/magpie-community-opencode-zed-auth-0.1.7.tgz.

Not tried: the GUI's Add on the live branch, or the China mirror path.
…odex's compaction_trigger as sent and its sealed compaction goes back unchanged; a checkpoint stays with its writer (yetone#1099)

A new per-provider option, off by default. With it on, Codex's
compaction_trigger reaches the provider as Codex sent it, and the
provider's encrypted compaction item goes back to Codex unchanged instead
of magpie's magpie1: summary. Off, nothing changes.

Where it can be on (provider.CarriesCompaction): a provider with a
Responses API (Sub2API and other OpenAI relays, a remote magpie), a
ChatGPT (codex) account, and the built-in GitHub Copilot on its GPT
Responses path. Copilot's Claude models are asked on chat completions, so
they still get magpie's summary (CompactsOn). A group compacts natively
only when every member does.

No silent downgrade:
- A turn carrying a sealed compaction goes only to the key or account that
  wrote it (compactions.json beside affinity.json, 0600, 1024 entries /
  30 days). With none left, Codex gets a 400 saying the conversation was
  compacted natively on <who>, not a summarised or dropped context.
- A provider whose request would have to be translated (not relayed) is
  refused with a 400 instead of losing the item.
- The upstream's own refusal goes back to Codex as it came, one call.
- base_instructions and Codex's identity are untouched.

Turning it on:
- GUI: the provider editor's Compaction row, "Codex compacts on this
  provider" (shown only where the wire can carry it; zh, zh-TW, ja, de).
- CLI: magpie provider set <id> compaction=native|magpie. A provider that
  can't carry it is refused.

Run on origin/main 0050140, then rebased onto 84f14e5 (no file in
common) and reran gofmt, vet and go test for gateway, gui, provider,
cli, usage and sessions: all ok. Rebased again onto 8d2a4a7 (gateway.go
and codex_backend.go changed on both sides, merged clean) and reran
gofmt, vet (darwin, linux nogui, windows) and go test for gateway, cli,
provider and gui: all ok. Last rebased onto 7a3fed6 (only
gateway-routing.md shared, merged clean): gofmt, vet on all three OSes,
and go test -run for the native compaction, sink, CORS, rebind and origin
tests in gateway plus the provider tests: ok.
- gofmt -l: clean. go vet darwin, GOOS=linux -tags nogui, GOOS=windows:
  clean.
- set -o pipefail; go test -timeout 40m ./...: every package ok but
  internal/provider's TestCopilotSecretToolUsername (copilot_cli_test.go:93
  token "", 10.05s), the open load flake yetone#1627; it passes alone -count=5
  and this change doesn't touch it.
- GUI (node --test, chromium + webkit): the provider-editor files, 946
  pass, 0 fail; the new provider-native-compaction.test.cjs runs 5
  languages at 1100 and 420 (60 cases).
- New Go tests: TestNativeCompactionPassesTheTrigger, ...RefusalGoesBack,
  ...StaysWithItsWriter, ...Copilot (gateway), TestCarriesCompaction,
  TestSetNativeCompaction (provider), TestProviderSaveNativeCompaction
  (gui), TestProviderCompactionPair (cli).

Broken by hand, each red, then restored:
1. codexInputAs dropping the trigger even when native: all 4 gateway
   tests fail.
2. checkpoint filter off: StaysWithItsWriter fails.
3. forced relay off: all 4 gateway tests fail (the web_search tool sends
   the turn to translation).
4. GUI save sending nativeCompaction false: the GUI test's first case
   fails.

Not tried: any real upstream (Sub2API, ChatGPT, Copilot). Copilot may
refuse a compacted history holding web_search items; that refusal would
go back to Codex as-is.
…tile, and the tooltip says today's total (ylorn on Discord)

ylorn (Hyprland, 0.1.1241): the StatusNotifierItem's IconPixmap was the
coloured app icon on an opaque white tile, which clashed with a bar's
monochrome icons and became a solid block on bars that tint icons by
alpha; the tooltip said only "magpie".

- Linux now gets the macOS menu bar's template bird (tray.png) in one
  colour on transparency (trayIconFor / linuxTrayIcon, trayglyph.go):
  light, or dark when xdg-desktop-portal's org.freedesktop.appearance
  color-scheme is 2 (prefer light); it is set again on the portal's
  SettingChanged (watchTrayScheme, own D-Bus connection). An unreadable
  portal counts as no preference (light). No IconName is set: a host
  that resolves a -symbolic name but draws it untinted would show a
  black bird on a dark bar with no pixmap fallback. macOS keeps its
  template icon, Windows the coloured app icon.
- With no cards in the menu bar the tooltip is "magpie" over one line of
  today's total, as the panel's Usage tab sums it (trayIdleTip: all
  tokens, cost in Settings' currency, requests), refreshed with the
  cards' loop. Cards picked: unchanged.
- The menu is unchanged (Open / version / Quit): no info lines. The
  panel one click away has the models and usage, and the menu is shared
  with Windows and macOS.

Run (sandbox HOME, pinned GOMODCACHE, shared GOCACHE), on origin/main
21aa402:
- TestTrayIconPerOS fails with Linux given appIcon back ("linux:
  (0,0)-(64,64), want the menu bar bird's (0,0)-(44,44)"); TestTrayIdleTip
  fails with the tooltip back to plain "magpie". Both pass restored, with
  TestPortalLight, TestTrayTotals, TestTrayIconSize, TestSNITipIsSafeMarkup.
- gofmt -l clean; go vet ./... (darwin), GOOS=windows go vet ./...,
  GOOS=linux go vet -tags nogui ./... clean; GOOS=windows go build and
  GOOS=linux go build -tags nogui OK.
- traytip_linux.go (linux && !nogui, cgo) can't be vetted from macOS: the
  two new functions were copied into a darwin build and vetted there.
- go test -p 4 -timeout 40m ./...: exit 0, 53 ok, no FAIL.
- GUI: tray-*.test.cjs (Playwright 1.62.0): 112/112 pass.

Not tried on a real Linux bar (the Windows box with its WSL2/Omarchy VM
was unreachable over ssh): the pixmap, its colour on a light/dark scheme
and the tooltip on waybar/Quickshell/KDE are unverified on hardware.
…ay's observer point (P3)

P3 of docs/subsystems/plugin-runtime.md opens the gateway to plugins. The
observer point is first: it only reads, so it carries the least risk.
With plugins/otel off, the gateway exports nothing and does no observing
work. It keeps no reply whole and adds no request context.

- usage.Observer:
  - Methods: Serve, Bodies, Begin(Request) Watch and Ledger(Record). Watch
    has Attempt and End.
  - Observe(o) registers an observer until its remove is called.
  - ServeObservers is counted, and only Server.ListenAndServe calls it.
  - ObservedBodies takes the most any observer wants.
  - Record.SkipOTel becomes Watched, set by appendUsage for a request an
    observer watched.
- The gateway's otel.go becomes observe.go:
  - observeRequest asks each observer through Begin.
  - attempt and end call each Watch. Bodies are added only when an
    observer asks for them, and the whole reply only for WholeBodies.
- internal/otlp: the exporter (otel*.go) moves out of internal/usage, and
  the gateway's per-request span state moves into request.go. otlp.New()
  is an Exporter, a usage.Observer. What it sends is unchanged.
- plugins/otel:
  - Apply registers otlp.New() and disposes of it when turned off.
  - InUse is 1 while settings.OTelExport says to send. A user who exports
    only through MAGPIE_OTEL_* keeps the plugin on under Essentials.
- pluginrt.KeepFresh: the process serving the gateway refreshes every 2s
  when plugins.yml or settings.json changes. A switch made in the window or
  the CLI reaches a running gateway, or a headless serve, without a restart.
- docs: plugin-runtime.md gets a P3 section, a gateway runtime path and an
  updated failure section; reference.md says the export is the otel plugin.

Run at this tree on origin/main 82f48f0 (rebased onto 52ee847 after,
which touches only the Linux tray icon):
- gofmt -l is clean. go vet passes on darwin, GOOS=linux -tags nogui and
  GOOS=windows.
- go test -count=1 -timeout 25m ./...: ok, 55 packages, none failed.
- New tests:
  - usage: TestObserverServesWithTheGateway and TestObserverLedgerAndBodies.
  - gateway: TestObserverPoint, covering unwatched, no bodies, captured,
    whole and whole streamed.
  - plugins/otel: TestOnExportsOffSendsNothing (on, off, on, with a real
    collector) and TestInUseByEnvironment.
  - pluginrt: TestKeepFreshFollowsPluginsYml.
  - The OTel tests moved with their code to internal/otlp.

Broken by hand, each red, then restored:
- Apply without Observe, and without its dispose.
- KeepFresh refreshing only once.
- InUse reading settings.json only.
- Ledger keeping Watched rows: 3 gateway OTel tests red.
- ObservedBodies taking the last observer's answer.
- Observe not serving a late registration.
- appendUsage not setting Watched.
- Whole bodies never kept.
- Bodies always on with no observer asking.
…loaded Mac's suite, not "token """ after two 5s waits (yetone#1627)

The cause: the test wrote its fake secret-tool with os.WriteFile, so its
first exec paid macOS's check of a newly written program (0.27s here at
load ~10, several seconds while a go test ./... starts its binaries, as
testenv.Program's own doc records). That ran into copilotSecretWait (5s),
which the happy path was held to: both lookups (account, then username)
were killed and the token read as "".

Not a product bug from this cause: copilotSecretTool runs only on Linux,
against the user's installed secret-tool, which has no such check. The 5s
bound is for a keyring prompt nobody answers (yetone#723) and stays.

The fix, in the test only:
- both fakes are testenv.Programs (a file that has already run once);
- the lookups that answer get copilotSecretWait = 10 minutes, since which
  attribute is asked is no question of time; only the keyring that never
  answers keeps 200ms;
- that case's bound moves from 10s to 30s: bounded it takes 2.4s at most
  unloaded, unbounded 60s, so a loaded runner has room on both sides.

Run at the reviewed head (macOS, load ~9-15):
- Injected delay (sleep 6 at the top of the fake): the old test fails with
  copilot_cli_test.go:94 token "", want the one under username (12.01s);
  the new one passes (27s). Removing only the 10-minute wait by hand, with
  the delay, goes red again.
- Broken by hand in copilot_cli.go: asking "account" only fails the test
  (token ""), and dropping the timeout fails it on the hang case (63s).
- go test -count=20 -cpu 1,4 -run 'TestCopilotSecretToolUsername$|TestCopilotCLI'
  ./internal/provider: ok.
- gofmt -l: clean. go vet -tags nogui for darwin, windows, linux: clean.
- go test -p 4 -timeout 40m ./... under a sandbox HOME: 53 packages ok,
  no FAIL.
… moves onto that model, not "Same as main model" (yetone#2257, luokinging)

A Fable tier set to GPT-6.1 Sol read "Same as main model" once the main
model was switched to Sol, and the next main-model move took it with it.
On a move of the main model, a tier equal to the new main model was kept
as its own only when it equalled what it would follow there. Fable follows
nothing on a model that is no Fable, and haiku follows a Claude model on
its Haiku, so neither was marked: Get then hid the tier as following, and
the next move rewrote it. The new main model itself now counts as the
tier's own model too, for every tier and both routes (magpie and Claude
Code's /model). GUI, TUI and CLI all read the same Field.

A user already caught by it sees "Same as main model" and sets the tier
again once; that state can't be told from a tier that follows.

TestClaudeFableTierOnClaude modelled 0.1.1229's wiring with fable=Sol
written before the main model moved onto Sol, which is this issue's own
case; its fixture now writes it after, as 0.1.1229 did.

Run:
- TestClaudeOwnTierOnTheNewMainModelStays (every tier, two own models,
  magpie and /model): on the old code fable fails for both models and
  routes, and haiku given Opus fails ("haiku reads \"\""); passes now.
- CLI in a sandbox HOME/CLAUDE_CONFIG_DIR, old and new binary: main ->
  k2.8, fable -> Sol, main -> Sol, main -> GLM. Old: fable gone after the
  move onto Sol, no ANTHROPIC_DEFAULT_FABLE_MODEL after GLM. New: fable
  Sol after both, env ANTHROPIC_DEFAULT_FABLE_MODEL=oa/gpt-6.1-sol.
- go test -tags nogui ./internal/agent/ ./internal/tui/ ./internal/cli/:
  ok. gofmt -l; go vet darwin, GOOS=linux -tags nogui, GOOS=windows: clean.
Not tried: the GUI by hand (no asset changed; it reads the same Field).
…efore adding (yetone#2251, ggbdpq)

An import link could set chat=, responses= and anthropic= but not
gemini=, though the editor and the TUI take a Gemini base URL; a link
with only gemini= failed with "it needs chat=, responses= or anthropic=".
ParseImport now reads gemini= under the same base-URL rules, and the
error names it (ggbdpq's patch, with docs/reference.md and the en/zh
docs rows).

Its siblings that listed the three base URLs:
- the GUI's import dialog warned "Your prompts and this key will go to ;"
  with no host for a gemini-only link: it names the Gemini host now;
- the site's handoff page (site/public/import.html) left it out of
  "Sends to";
- the docs' link builder (en, zh, ja) had no Gemini field and didn't
  count it as a base URL; the ja docs lacked the table row, and all
  three said one of chat/responses/anthropic is required.

Run:
- TestParseImportGemini fails on the old importurl.go ("the link gives
  no base URL: it needs chat=, responses= or anthropic="), passes now;
  go test -run Import ./internal/provider/ ./internal/gui/: ok.
- import-link-gemini.test.cjs: 20/20 (chromium+webkit, en/zh/zh-TW/ja/de,
  1100 and 420 px); against the old app.js the warning reads "will go to
  ;" and it fails.
- docs builder (en/zh/ja) and handoff page in Chromium from file://: a
  gemini-only builder gives magpie://import?name=…&gemini=…, no errors;
  the handoff page lists gem.example under Sends to.
- gofmt -l; go vet darwin, GOOS=linux -tags nogui, GOOS=windows: clean.
Not tried: a real magpie://import click through the OS handler.

Co-authored-by: ggbdpq <ggbdpq@gmail.com>
…no Library write target, and nothing it already has is removed (yetone#2254, ggbdpq)

After an agent is uninstalled, the Agents page shows "CLI not found"
(d0fcb8b), but Library still offered it for skills, MCP servers, shared
instructions and rtk, and a sync wrote into a folder nothing reads.

- library.Targets() leaves out an agent whose CLI is missing
  (agent.CLIMissing: false for WSL, plugin and detect agents), so sync,
  the read view, market, instructions, skills and found servers all
  drop it at their one upstream.
- Takes() refuses a gone agent by name, so `library mcp add ...
  agents=hermes` says "Hermes Agent's CLI isn't installed here; nothing
  reads its MCP servers until it is installed again" instead of "every
  agent already has it".
- The guards that must still see a gone agent's files (projects'
  notGlobal for skills and MCP, instructions' own-file check) read
  everyTarget(), so a set kept in the agent's own file is still refused.
- rtk: a gone agent isn't offered rtk; one that still has the hook is
  listed so the hook can be taken out.
- Nothing a user wrote is removed: sync acts per target, so a gone
  agent's skills, servers and AGENTS.md stay as they are, and the client
  keeps agent ids it doesn't show.

Run:
- Sandbox HOME, real binaries: old wrote mcp_servers into a gone
  ~/.hermes/config.yaml; new refuses with the message above, exit 1.
- New tests TestTargetsLeaveGoneAgentsOut, TestGoneAgentKeepsWhatItHas,
  TestRTKLeavesGoneAgentsOut; each broken by hand (Targets check,
  Takes refusal, everyTarget in instructions, rtk gate) goes red.
- go test -tags nogui ./internal/library ./internal/tui ./internal/cli
  ./internal/gui: ok. Fixtures that relied on a gone CLI now place one
  (holdCLI, TestRTKHermesHome, TestRTKOffPath), and so do profile's
  and davsync's sandboxes, whose library syncs to claude and codex
  (TestOldProfiles, TestProfileCarriesLibrary, TestSyncLibrary went red
  in the full run without them).
- gofmt -l clean; go vet -tags nogui darwin/linux/windows clean.
- go test -tags nogui -p 4 ./... at the pushed head: see the last
  commit of this push.

Not tried: the real GUI with an uninstalled agent on a live machine.

Co-authored-by: ggbdpq <ggbdpq@gmail.com>
…own line in the agent's file names (yetone#2253, cnlove7777777-art)

Usage -> Sessions' conversation said only "Assistant", so which model
wrote a reply couldn't be told, though the agents' files name it.

- sessions.Part gains model (omitempty), set on assistant parts only,
  from the part's own line: Claude Code's message.model (the model that
  answered; none for its "<synthetic>" lines), Codex's
  turn_context.model (the model the turn was sent to; a /model between
  turns moves the next turn), Pi's and omp's message.model, OpenCode's
  modelID and OpenCode 2's model.id.
- Kimi Code (its model is only on a usage.record written after the
  reply), Cursor (no model in the transcript) and gateway transcripts
  (GatewayTurn.Model is the model asked for, not the one that answered)
  name none. Nothing is guessed from nearby requests.
- The reader (sessions client.js) says it muted after the role, once a
  reply at its first part and again only where it changes inside the
  reply; the tooltip is "Model: <id>" (an existing string, translated).
  ledSaid takes the model as an argument, so the Usage page's
  per-request content, whose row already names its model, is unchanged.
  No layout change: one muted span in the existing header line,
  ellipsized at narrow widths.

Run:
- TestTranscriptModelClaude/Codex/Pi/OpenCodeV2/OpenCodeFiles: all five
  fail on the old content.go/transcript*.go, pass on the new.
- sessions-talk-model.test.cjs: 20/20 (chromium+webkit, en/zh/zh-TW/
  ja/de, 1100 and 420). Fails on the old app.js/client.js, and with the
  once-a-reply rule removed by hand (label every assistant part).
- sessions-talk, sessions-export, gateway-conversations,
  usage-ledger-content, usage-ledger-detail GUI tests: all pass.
- go test -tags nogui ./internal/sessions ./internal/gui
  ./internal/gateway: ok. gofmt -l clean; go vet -tags nogui darwin/
  linux/windows clean.

Full suite for this push (yetone#2257, yetone#2251, yetone#2254, yetone#2253) at 53677ed8d:
go test -tags nogui -p 4 -timeout 40m ./... with pipefail: every
package ok. (An earlier full run caught davsync's TestSyncLibrary and
profile's TestOldProfiles/TestProfileCarriesLibrary relying on a gone
CLI after yetone#2254; their sandboxes now have the CLIs, in yetone#2254's commit.)

Not tried: the real app against a live Codex rollout (fixtures follow
the lines the readers already parse for usage).
yetone and others added 29 commits October 16, 2026 03:41
…n is ended by usage, or cut

Linka (Discord #feedback): Gemini's OpenAI-compatible API sends a whole
function call (its thought signature, the usage) in one chunk and the
finish_reason in the next. When the upstream hung up between the two,
magpie relayed a stream that ended with neither, and Pi threw the
complete call away ("Stream ended without finish_reason") and asked the
whole 160k turn again. On a Chat upstream only a failed read was a cut,
so a clean EOF there went on as a stream with no end.

That API counts the usage only on the generation's last chunk(s): the
Vertex fixture (testdata/vertex_openai_thoughts.sse) has it only beside
finish_reason, Linka's frames have it on the call chunk and the finish
chunk, and Google's forum and third-party docs say the same; no Google
doc states it. So for a Chat upstream that is Gemini's (geminiCompat,
saysItsEnd) and ended without a finish_reason:
- its last chunk carried usage: the generation was done and only the
  finish_reason was lost. It is filled in (chatTail), "tool_calls" after
  a call and "stop" otherwise: relayed, as one chunk before [DONE]
  (written only if [DONE] hadn't gone); translated, as the reply's normal
  end (tool_use for Claude Code), streamed and unstreamed. Recorded ok.
- no usage at its end: the reply was cut, and the client gets "the reply
  ended before it was complete" (errEndedShort), as a Gemini stream's
  finishReason is told (yetone#1643, yetone#1644): the relay's error chunk, the
  translated stream's error, an unstreamed 502.
A failed read keeps its own "connection lost mid-reply". Other vendors'
Chat streams are unchanged.

Tests (gemini_compat_cut_test.go, Linka's frames): relayed, the call
with usage ends with finish_reason tool_calls then [DONE], text with
usage with stop, the call without usage with the error and no
finish_reason; a whole reply with or without [DONE] goes as it came
with one finish_reason; kimi-k3 with and without usage unchanged.
Translated: with usage one message_stop with stop_reason tool_use and
no error, streamed, and a 200 tool_use unstreamed; without usage one
error and no message_stop, and a 502 unstreamed.
Broken by hand, each red: the relay fill off (relay test: no
finish_reason), the streamed fill off (0 stops, 1 error), the
unstreamed fill off (502), usage ignored in chatTail.finish (every
no-usage case ended as whole, unstreamed 200).

Run: gofmt -l clean; GOOS=darwin/linux/windows go vet -tags nogui ./...
ok; go test -run 'TestGeminiCompat|EndedShort|TestStreamCut|
TestWellFormed|Sniff' ./internal/gateway ok.
Docs: gateway-routing.md says when the finish_reason is filled in.
Full suite: go test -tags nogui -timeout 45m ./... at base a61a6f9 + this, EXIT 1: 65 packages ok, internal/gateway red only in TestVisionWaitIsntTheDescribersTime (300ms describer limit under load, unrelated; passes -count=5 alone; filed yetone#2444). Rebased onto 1a16fc0, internal/gateway whole: ok.
…rom no answer, and is kept while the item is unchanged (yetone#2367)

Dony2050 (yetone#2367) runs Codex with cli_auth_credentials_store = "auto" and no
auth.json, so magpie reads "Codex Auth" through /usr/bin/security. Four
faults:
- One 10s timeout covered both of macOS's prompts and the user typing their
  password. A read they approved after 11-16s still failed.
- A timeout and a refusal gave the same error.
- A read was kept for 10s only. With "Allow" (once), background polls
  prompted again about every 30s.
- Only a restart cleared the 10-minute failure cache.

What changed:
- Reading the secret is one ask per item at a time (askCodexKeychain). It
  runs in its own goroutine for up to 2 minutes (keychainAsk), outside
  codexKeychainMu, so no `security` run happens under a lock a reader needs.
  A reader waits at most 5s from when the ask started (keychainHold), then
  gets errKeychainWaiting. A request or a page never waits out the prompt.
  The ask's answer is kept when it lands, unless magpie kept a newer one
  after it started.
- Each failure says what happened:
  - exit 44: no item.
  - exit 51 (errSecAuthFailed) or 128 (errSecUserCanceled): denied or
    cancelled (errKeychainRefused). Checked against SecBase.h. Exit 44 was
    also checked on a throwaway keychain in the scratchpad.
  - the 2 minutes ran out: no answer (errKeychainNoAnswer).
  - anything else: errKeychainUnread.
  None of them counts as signed out.
- A successful read is kept while the item is unchanged. After 10s a reader
  runs Stat (find-generic-password without -w), which shows no prompt and
  returns only the item's mdat. The secret is read again only when the mdat
  changed.
  - Every write of the credential re-derives the cache:
    - codexStore.write keeps the date of what it wrote (keepCodexKeychain).
    - Switch, sign-out (deleteCodexLogin) and refresh forget a read
      (forgetCodexKeychain), so the next read checks the date.
  - forget keeps a refusal for its 10 minutes (yetone#2408).
  - resetCodexKeychain clears everything. Only tests use it.
- The saved accounts' line in the GUI has a "Try again" link beside Remove
  when a read was refused, got no answer or is waiting (Exclusion.Retry).
  It posts /api/provider/keychain-retry, and RetryCodexKeychain reads once
  past the refusal.
  - The reason now says "Always Allow" lets /usr/bin/security read the item,
    so macOS stops asking each time Codex changes it.
  - Two new strings in zh, zh-TW, ja and de.
- docs/subsystems/providers-accounts.md: the "Codex's credential store" row
  describes the above.

Tests:
- TestCodexKeychainAskedSeldom (rewritten) covers:
  - an unchanged item over aged and forgotten rounds asks once;
  - a changed item asks again;
  - a gone item is not found without asking;
  - a refusal is kept through forget.
- TestCodexKeychainPromptWaitedFor: a held prompt gives "waiting" to the
  first and to 8 concurrent readers without blocking. Once answered, the
  read succeeds with one ask.
- TestCodexKeychainRetry: Retry and Why for refused, no answer and no item,
  and RetryCodexKeychain.
- TestSecurityKeychainExits (codex_keychain_ask_unix_test.go): a fake
  `security` built with testenv.Program covers:
  - a 2s answer with keychainWait = 1s succeeds;
  - Stat parses the mdat;
  - exits 44, 51, 128 and 1 map as above;
  - a hang gives no answer.
- signedout_test: Retry is set only for an unread item.
- GUI keychain-retry.test.cjs runs in chromium and webkit, in en and zh:
  - order Try again, Remove;
  - the POST body;
  - the line leaves after a click;
  - no link without retry.

Broken by hand (each restored after):
- Find bounded by keychainWait again: TestSecurityKeychainExits red ("a read
  the user answered late ... no answer in time").
- exit 51 not classed as refused: TestSecurityKeychainExits red ("Find,
  denied").
- Readers wait on the ask (no hold): TestCodexKeychainPromptWaitedFor hangs;
  the package timed out.
- forget clears every read: TestCodexKeychainAskedSeldom red (4 asks).
- mdat match ignored: AskedSeldom red (7 asks).
- Cache never re-checked: AskedSeldom red (the changed item wasn't seen).
- RetryCodexKeychain also deleting the failure cache: still green, because
  a fresh read asks past a refusal. That loop was removed as redundant.

Not tried:
- A real macOS prompt, or an item Codex itself wrote. Every test uses a fake
  `security`; the real keychain and ~/.codex were not touched.
- Put and Remove are still bounded at 10s.

Run at this head, rebased on origin/main 1a16fc0:
- gofmt -l: clean.
- go vet -tags nogui ./...: ok for GOOS darwin, linux and windows.
- go test -race -count=2 on the keychain, signed-out and Codex sign-in tests
  in internal/provider: ok.
- GUI (node --test, NODE_PATH playwright 1.62.0): keychain-retry, i18n-keys,
  saved-signedout-remove, gui-de, gui-ja, gui-zh-tw: 24/24 pass.

Then rebased on origin/main 167df70 (TestSecurityKeychainAnswers' fake is
a testenv.Program; no overlap in hunks): go vet -tags nogui for darwin, linux
and windows on provider and gui, and go test -race ./internal/provider: ok.

Full suite: go test -timeout 45m ./... in a sandbox HOME, set -o pipefail, at
this tree on origin/main 1a16fc0: exit 0, 66 packages ok, no FAIL (gateway
1588s, agent 1497s, provider 510s).

Rebased onto origin/main a6f908b (yetone#2440, yetone#2437 Gemini finish_reason; no overlap): go test -tags nogui ./internal/provider ./internal/gui ok.
…eld at the top

8f55c27 (yetone#2396) holds Settings' tabs at the view's top. "100% picked at
the foot of the page" scrolled until the 100% button was within 60px of
the view's top, which is now under that head, and the click timed out on
"<div class="row-head set-head stuck"> intercepts pointer events" in both
engines (20/2 on origin/main 1a16fc0). It stops 60px under the head's
foot now, as a reader could still click it.

Run: text-size 22/0 (chromium, webkit) at 1a16fc0 + this change.
…ndows rule left two Mac tests red on Windows CI

Added under 10-15's criteria still missed: 64b0f3a (menu-scroll, fixed
a61a6f9), 8f55c27 (text-size, fixed d3ca90a) and 94942f9
(agent-connect-no-models, cf21b71, yetone#2435) each ran the GUI files they
chose; 6e2aa8e's Windows rule and the dsh tests fixed in ce1251c.

Docs only; no code or test changed, nothing run.
The owner: "Provider表单太复杂了…有一些可以折叠在高级里面". Headers,
proxy, concurrency and RPM limits, prices, context and output windows,
native compaction and fallback now sit in an "Advanced" fold beside the
key and models. It opens on its own when any of them is set, keeps the
state it was left in across the editor's redraws (keyed by the provider
being edited), and a Save refused on a field inside it opens it and
focuses that field. A signed-in account's editor folds the same.
Strings in zh, zh-TW, ja and de.

Motion ("整个弹窗高度的改变也应该是丝滑的动画"): the fold grows and
shrinks on one spring-like curve, its rows fading in, its arrow turning,
and the dialog's height follows on the same curve both ways; clicked
again mid-way it turns back from where it is. Reduce motion: at once.

Every dialog holds its place: the element last pressed or focused stays
where it was while the dialog grows or shrinks (a relative top plus a
max-height cap), so the button under the pointer doesn't run away. Where
there is no room above, only a pressed button below the fields shortens
them through the cap; nothing a click does scrolls the fields
(models-fold, names-fold). An element mid-scale (a Copy button's pop) is
measured by its parent (key-copy). The names list holds its height while
drawNames rebuilds it, so WebKit doesn't clamp the editor's scroll mid
rebuild (provider-detect, "Ask on one API moved" 64→190).

Tests: provider-advanced-fold (new, 5 languages × 2 engines, 420px);
about 21 editor tests open Advanced before reaching a folded field;
balance-method and plugin-move find the fold by its class; detect-draft
allows 0.5px. appScriptBudget 1,250,000 and coreScriptBudget 1,426,000
(1,232,466 and 1,405,937 with this).

Run: go test ./internal/gui/ ok. Every GUI file (771) in a full run,
4 at a time; its reds, rerun alone at this head, both engines:
key-copy 8/0, models-fold 4/0, names-fold 8/0, balance-method 42/0,
provider-detect 4/0, plus provider-remove 16/0, plugin-move 8/0,
provider-detect-draft 20/0, provider-advanced-fold 50/0,
provider-add-models 4/0. Red on origin/main as well, filed: yetone#2435
(agent-connect-no-models, fixed in cf21b71), yetone#2439 (fonts), yetone#2443
(key-balance-card), yetone#2228 (quota-count-wrap, quota-name-translated),
yetone#1905 (routing-wb-restricted), yetone#2448 (s3-sync); mobile-web and
library-workbuddy-trust red only under load, noted on yetone#1544;
readable-focus and text-size fixed on main in 1a16fc0 and d3ca90a.
…etone#2447)

EdwinDigital: in the tray panel, WorkBuddy's row said its models in words
("gpt-6.1-sol · Microsoft Foundry, claude-opus-5-5 · Microsoft Foundry +1"),
and the words took the row: the name was squeezed to 10px at 550px and to
nothing at 440px and below, or at any width once the pointer came onto the
row and the model's logo eased in. The row's columns were 20px, a name in
minmax(0, 1fr) and the words in auto, so the words claimed their whole
width and the name gave way.

Now the name's column is fit-content(60%): it keeps its own width, up to
60% of the row, and the words take what is left and cut first (.vt already
ellipsizes). The words sit at the row's end (justify-self: end), so their
effort bars and chevrons stay in one column down the list. It holds for
every panel row with a summary (a model list, one model's label, import,
launch, CLI not found), not WorkBuddy alone.

Name width, WorkBuddy / Claude Desktop, before -> after, chromium and webkit
the same: 550px 10.05 / 8.09 -> 76.05 / 103.92; 440px and 360px 0 / 0 ->
76.05 / 103.92; hovered the same after.

Run at 0cad76c:
- agent-panel-name-width.test.cjs (new): chromium and webkit; en, zh,
  zh-TW, ja and de; 550, 440 and 360px; hovered and not. Each name is
  whole, the words are after it and inside the row, right edges line up,
  the model lists are cut, and nothing scrolls sideways. 32/32 pass. With
  HEAD's app.css it fails 32/32 (name 10.05px at 550, 0 below).
- GUI, every file that renders the panel or an agent row (101 files,
  4-wide): # tests 1390, pass 1387, fail 3. The 3 reds also fail at HEAD
  without this change, run alone: key-balance-card chromium de
  (view-usage 570>560, yetone#2443) and usage-fold chromium en/zh (the view
  didn't scroll, 184 !== 180, yetone#2163).
- The whole GUI suite (780 files) was not run.
- gofmt -l clean; GOOS=darwin/linux/windows go vet -tags nogui ./... ok;
  go test -tags nogui -timeout 45m ./internal/gui/... ok (385.6s).
… line (yetone#2446)

A passthrough stream cut mid-line got its error event written onto the
half-written line, so the client read "event: error" as more of that
line's data and never saw the error as an event. ggbdpq's patch (yetone#2446)
tracks whether the last write ended a line (passthrough's write, and
holdWriter.atLine for cutLoop) and puts a blank line, two newlines when
mid-line, before the appended event.

The same join could happen to the other event passthrough appends: the
finish_reason filled in for a Gemini OpenAI-compatible reply that ended
done (a6f908b). It now goes through the same separator (apart). The
truncated pending event is still dispatched; SSE can't take it back, but
the error now arrives as an event of its own.

holdWriter's other appends (release, failTo) only follow keepalive
comment lines, so they already start on a line.

Tests: TestPassthroughStreamCutMidLine, TestPassthroughStreamEndsOnLineEnd,
TestLoopCutMidLine (ggbdpq), TestGeminiCompatFinishAfterPartialLine.
Broken by hand: apart returning "\n" always -> CutMidLine and
GeminiCompatFinishAfterPartialLine red; the Gemini fill-in without the
separator -> GeminiCompatFinishAfterPartialLine red; cutLoop's "\n\n" as
"\n" -> TestLoopCutMidLine red. Restored, all green.

gofmt -l clean; GOOS=darwin/linux/windows go vet -tags nogui ./... clean.
Full suite: go test -tags nogui -timeout 45m ./... at base 0cad76c, 66 packages ok, 0 FAIL (gateway 1869s, provider 445s; sandbox HOME)

Co-authored-by: ggbdpq <67929842+ggbdpq@users.noreply.github.com>
…etone#2445)

B.ai answered a 380K request, under the model's 1M window, with 400
"Input token exceed the limit (request id: …)". No pattern in tooLongRe
matched it, and quotaWords wants "exceeded", so retryable() was false and
group/top's other three members were never asked (worksyang).

tooLongRe now also matches "input token(s) (count) exceed(s|ed) (the)
(max|maximum) limit". The refusal is taken as an overflow, so the chat
loop's existing yetone#700 path asks the next member that may hold the request
(withRoom, failOverflow). Nobody rests, another account of the same model
isn't asked, and with no member left the agent is told it overflowed, to
compact. paceWords still keeps rate limits worded with input tokens out,
and a plain bad request still isn't asked of anyone else. System One's
decision groups (decide_group.go, which the report cites) are unchanged:
their requests are short decision text.

Test: TestRelayInputCapGoesToTheNextMember uses the reporter's message
verbatim. The JSON around it is not the reporter's; the issue shows only
the message. Docs: gateway-routing.md failure paragraph + verification.

Run:
- Broken by hand, the new test went red each time:
  - pattern removed: group answered 400, member 2 never asked;
  - paceWords check removed: two rate phrasings taken as overflow;
  - retryable true for every 400: the plain bad request reached member 2.
- TestRelayInputCap, TestEveryVendorsOverflow, TestOverflow*,
  TestContextOverflow*, TestOutOfBudget*: ok.
- gofmt -l: clean. go vet -tags nogui ./... on darwin, linux, windows: ok.

Full suite: go test -tags nogui -timeout 45m ./... at 0cad76c + this
change, sandbox HOME: 66 packages ok, 0 FAIL (internal/gateway 1768s).
@G_Z_Y looked for Cursor CLI on the Agents page and took the "Cursor" row
for the editor. The row is cursor-agent, so it is named Cursor CLI. What
it keeps in ~/.cursor is the Cursor editor's too (MCP servers, skills,
chats), so the Library and Sessions name it Cursor there (SharedName).

Run at this head before the rebase onto 0f391a7: go test -tags nogui -count=1 -timeout 45m ./... under a sandbox HOME, every package ok; gofmt -l clean; go vet clean for darwin, linux (nogui) and windows; GUI files agent*, i18n*, gui-*, getting-started, library*, sess* and import-row in both engines: 1638/1638.
The editor makes ~/.cursor on its own, so the folder alone says nothing
of cursor-agent (dirShared): cli-config.json or the command finds Cursor
CLI. The Library still writes the MCP servers and skills the editor reads
there when only the editor is here (SharedHere).

Run at this head before the rebase onto 0f391a7: go test -tags nogui -count=1 -timeout 45m ./... under a sandbox HOME, every package ok; gofmt -l clean; go vet clean for darwin, linux (nogui) and windows; GUI files agent*, i18n*, gui-*, getting-started, library*, sess* and import-row in both engines: 1638/1638.
Cursor CLI updates itself and isn't on npm or Homebrew, so magpie shows
its version only (@G_Z_Y on X saw none).

Run at this head before the rebase onto 0f391a7: go test -tags nogui -count=1 -timeout 45m ./... under a sandbox HOME, every package ok; gofmt -l clean; go vet clean for darwin, linux (nogui) and windows; GUI files agent*, i18n*, gui-*, getting-started, library*, sess* and import-row in both engines: 1638/1638.
"Only Cursor's own models" beside "Can't connect" read as two things.
Each of the four native-only rows now says it uses only its vendor's
models, so magpie can't connect it, in every language, and the line
wraps instead of being cut short.

Run at this head before the rebase onto 0f391a7: go test -tags nogui -count=1 -timeout 45m ./... under a sandbox HOME, every package ok; gofmt -l clean; go vet clean for darwin, linux (nogui) and windows; GUI files agent*, i18n*, gui-*, getting-started, library*, sess* and import-row in both engines: 1638/1638.
…ounted out, as the plugin does (plugins#138)

hheei (magpie-community/plugins#138): an account at 560 / 560 credits
was ranked spent for deepseek-v4.1-flash too, which WorkBuddy serves at
x0.00 and which draws no credits. The plugin was fixed in 0.1.23
(notModels) and the mover min is 0.1.24 already; the built-in, which
users who haven't moved still run, had the same fault. wbQuota's
personal Credits window and the enterprise window now pass through
wbSpareFree, which scopes them (QuotaWindow.matches) to the models not
marked Free in the provider's last fetched list (catalog.Live). With no
list read, every model counts, as before. The 14018 "Credits exhausted"
rest already rests only the paid model (PaidBesideFree).

Run:
- TestWorkBuddyCreditsSpareFreeModels (new): with no live list the free
  model reads 100%; with one, deepseek-v4.1-flash (either case) reads 0
  while -sg (x0.03) and gpt-5.5 read 100. Broken by hand (wbSpareFree
  returning early): red, "deepseek-v4.1-flash: 100% used, want 0".
- gofmt -l clean; go vet -tags nogui ./... for darwin, linux, windows: 0.
- Full suite: go test -timeout 45m ./... in a sandbox HOME, twice: every
  package ok except internal/gui's TestPanelJoinsFullScreenSpaces ("the
  panel shown doesn't take the keyboard"), which is red the same way
  alone on a fresh origin/main be1030e worktree; filed as yetone#2450.
…#2449)

TestPluginLoginInstallsItsCLIFirst went red on macOS CI (run
38798393584) with "not signed in after the install: []". cli's run()
sets catalog.Changed = agent.SyncCatalog, and a sync left going from an
earlier step holds the catalog (provider.Hold), plugins and their
sign-ins among it, for up to a second. The host's "auth" event dropped
that only through catalog.Touched, told in the background, so on a busy
runner provider.Logins right after the sign-in read the snapshot taken
before it.

plugin.OnChangeNow registers hooks run before changed() or listed()
returns, i.e. before the host's reply after the event is read; provider
registers dropHeld there. Changed() is filememo.Forget + dropHeld.

Reproduced by injecting a pause where SyncCatalog holds (patch kept
outside the tree): without the fix red 3/3 with the CI message; with it
ok -count=5. TestHeldGoesWithAPluginChange: red without the fix ("held
from before the change: built 1 times, want 2"), green with it, also
-race -count=20.

Run: -count=20 -run TestPluginLoginInstallsItsCLIFirst ./internal/cli
ok; internal/cli, internal/plugin, internal/provider whole ok; gofmt -l
clean; go vet -tags nogui for darwin, linux, windows on the three ok.
Full suite: go test -tags nogui -timeout 45m ./... at 7fefbc6 with both yetone#2449 commits (this one and the Settle fix after it) (sandbox HOME, macOS): exit 0, 66 ok, 18 no test files, 0 FAIL.
Settle's own Restart tells the OnChange hooks, and one that calls
Cached() started refreshProvidersInBackground after Settle's last
refreshing.Wait. callWithTimeout starts the host under WithoutCancel, so
it went on once Settle returned and spawned a host in the test's removed
HOME ("Module not found host-....js", uv_cwd) or in the next test's,
while that one signed in. provSettling keeps Cached and the loop's
continue branch from starting one while Settle runs.

TestSettleStartsNoRefresh: red without the fix ("Settle started 3
refreshes"), green with it, also -race -count=20. In go test -v
./internal/cli the stray host lines went from 7 to 0.

Run: internal/cli, internal/plugin, internal/provider whole ok; gofmt -l
clean; go vet -tags nogui for darwin, linux, windows ok.
Full suite: go test -tags nogui -timeout 45m ./... at 7fefbc6 with both yetone#2449 commits (bd4e9da7b's and this one's changes) (sandbox HOME, macOS): exit 0, 66 ok, 18 no test files, 0 FAIL.
…pacts (@wilsonxuuuu on X)

@wilsonxuuuu's pi, on gpt-6.1 through magpie, stopped on
"magpie API error (413): {...,"type":"api_error"}" at 114k of a 1M
window and never auto-compacted. The vendor's 413 was a body byte cap.
pi's overflow table (pi-ai utils/overflow.ts) knows a byte cap only as
request_too_large, Anthropic's type, which magpie already gave on the
Anthropic route but not on chat/responses. writeErrorSaid now types
every 413 request_too_large (Gemini keeps its own status shape). The
status stays 413 and the byte-limit hint stays; it is not turned into
context_length_exceeded, so Codex and Claude Code read it as before
(Codex reads code, not type). pi compacts once and retries once
(_overflowRecoveryAttempted), so a body that is still too big after
compacting stops instead of looping.

Run:
- pi 1.1.0's own isContextOverflow on the reporter's body: api_error
  false, request_too_large true.
- TestPayloadTooLargeSaysRequestTooLargeToPi (an nginx 413 page from a
  chat upstream) and the new Chat/Responses rows of
  TestBodyRejectionErrorShapes fail with the gateway.go change reverted
  by hand, pass with it.
- go test -tags nogui -count=1 -timeout 45m ./... under a sandbox HOME:
  every package ok. gofmt clean; GOOS=linux and windows go vet of
  internal/gateway clean.
Not tried: a real pi session hitting a real vendor 413.
…arch (yetone#2439)

The font menu, session combo and row menus focus whichever item the
pointer moves onto. When the search narrowed the list under a pointer that
hadn't moved, the redrawn item under it got a mouseenter too, took the
focus out of the search, and the next letters went nowhere. fonts.test.cjs
went red whenever the list landed while the pointer sat over the menu.

hoverFocus now remembers where the last mousemove was. A mouseenter there
is a redraw's (UI Events fires mouseenter before its mousemove), so it
leaves the focus where it is. A real move still focuses the item.

The new fonts test hovers an item, then types "HarmonyOS" into the search.
The focus has to stay in the field, and Enter has to pick HarmonyOS Sans SC.
- Broken by hand: with the old `b.onmouseenter = () => b.focus()`, the test
  is red in chromium and webkit (the focus is on the item, and the typed
  text is cut short).
- With the fix: green in both engines, 3 runs.

Full suite: node --test --test-concurrency=4 internal/gui/tests/*.test.cjs
(chromium and webkit) on a61a6f9 with all three of these commits'
changes: 10655 tests, 10616 pass, 39 fail, all in 16 top-level tests.
Each of their files was then run alone:
- green alone on origin/main 0085714 and on this tree (load):
  import-link-gemini, market-have, plugin-page, readable-focus, text-size.
- red alone on main too, filed or commented:
  - quota-count-wrap and quota-name-translated: yetone#2228
  - s3-sync: yetone#2448
  - usage-agents-hide: yetone#2276
  - usage-checkin: yetone#2142
  - usage-fold: yetone#2163
  - tray-usages: yetone#2453, new
After rebasing onto 0085714, these were run again on this tree, alone:
- green: fonts, key-balance-card, plugin-move, i18n-keys.
- still red, the same as main: quota-name-translated, quota-count-wrap, tray-usages.
The full GUI run was not repeated after the rebase.
go test ./internal/gui on 0085714 plus these changes, sandbox HOME:
FAIL. The only failure is TestPanelJoinsFullScreenSpaces, which is yetone#2450
and is red on main too. TestCoreScriptBudget passes.
…rd (yetone#2443)

At 560px, German's "Zurückgesetzt 19. Okt. 02:37 ·" was wider than the room
left beside the other parts of the reset line. The span is white-space:
nowrap, so it ran 10px past the card. key-balance-card's view-usage test
caught it only in weeks when the date fell on a long German month and
weekday, so it was red on main some days and not others.

Each part of .quota-reset (and .quota-holds) is now white-space: normal
and min-width: 0. A part goes whole onto its own line when it doesn't fit,
and only a part wider than the card breaks between its words.

The test now pins its weekly reset to the next 28 Sept 02:37 that is more
than eight days ahead. That is the widest German date, so the test no
longer depends on today's date.
- Broken by hand: with the old CSS, the pinned test is red in chromium de
  (10px past the card).
- With the fix: green in both engines, every language.

Full suite: node --test --test-concurrency=4 internal/gui/tests/*.test.cjs
(chromium and webkit) on a61a6f9 with all three of these commits'
changes: 10655 tests, 10616 pass, 39 fail, all in 16 top-level tests.
Each of their files was then run alone:
- green alone on origin/main 0085714 and on this tree (load):
  import-link-gemini, market-have, plugin-page, readable-focus, text-size.
- red alone on main too, filed or commented:
  - quota-count-wrap and quota-name-translated: yetone#2228
  - s3-sync: yetone#2448
  - usage-agents-hide: yetone#2276
  - usage-checkin: yetone#2142
  - usage-fold: yetone#2163
  - tray-usages: yetone#2453, new
After rebasing onto 0085714, these were run again on this tree, alone:
- green: fonts, key-balance-card, plugin-move, i18n-keys.
- still red, the same as main: quota-name-translated, quota-count-wrap, tray-usages.
The full GUI run was not repeated after the rebase.
go test ./internal/gui on 0085714 plus these changes, sandbox HOME:
FAIL. The only failure is TestPanelJoinsFullScreenSpaces, which is yetone#2450
and is red on main too. TestCoreScriptBudget passes.
The redraw comes from renderProviders, for example when an account's
allowance read lands (loginUsageOf). It drew a subscription's "Move to
the plugin" field again from the provider alone. Midway through a move,
the button then said "Move to the plugin" again and took presses: a
second move could start while the first ran. A failure that came after
the redraw was said into the old field, which was gone, so the user saw
nothing.

renderMove now keeps the running move's label, and a failure's reason,
per provider in moveSaid. A redraw draws both again. A failure that lands
after a redraw redraws the providers, so the reason shows in the field
on screen.

Found while chasing yetone#2431 (plugin-move's editor scrolls 23px in WebKit).
That shift was not reproduced: 16 loaded WebKit runs were green, and so
were injected late usage, late icons, a busy main thread and a forced
chain. The findings are on the issue. This doesn't close yetone#2431.

plugin-move.test.cjs now redraws mid-move and after a failure.
- Broken by hand: without moveSaid it is red 4 of 4, both engines.
  The errors are "a redraw mid-move forgot the move" and "a redraw
  forgot why the move failed".
- With the fix: 8 of 8 green, 3 runs.

Review: a failure said is kept only while the move's state is the one it
failed from, so a move that lands another way (the hourly hand-over)
doesn't keep showing it. plugin-move*, fonts: 56/56 in both engines after.

Full suite: node --test --test-concurrency=4 internal/gui/tests/*.test.cjs
(chromium and webkit) on a61a6f9 with all three of these commits'
changes: 10655 tests, 10616 pass, 39 fail, all in 16 top-level tests.
Each of their files was then run alone:
- green alone on origin/main 0085714 and on this tree (load):
  import-link-gemini, market-have, plugin-page, readable-focus, text-size.
- red alone on main too, filed or commented:
  - quota-count-wrap and quota-name-translated: yetone#2228
  - s3-sync: yetone#2448
  - usage-agents-hide: yetone#2276
  - usage-checkin: yetone#2142
  - usage-fold: yetone#2163
  - tray-usages: yetone#2453, new
After rebasing onto 0085714, these were run again on this tree, alone:
- green: fonts, key-balance-card, plugin-move, i18n-keys.
- still red, the same as main: quota-name-translated, quota-count-wrap, tray-usages.
The full GUI run was not repeated after the rebase.
go test ./internal/gui on 0085714 plus these changes, sandbox HOME:
FAIL. The only failure is TestPanelJoinsFullScreenSpaces, which is yetone#2450
and is red on main too. TestCoreScriptBudget passes.
… a meter (yetone#2451)

tianxia3111 runs Command Code on the keyed preset with a plan's key. Its
card showed the 5-hour and weekly windows as meters, but the month's
credits only as dollars left ($5.27), with no share of the month. The
cause: billing/credits leaves out the planId, so the key's card couldn't
know the plan's monthly pool. The signed-in subscription's card gets
that pool from billing/subscriptions.

The key's card (planKeyWindows -> cmdKeyWindows) now asks
billing/subscriptions with the same key for the plan. It then makes the
month a "Credits" window beside the other two, through the same
cmdQuotaOf the subscription's card uses ("$4.78 / $10.00" on a Go plan).

- The plan id is kept per key for ten minutes, since
  billing/subscriptions takes 15-20 s at times.
- The id read before still counts while the plan can't be read.
- A key whose plan was never read keeps its two windows alone.
- A pay-as-you-go key, with no windows, still gets no card.

planWindows' request and status handling move into planBody, so that
cmdKeyWindows can decode the reply itself; other vendors are unchanged.

Run:
- New TestCommandCodeKeyCardHasCredits uses the reporter's own
  billing/credits reply. Its billing/subscriptions reply has the shape
  the other Command Code tests use (individual-go-monthly), because the
  reporter's plan id isn't known.
- With the new branch disabled by hand, the test went red: two windows,
  no plan. With it, green.
- TestKeyQuotasKeepLast now answers billing/subscriptions with 503 and
  still sees its two windows.
- gofmt; go vet -tags nogui for darwin, linux and windows.
- Not tried against Command Code's real API: there is no plan key here.
- Full suite (go test -timeout 45m ./..., at f1c91a9 plus this change):
  every package ok except internal/gui, red only on
  TestPanelJoinsFullScreenSpaces, which is yetone#2450 (macOS 26.2, red on
  main before this change).
…ne#2452)

What changed
- viewGroup drew every member and every rule and left the rest to the
  terminal. A group with more of them than the terminal has rows (a
  whole provider's thirty models at 80x24) scrolled the header and the
  highlight off the top, and j/k moved a pick no one could see. The
  library page (yetone#2220) and the agents page (yetone#2428, fed24aa) had the
  same disease; this was the last page with a cursor and no window.
- groupWindow draws the rows around the pick, as ggbdpq's patch does: a
  pick among the models keeps the first rules below it, a pick among the
  rules keeps the models' tail above it, a short group is drawn whole.
- groupRows counts every other line the page draws, including the
  pattern lines, the classifier line and the "none" line, which the
  patch's flat h-10 left out. With h-10 the page still ran three lines
  past the terminal and its header scrolled away.

Run
- New TestGroupViewKeepsTheRowPicked: thirty models; one model with 24
  rules; both, with three patterns and an intent rule's classifier.
  Every pick at 24 and 40 rows must draw no more lines than a short
  group's page, keep "◉ magpie" on screen, and mark the pick's own
  numbered row. A tall terminal draws the whole group.
- Broken by hand: old routing.go goes red (pick 0 draws 41 lines,
  22 fit), the patch's h-10 goes red (25 lines), and leaving the
  patterns uncounted goes red (23). With the fix it's green.
- Other cursor pages were audited (agents, providers, groups, library,
  sessions, pickers). All are windowed; only the group page had none.
- gofmt -l clean; go vet -tags nogui for darwin, linux and windows.
- Full suite: go test -timeout 45m ./... at f1c91a9: 65 packages ok, internal/gui red only in TestPanelJoinsFullScreenSpaces ("the panel shown doesn't take the keyboard"), the macOS 26.2 red filed as yetone#2450 and red on main without this change.

Co-authored-by: ggbdpq <ggbdpq@gmail.com>
Fate asked on Discord to see what a single request on the Routing page sent
and got back, such as a Codex prediction. The bodies were captured already,
in the Gateway page's 40 recent calls and the opt-in request archive, but
nothing led there from a Routing request.

- gateway: a Call carries the Routing request it was (Call.Route = the
  trace's Route.ID), set where serve, the Codex backend and a refused title
  record it. Server.RouteCall finds it in the ring. A route id is not a
  call id. A route names its archive copy (Route.Archive =
  call.archiveName()), set only when the archive was on, so it reaches the
  saved history too.
- gui: GET /api/gateway/call takes ?route= besides ?id=. It is the same
  handler, the same no-store, the same key guard and the same 404 once the
  ring has let the call go.
- Routing page: the story's head has "Request and response". It fetches
  only when pressed, and shows the masked bodies through callBodyPanel (an
  SSE reply reads as its events). A request the ring no longer has shows
  the archive's copy through archivePanel when the route names one.
  Otherwise it says the last 40 are kept until magpie quits and how to turn
  the archive on. Nothing more is captured or kept than before.
- The head now wraps. At narrow widths its buttons ran past the story
  ("Back to live" was cut off at 440px).
- The two button words live in i18n.js: i18n-keys' "a plugin's words are
  its own" found them inside core strings.
- docs/subsystems/gui-shell.md says how a route reaches its bodies.

Tests:
- TestRoutingRequestOpensItsBodies (new): each of two routes gets its own
  bodies; route 0 and an unknown route get 404. Red without
  `call.Route = tr.ID` ("route …: 404").
- TestArchiveWholeBodies now checks that the route names the archive. Red
  without the t.Archive line (`route names archive ""`).
- routing-bodies.test.cjs (new), chromium and webkit at 440px, en, zh,
  zh-TW, ja and de, 10/10. It covers the ring's copy read only when asked,
  hiding, another request closing it, the archive's copy, the
  how-to-keep note, nothing past the story and no left border.
  - Red with the client asking ?id= and with the head's wrap removed.
- routing-request-metrics.test.cjs: with the head wrapped, its 480px
  picker clicks in WebKit landed on the footer. Playwright's WebKit places
  elements in the scrolled view hundreds of px lower than
  getBoundingClientRect, which elementFromPoint agrees with. The test now
  clicks where the page shows them. 5 runs of both engines, 20/20.

Run in a sandbox HOME at f1c91a9 plus this change:
- gofmt -l: clean. go vet -tags nogui: darwin, linux and windows ok.
- GUI: routing-*, panel-routing*, recent-call*, i18n-keys and
  header-relang in both engines: 1079 tests, 1079 pass (a first run had
  2 reds, both fixed above).
- go test ./internal/gui after the i18n move (TestPanelJoinsFullScreenSpaces
  skipped): ok.

Full suite: `go test -timeout 45m ./...` gave 65 ok, 1 FAIL. The FAIL is
internal/gui TestPanelJoinsFullScreenSpaces ("the panel shown doesn't take
the keyboard"), red 3/3 alone here and red the same on origin/main
1d227a7 just fetched. It is filed as yetone#2450.
…t at a time (@NetRozee on X)

@NetRozee on X: "when I add a thousand keys for a custom endpoint magpie
just froze or crash by itself".

Cause. The Providers page asked, for each key, what it rests by
(RestKey), and RestKey listed every key on again (KeysOn), which itself
looked along the list for each key: cubic in the keys. /api/providers
and every /api/keys/* answer, an import too, are drawn through it. The
gateway asked, for each key of every request, whether it serves the
model (Serves), and each asking read the provider's lists again (a
filememo stat, and a read while the file is new) and looked along each
model's key ids. fetchPerKey looked along the lists for each key and
model the same way.

Fix.
- KeysOn drops repeats by a set.
- RestKeys finds once whether several keys are on; providerInfo uses it.
- Serving(model) reads the lists once and answers for each key from sets
  (keyLists, made again when the lists are read anew); perKeyBarred and
  the test pick use it. Serves is Serving for one key.
- fetchPerKey keeps each model's keys as a set and indexes the old list
  by key once; a model kept for a failed key is kept once, as before.
- KeysListed and ListsRead count listings and list reads for the tests.
Behaviour is otherwise the same.

Measured (go test in a sandbox HOME, a custom provider, 50 models):
                         1000 keys          5000 keys
  providerInfo           1.54s -> 7.5ms     2m42.9s -> 18.3ms
  providersState         1.65s -> 110ms     2m40.3s -> 146ms
  POST /api/keys/import  1.64s -> 82ms      2m44.7s -> 288ms
  of one more key
  gateway perKey         100.9ms -> 8.2ms   (old not run) -> 40ms
  (each request)

Tests (counts, not time), each red on the old code by hand:
- TestProviderInfoListsKeysOnce: old 1001 listings for 1000 keys, 11 for
  10; new 0 and 0.
- TestPerKeyReadsListsOnce: old 1001 list reads for 1000 keys, 11 for
  10; new 1 and 1.
- TestServingReadsListsOnce, TestRestKeysIsRestKey,
  TestKeysOnDropsRepeats: same answers as before.

And the vendor reads. KeyBalances, PlanQuotas and remoteKeyCards started a goroutine
for each key and asked the vendor with all of them at once: a thousand
keys were a thousand requests to one relay together, each holding a
connection and its body.

Fix. keySlots holds each provider's reads to keyReadsAtOnce (8); other
providers' keys are read beside them. A read whose context is done goes
on without a slot and fails at once, as before.

Tests:
- TestKeyReadsEightAtOnce: a Command Code provider of 31 keys and a
  remote magpie of 31 keys behind a server that holds every request.
  Old code (the slots taken out by hand): 31 at once for balances, plans
  and remote limits; new: 8, every card still there.
- TestKeySlots: a done context takes no slot; another provider's keys
  have their own.

Run: gofmt -l (clean); GOOS=linux and GOOS=windows go vet -tags nogui
./... (clean); go test -tags nogui -count=1 -timeout 45m ./... in a sandbox HOME (GOCACHE, GOMODCACHE pinned) on af60154 with this series: 66 packages ok, 18 without tests, no FAIL (gateway 468.5s, provider 215.9s, gui 122.8s).
… on X)

@NetRozee on X: a custom endpoint given a thousand keys froze magpie.

Cause. The editor's Advanced > Proxy drew a line for every key, each
with a Provider's proxy | Direct | Custom control. Each control, once in
the page, looked up its place among every .segs in the editor and slid
its thumb (segsPlace, slide): quadratic, with a layout read each. The
Accounts list above it already folded (keyFold); this list didn't.

Fix. proxyLines folds a provider's keys as keyFold does: past KEYS_FOLD,
the first KEYS_FIRST and those with a proxy of their own, then All N
keys; opened, a filter (keyMatches, as the Accounts list's) over
KEYS_PAGE lines and how many more, and Show fewer. Typing draws the
lines again under the filter, which stays in the page (yetone#1055). A key not
drawn keeps its proxy: Save posts every key's own, drawn or not.
Subscription accounts' lines are unchanged. No new strings.

Measured (Playwright, the editor opened on a provider of N keys):
                1000 keys          5000 keys
  Chromium      1067ms -> 100ms    55.6s -> 93ms
  WebKit        4809ms -> 68ms     203s -> 81ms
  DOM           9388 -> 434 nodes  45388 -> 434 nodes

Tests: provider-many-keys-proxy.test.cjs (new, en and zh, Chromium and
WebKit): fails in all four on the old app.js (no All 300 keys). With
account-proxy, provider-many-keys, filter-ime, usage-keys-fold,
i18n-keys, click-scroll, provider-proxy, proxy-own-wording,
routing-proxy, plugin-proxy-page, signin-proxy and proxy-test: 221 of
221 pass in both engines.

Run: gofmt -l (clean); GOOS=linux and GOOS=windows go vet -tags nogui
./... (clean); go test -tags nogui -count=1 -timeout 45m ./... in a sandbox HOME (GOCACHE, GOMODCACHE pinned) on af60154 with this series: 66 packages ok, 18 without tests, no FAIL (gateway 468.5s, provider 215.9s, gui 122.8s).
provider-many-keys-proxy.test.cjs rerun after the suite: 4 of 4.
…k_li21 on X)

@mark_li21 asked: 能支持通过curl例子直接导入provider不? A curl example
from a vendor's docs, pasted into a custom provider's Base URL or API key,
now fills in the editor instead of being pasted as text.

curlOf (app.js) is one pure function. It reads the paste the way a shell
would: lines joined by \, cmd's ^ or PowerShell's backtick; curl.exe;
quotes and escapes. From that it takes:

- the API that the URL's endpoint names: /chat/completions is Chat,
  /messages is Anthropic and /responses is Responses;
- the base URL before that endpoint, spelled by respellURL the way the
  editor already wants it;
- the key from Authorization: Bearer, x-api-key or api-key;
- the model in the JSON body.

A key that is only a placeholder ($X, ${X}, %X%, <token>, YOUR_API_KEY)
is not taken. The key box is left empty and focused instead. A new
provider adds the model to its models. A saved provider's picks are left
alone, because adding to an empty list would narrow what it serves. A
preset's editor only takes the key.

A note in the editor says what was filled in. It also points out a
placeholder left in the URL, such as {WorkspaceId}, and a URL with no
known endpoint. Its four strings are translated into zh, zh-TW, ja and
de. Text that isn't curl pastes as before, including several keys at
once.

Found along the way: url.onpaste = fromCurl cancelled every plain paste
into the Base URL, because an on-handler that returns false cancels the
event. The handler is now wrapped. The test fails on the unwrapped one.

Fixtures, in internal/gui/tests/fixtures/curl, are the docs' own bytes:
- OpenAI chat (API reference) and Responses (quickstart);
- Anthropic Messages;
- DeepSeek, Zhipu, Moonshot and OpenRouter quick starts;
- Alibaba Bailian's Windows cmd example (^ and \"-escaped JSON);
- SAP AI Core's PowerShell one-liner (curl.exe, <Deployment_url>);
- Unsloth Studio's PowerShell example, with backtick continuations, as
  its usage panel renders the template. This one is not copied from a
  static doc page.

The doc row is "A pasted curl command" in
docs/subsystems/providers-accounts.md.

Run at 674b6ca (origin/main) plus this change:
- provider-curl-paste.test.cjs (new): 10/10. It covers Chromium and
  WebKit; curlOf on every fixture, with LF and CRLF line ends; and the
  editor in en and zh at 900px and 440px. The editor checks are the
  filled form, the focused key, the note's text, plain and many-key
  pastes, the saved body, no sideways scroll, no <select> and no page
  errors.
- Related GUI files, run together with the new one, 112/112:
  gui-de, gui-ja, gui-zh-tw, i18n-keys, key-import, nokey-editor,
  provider-add-gemini, provider-base-api, provider-many-keys,
  provider-many-keys-proxy, provider-url-each-api and vertex-editor.
- Broken by hand, the new test goes red:
  - fromCurl taking nothing: 8 of 10 fail (the editor runs);
  - the endpoint not cut from the base URL: 10 of 10 fail;
  - url.onpaste = fromCurl unwrapped: the plain-paste check fails.
- gofmt -l: none. GOOS=linux and GOOS=windows go vet -tags nogui ./...: ok.
- go test -tags nogui -count=1 -timeout 45m ./... in a sandbox HOME:
  exit 0, 66 packages ok, no FAIL.

Not tried:
- the real app (Wails) on macOS, or WebKitGTK on Linux;
- Windows;
- a pasted command used to reach a real vendor with a real key.
OpenPencil (ZSeven-W/openpencil, the openpencil-desktop app) keeps the
builtin agents its chat runs on in settings.json under Rust's
dirs::config_dir()/openpencil (read from its settings_io.rs and
settings_payload.rs). magpie adds one builtin_agents entry, id "magpie",
kind openai-compat at the gateway's /v1 with the magpie-openpencil key,
preset custom, the picked models as OpenPencil writes a list: the first
as `model`, `models` only for two or more, at most 64. The user's other
entries and settings stay, and so do the display_name and enabled they
give magpie's entry; a sync with nothing to change doesn't write. A file
magpie makes gets "version": 1 (OpenPencil sets a file without it aside
as corrupt); a file with another version, or none, is left alone.
Disconnect takes the entry out, and the file when nothing else is left.
OpenPencil reads the file once at start and writes it all back when it
quits, so a change is refused while openpencil-desktop runs (Windows
can't be asked, so the notice says to quit it first). Notice translated
in zh-CN, zh-TW, ja and de. README (en, zh-CN), docs/reference.md and
docs/subsystems/agent-wiring.md list it.

open-pencil/open-pencil, the other app of the name, keeps its providers
in its webview's localStorage, so it gets no row.

Run, under a scratchpad HOME with GOCACHE/GOMODCACHE pinned:
- go test -p 2 -tags nogui -count=1 -timeout 45m ./...: ok, 66 packages
  ok, 18 with no test files
- gofmt -l: clean; GOOS=linux and GOOS=windows go vet -tags nogui ./...: clean
- go test ./internal/agent -run '^TestOpenPencil': ok
- node --test agent-notice-lang, i18n-keys, header-relang (Chromium and
  WebKit, every language, wide and narrow): 37/37 pass
Broken by hand, each went red: the running check, the 64 cap, the
version check, keeping the user's display_name, the no-op sync, and
writing the version into a new file.

Real use: OpenPencil v0.8.4 (latest release, arm64, sha256 checked against
its SHA256SUMS) under a sandbox HOME, started on a settings.json magpie
wrote with the user's own Anthropic entry beside it. It started and ran
15s with the file intact and no .corrupt copy. v0.8.4 has no `models`
field, so it lists only the first magpie model; main lists them all.

Not tried: a chat request through OpenPencil's UI; its exit save (it
didn't quit on an AppleEvent and was stopped with TERM, so the write-back
wasn't seen); Linux and Windows; a build of OpenPencil's main.
TestPrimeAgent expects no detected agent before its config exists, but Agent.Detected also correctly finds prime-agent on PATH. A locally installed CLI made the full suite fail. Reproduced with unchanged production and test code at upstream a15e576; isolate PATH for this config-only fixture, as other config-detection tests do. No production behavior changes.

Verification: TestPrimeAgent fails on the untouched base with prime-agent installed, then passes with -race after this change. go vet ./internal/agent passes for darwin, linux (-tags nogui), and windows. The complete integration suite is being rerun; its final result belongs to the integration commit.
…#136)

Explicit CLI identity invalidation now clears user/plan/ok and removes only
that CLI's persisted answer, so a failed first ask cannot serve the old
account or restore it after restart. Ordinary failed refreshes still keep
the previous answer, and the generation guard still discards an old ask.
Update the providers/accounts reference to document this failure behavior.

On upstream a15e576 with the new test and
unchanged production code, TestCLIIdentityForgetThenUnanswered fails with
old@example.com / Pro / true after forget plus a failed ask. It passes
with this fix. Tests cover another CLI's persisted entry and in-flight
invalidation too. Devin output-before-exit handling is already upstream;
add nonzero signed-in/signed-out cases to TestAskDevinStatus rather than
restoring the old implementation.

Verified with Go 1.26.8 under a temporary HOME and pinned Go caches:
- gofmt -l changed Go files: no output; git diff --check: clean.
- CLI identity and Devin status tests: pass, including -race.
- go vet ./... on macOS; GOOS=linux go vet -tags nogui ./...;
  GOOS=windows go vet ./...: all pass.
- macOS, Linux nogui, and Windows go build: all pass.

No real vendor-account check or native Linux/Windows runtime test was run.
No GUI files changed; GUI browser suites were not run.
Preserve the original PR head 0aaae4c
in ancestry while retaining the tested tree based on upstream
 a15e576. Upstream already implements
the unanswered-status handling and parses Devin output before exit status;
keep that newer implementation rather than reintroducing obsolete APIs.
The preceding commits fix explicit identity invalidation and isolate the
Prime Agent config-detection fixture from a CLI installed on PATH.

Final integration validation (Go 1.26.8, temporary HOME, pinned caches):
- go test -tags nogui ./... -timeout=45m: PASS, exit 0, 892.1 seconds.
- CLI identity/Devin tests and isolated TestPrimeAgent with -race: PASS.
- macOS, Linux nogui, Windows go vet ./... and builds: PASS.
- Affected agent tests re-vetted for all three platforms after isolation:
  PASS. Changed Go files formatted, git diff --check clean.
The new forgotten-account test fails with old@example.com / Pro / true
on unchanged upstream production code, then passes with this fix.

The first short-timeout suite was stopped and is not a pass. The next
full run failed only TestPrimeAgent, independently reproduced at the
unchanged upstream base and fixed in its own preceding test-only commit.
The final complete suite passes. No GUI browser suite, real vendor-account
check or native Linux/Windows runtime test was run.
@MumuTW MumuTW changed the title accounts: an agent CLI that didn't answer no longer signs its account out accounts: do not restore a forgotten CLI identity after a failed status check Oct 16, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants