2390 Commits
Author SHA1 Message Date
ggbdpqandfactory-droid[bot] d81706f974 gateway: a Chat or Responses client's xhigh on Claude Opus 4.7 and later, Sonnet 5 and later or Haiku 5.5 goes as xhigh, not max
buildAnthropic asked every Claude that thinks only adaptively for max
when the client asked xhigh, on the grounds that 4.6 has no xhigh. Only
Opus and Sonnet 4.6 lack it: Anthropic's effort docs list xhigh on
Fable 5 and 5.1, Mythos 5 and 5.1, Opus 5.5, 5, 4.8 and 4.7, Sonnet 5.5
and 5, and Haiku 5.5, and describe max as no constraint on token
spending. So Codex, Cursor or any Chat client picking xhigh on
claude-opus-5-5 was billed for max. Claude Code's own levels in magpie
(claudeEffortsFor) already say only Opus and Sonnet 4.6 skip xhigh.

Now xhigh becomes max only on 4.6 (noXhigh, which reads the version
through claudeVersionOf, shared with adaptiveOnly). A relayed thinking
budget is unchanged: budgetOf gives xhigh and max the same budget, and
31999 is Claude Code's old ultrathink, so a budget above high still
goes as max. docs/subsystems/gateway-routing.md now says which models
are asked adaptively (#1470) and where xhigh stays xhigh.

What was run: TestXhighStaysXhighWhereClaudeTakesIt (Chat and Responses
through the server stack to a fake Anthropic upstream) fails on the old
main with output_config = map[effort:max], want "xhigh" for Opus 5.5
(Chat and Responses), Opus 4.7, Sonnet 5 and Haiku 5.5, and passes with
the change; Opus and Sonnet 4.6 get max before and after, and a Fable
5.1 row asserts xhigh. TestAdaptiveThinking's Opus 5.5 xhigh case now
asserts xhigh, beside a 4.6 case asserting max. Broken by hand (noXhigh
always false) the 4.6 cases fail. gofmt -l prints nothing; go vet -tags
nogui ./... passes for darwin, linux and windows; go test -tags nogui
-count=1 ./... under a temp HOME passes. Through the aihubmix relay,
claude-opus-5-5 with adaptive thinking and effort xhigh answers 200.
Not tried against Anthropic directly (no credential outside the
keychain); a relay that checks effort against an older list could still
refuse xhigh.

Reported and fixed by ggbdpq (#1471).

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-10-10 15:17:13 +08:00
ggbdpqandfactory-droid[bot] 55ac9ec5d2 gateway: Claude Fable and Mythos are never sent a thinking budget, so a Chat client's reasoning_effort or a relayed budget no longer gets their 400
adaptiveOnly read a Claude's version with claudeVersion, which knew
opus, sonnet and haiku only, while claudeLine (thinkingOffOf,
samplingRefused) already knew fable and mythos. So claude-fable-5,
claude-fable-5-1, claude-mythos-5 and claude-mythos-5-1 were taken for
models that take thinking.type=enabled with budget_tokens:
buildAnthropic turned a Chat or Responses client's reasoning_effort into
a budget, forwardOnce's adaptiveThinking relayed an agent's budget as it
came, and AdaptiveThinking didn't tell Pi (forceAdaptiveThinking), dsh
or omp (anthropic-adaptive) to ask adaptively. Anthropic documents that
Fable 5 and 5.1 and Mythos 5 and 5.1 answer thinking.type=enabled with
budget_tokens with a 400 and take only {"type": "adaptive"} with the
effort in output_config.

claudeVersion now reads fable and mythos too, so all three paths treat
these models as Opus 5.5 is treated. claude-mythos-preview has no
version in its id and is left as it was.

What was run: TestFableAndMythosNeverGetABudget (a fake Anthropic
upstream answering 400 to enabled) fails on the old main: adaptiveOnly
false for all five ids, and status 400 for a relayed Fable budget, a
Chat client's reasoning_effort on Fable and a relayed Mythos budget. It
passes with the change, as do TestAdaptive*, TestThinkingOff*,
TestSampling*, TestClaudeFiveFive* and TestHaikuFiveFive*. Broken by
hand (mythos dropped from the regex) the Mythos cases fail again.
gofmt -l prints nothing; go vet -tags nogui ./... passes for darwin,
linux and windows; go test -tags nogui -count=1 ./... under a temp HOME
passes.

Not tried against Anthropic itself: the owner's Claude sign-in keeps
its token in the keychain, which a sandbox doesn't read (and must not
refresh), and the one relay with Fable listed (aihubmix) has no balance
for it; that relay also takes a budget on Opus 5.5, so it doesn't show
Anthropic's refusal either.

Reported and fixed by ggbdpq (#1470).

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-10-10 15:17:11 +08:00
ggbdpqandfactory-droid[bot] 8714abd45d agent: TestUpdateWithBunBridgesSOCKS reads only the proxy variables it sets, so a machine with NO_PROXY or NODE_USE_ENV_PROXY set no longer fails it
The test collected every variable whose name ends in _PROXY from
updateEnv's environment and required each to be a bridged
http://127.0.0.1 URL for bun. updateEnv passes the rest of the
environment through, so a NO_PROXY of the machine running the test
("bun is given localhost,127.0.0.1") or Node's NODE_USE_ENV_PROXY=1
("bun is given 1") made it red, though neither names a proxy. It now
reads the six proxy variables it sets (the ones netproxy.EnvForBun
bridges, in either case). Test-only; updateEnv is unchanged.

What was run: on the old main the test is red with
NO_PROXY=localhost,127.0.0.1 and, separately, with NODE_USE_ENV_PROXY=1
in the environment, green with neither. With this change it is green
with both set. Broken by hand (updateEnv's netproxy.EnvForBun call
removed) it fails with "bun is given socks5://127.0.0.1:4401". gofmt -l
prints nothing; go vet -tags nogui ./... passes for darwin, linux and
windows; go test -tags nogui -count=1 ./... under a temp HOME passes.
Not run on the Windows box.

Reported and fixed by ggbdpq (#1472).

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-10-10 15:17:08 +08:00
yetone ddd4f43144 provider: a routing group can be duplicated, from its menu on the Routing page or with magpie group copy (lc on Discord)
provider.CopyGroup(id, name) saves a copy of the group as it is now: its
models and patterns, routing, stays, rules, classifier, effort, context,
levels, and fast and off members. The copy is deep, so its rules' time
windows aren't shared. Its name is the one given, or "<name> copy", with a
number added past a name in use. Its id comes from the name, numbered past
any id in use, removed ones included. It is listed right after the group
it copies. The copy is a group of the user's and switched on, even when
it copies a found group or a switched-off one. The original is unchanged.

Surfaces:
- GUI: a group row's menu (right-click, or a click on its logos) has
  Duplicate under Move up and Move down. It posts /api/groups/copy with
  the id and t("{name} copy"), so the name is in the reader's language.
  It asks before discarding an editor with unsaved changes. The status
  then says "Duplicated {name}". No click scrolls the page.
- CLI: magpie group copy <id> [name]; duplicate, dup and clone work too.
- zh, zh-TW, ja and de strings. docs/reference.md and the site's start
  page (en, zh, ja) list the command.

Run:
- TestGroupCopy (internal/gui): the copy has the models, pattern,
  routing, rule and off member, is on, and is listed after the original.
  The original stays off and unchanged. A second copy is "Fast copy 2" /
  fast-copy-2. An unknown id is refused.
- TestGroupCopyCLI (root): the default name, a given name, a found
  group's copy as the user's, and the errors.
- group-duplicate.test.cjs: Chromium and WebKit, en/zh/zh-TW/ja/de at
  420px, 10/10. It fails 10/10 on the old routing.js. Siblings
  group-arrange, removed-groups-menu-scroll, group-bulk-remove and
  click-scroll pass 36/36.
- Sandbox CLI (sandbox HOME): group copy and duplicate made "DupTest copy"
  and "DupTest copy 2", listed after DupTest.
- gofmt -l clean. go vet darwin/linux/windows on the root, provider and
  gui packages: ok.
2026-10-10 15:08:14 +08:00
yetone 8b56cf2d57 provider: an Antigravity or Gemini CLI sign-in's models can be tested, asked through the gateway's translator (lc on Discord)
lc: "antigravity的测试按钮不可用,别的供应商正常" — even after restarting
magpie and signing in again. A Google sign-in speaks only Code Assist, an
API whose envelope (project, model variant, request wrapper) the gateway
builds for each agent request. The provider test only sends its own tiny
request to a Chat/Responses/Anthropic/Gemini URL, so ModelTest() returned
"own-api" and the GUI turned Test models and each model's test off.

Now the gateway registers provider.ProbeTranslatedVia, the same pattern as
ProbeClaudeVia. For a Google Code Assist account, Test and TestModels ask
the model a "hi" (max 16 tokens) through askTranslated, the same path an
agent's request takes. The reply is read up to the first word, thought or
tool call, or until the vendor's error, which is reported with its status.
ModelTest() is "" for such an account, so the GUI's buttons are on. Plugin
providers and other own-API sign-ins (Kiro, Cursor…) are unchanged.

Run:
- TestAntigravityModelTest (internal/gateway): an Antigravity login and a
  fake Code Assist upstream. ModelTest is "". TestModels is OK on
  CodeAssist with the account named, and sent one :streamGenerateContent
  with project p1 and "hi". Provider Test is OK. A 403 PERMISSION_DENIED
  comes back as status 403 with its message. With testsTranslated forced
  false by hand (the old behaviour), it fails: "own-api".
- go test -tags nogui ./internal/{provider,gateway,gui} under a temp HOME
  with pinned caches: ok. gofmt -l clean. go vet darwin/linux/windows on
  those packages: ok.
- Not tried against a real Antigravity account. Live state was read only.
2026-10-10 15:08:14 +08:00
yetone df2b44ef6c provider: a model's test can be asked as Codex or Claude Code asks, for a relay that serves only them (耍赖天都爱 on Discord)
A relay that serves only Codex or Claude Code tells them by their headers
("This account only allows Codex official clients"). The gateway already
lets those headers go on to a provider reached with a key, so the agents'
own requests were served, but the provider/model test is magpie's own
tiny request (Go's User-Agent), so it was turned away and said the
provider didn't work.

provider.TestAs(ctx, "codex"|"claude-code") asks Test and TestModels as
that agent asks. It goes to that agent's API (Codex: Responses, else Chat;
Claude Code: Anthropic Messages), streamed, store:false on Responses, and
with the headers the gateway last let through for it. The gateway notes
them with provider.SawClient beside the existing pass-through, without the
session, thread, turn, request id, subagent kind, anthropic-beta or
anything that signs. They are kept in client-shapes.json so the CLI has
them too.

Claude Code's headers are never made up. Until a Claude Code request has
come through magpie, the test says so. They are sent only to a relay,
never to anthropic.com, googleapis.com, Bedrock or Azure. A signed-in
account, a decision-only provider, and OpenCode/Cline/Kilo (which already
ask as their own client) are not offered it. Codex's fall back to the
headers a ChatGPT account's requests use (codexSign) until Codex has come
through.

Surfaces:
- CLI: magpie provider test <id> [--as codex|claude-code] [model…]
- GUI: a model chip's right-click has "Test as Codex" and "Test as Claude
  Code" where the provider offers them (providerJSON.testsAs). An item that
  can't run yet is off, with the reason shown. zh, zh-TW, ja, de strings.

Run:
- TestModelTestAsClient (internal/gateway). A fake relay 403s anything but
  codex_*/claude-cli/. The plain test gives 403. As Codex it is OK on
  /v1/responses with stream+store:false and codex_cli_rs headers. As
  Claude Code it is refused before Claude Code came, then OK after one
  Claude Code request through the gateway, with its UA/x-app/x-stainless
  but no session id or beta. Anthropic's API is refused. With the beta
  filter removed by hand the test goes red.
- model-test-as.test.cjs, Chromium and WebKit, en/zh/zh-TW/ja/de at
  420px: 10/10. On the old app.js it fails (no such items). Siblings
  model-test-one, model-copy-id, model-test-why, provider-typed: 28/28.
- Sandbox CLI (fake chat relay on :3571, sandbox HOME):
  - provider test relaya --as codex deepseek-chat sent
    codex_cli_rs/0.162.0 … with originator and version, stream:true.
  - --as claude-code said the relay has no Anthropic endpoint.
- go test -tags nogui ./internal/{provider,gateway,gui} and the root
  package under a temp HOME with pinned caches: ok. gofmt -l clean.
  go vet darwin/linux/windows on those packages: ok.
2026-10-10 15:08:14 +08:00
yetone 4df1402d10 provider: Provider in model names Off names a remote magpie's models alone, not with the other magpie's provider or "routing group" after them (ARNO on Discord)
A remote magpie's list names each model by its label there (magpie_label),
which carries that magpie's provider, or "· routing group", by that
magpie's own setting. Here it was taken as the model's name, so turning
Provider in model names Off on this computer still left "DeepSeek Chat ·
Relay A" and "Fast · routing group" in every agent's list.

The list's display_name, the name there alone, is now kept beside it
(catalog.Model.Plain, Entry.Plain). provider.Labels names an entry by it
under Off, and under own for a name the user gave; two that would read the
same keep their labels as before. On is unchanged ("DeepSeek Chat · Relay A
· Remote magpie"). /v1/models' display_name is that name alone too, as it
is for every other model, so a magpie chained after this one gets it. The
provider editor's Names & levels line says the same ("plain" in the
provider's model JSON).

Reproduced in a sandbox: fake vendor on :3571, magpie A on :3572 with it
and a group, magpie B on :3573 with remote-magpie → A and `model suffix
off`: B's /v1/models said "DeepSeek Chat · Relay A", "Fast · routing
group". With the fix: "DeepSeek Chat", "Fast"; on: "DeepSeek Chat · Relay
A · Remote magpie".

Run:
- TestRemoteMagpieSuffixOff (internal/gateway, A's list bytes as served)
  fails on the old code (off: "DeepSeek Chat · Relay A", "Fast · routing
  group"), passes with the fix.
- name-suffix-remote.test.cjs, Chromium and WebKit, en/zh/zh-TW/ja/de at
  420px: 10/10; on the old app.js it fails ("… · Relay A"). Sibling
  name-suffix-*.test.cjs and names-fold: 86/86.
- go test -tags nogui ./internal/{catalog,provider,gui,agent,gateway}
  under a temp HOME with pinned caches: ok. gofmt -l clean; go vet
  darwin/linux/windows on the touched packages: ok. Full suite before push.
2026-10-10 15:08:14 +08:00
yetone 9e665491ec context: Tuned for you — each agent's settings worked out from the user's own calls, for the fewest tokens, put in on a click and undone on another
The Context tab now replays the period's calls of each agent against the
settings that decide what they cost, and shows, per agent and per model,
the value that would have used the fewest tokens for how this user works:
Claude Code's auto-compact window per model (modelSettings.<model>.
autoCompactWindow), its main and subagent prompt cache lifetimes
(promptCacheTtl, subagentPromptCacheTtl: 5m or 1h), and Codex's
model_auto_compact_token_limit. Each row says where the value comes from
now (the agent's default, the user's own setting, one magpie set, or a
variable that comes first and locks it), why (how fast conversations grow,
what a compaction leaves, how long the user pauses), the cost curve, and
the saving. Nothing is written until "Use …" is clicked; what was there is
stashed and Undo puts it back, unless the user changed it since. A window
the user set past the model's own is shown capped, as Claude Code caps it
(min(setting, model window)), so no saving is claimed that wouldn't happen.
Advice under a 3% saving is "already the leanest".

On the owner's 7 days (88K Claude Code calls, 956 Codex): opus keeps 200K
(their 500K is capped to it), fable 5.1 200K → 160K saves 5% (2.56M
tokens), both cache lifetimes and Codex's 272K are already the leanest.

Run: go vet ./... and go test ./... pass (internal/tune, internal/agent
TestTune*: user-set values kept, env locks, undo after a user edit, TOML
key order). context-tune.test.cjs passes in Chromium and WebKit, en/zh/
zh-TW/ja/de, at 360 and 1100px (22); context-window, context-scroll-end
pass. Full GUI suite: library-servers-bulk and market-have failed once
under load and pass alone; market-have fails on origin/main 494dedcf too
(1 of 8 runs). Claude Code 2.1.296 in a sandbox config dir shows
"Auto-compact window for Fable 5.1: 160k tokens (from settings)" for the
written modelSettings key. The real-data advice was read from the owner's
own ~/.claude and ~/.codex, nothing written.
2026-10-10 15:05:53 +08:00
yetone a154197bbb accounts: ChatGPT accounts come in from codexbar's config.json (碳碳双键)
碳碳双键 on Discord (#general): several Codex accounts switched with
codexbar on the old computer; after a magpie backup and restore the new
one had only the account Codex itself was signed in to. A backup carries
no sign-in by design (a refresh token rotates, so two computers can't
share one); importing is how accounts move. But codexbar's own store,
~/.codexbar/config.json, wasn't a shape the importer knew: its accounts
sit under providers[].accounts, so the whole file was read as one entry
with no token ("#1: failed").

codexbarEntries reads it as codexbar (lizhelang/codexbar 9f2e0f53)
writes it (CodexBarConfig.swift, CodexBarConfigStore): the accounts of
the provider of kind openai_oauth and openAI.remoteConnectionAccounts,
camelCase tokens, the workspace from openAIAccountId (its id is
codexbar's own). api_key accounts are said to be keys; other providers
(API endpoints of other vendors) are left alone; a sign-in in both places
is read once. codexbar signs in with Codex's own OAuth client, so the
refresh works as for Codex's auth.json. Its own export (Sub2API's shape)
was already read.

The import window's texts (en, zh, zh-TW, ja, de), the CLI's warning and
the docs name codexbar; reference.md says a backup carries no sign-in and
that importing the old computer's file is how accounts move.

Run:
- TestImportCodexbarConfig, fixture in codexbar's encoder output (pretty,
  sorted keys, ISO dates; two Team seats, a duplicate remote account, an
  access-only account, a DeepSeek api_key provider): fails on the old code
  ("got #1:failed"), passes on the new; seats kept apart by
  openAIAccountId with an id token that doesn't name the workspace.
- Built nogui binary, sandbox HOME: `magpie accounts import codex
  config.json --yes` lists the three accounts, ChatGPT refusing the fake
  tokens; no real codexbar install here to try with real accounts.
- login-import.test.cjs (chromium, webkit; en, zh) and i18n-keys.test.cjs
  pass.
- go test -tags nogui ./... (temp HOME, pinned caches): all ok.
- go vet (darwin, GOOS=linux, GOOS=windows) on . and internal/provider;
  gofmt -l clean.
2026-10-10 15:04:23 +08:00
yetone 494dedcf8f desktopdir: Claude Desktop installed from the Microsoft Store (package AnthropicPBC.Claude_*) has its folders found, as the setup.exe's Claude_* package's are
Desktop 2.31226.1's own code knows two MSIX package families, Claude_pzs8sxrjxfjjc
and AnthropicPBC.Claude_fnn82j28hfe8t (hze/_ze in index.chunk-Cyt2WTko.js).
packageDir only globbed Packages\Claude_*, so a Store install's LocalCache
was never found: magpie wrote Claude Desktop's gateway, MCP servers and
read its sessions and skills in the unpackaged folders instead.

Checked while following up v0.1.1156: the same bundle has a
%LOCALAPPDATA%\Claude-Data folder for 1p userData, but in 2.31226.1 it is
dormant. globalThis.userDataMoved is only read, never set, and the
Claude-Data dir is chosen only when %APPDATA% is a network share, while
relocateUncUserData does nothing in index.pre.js. So 1p userData is still
%APPDATA%\Claude (or the package's copy), and desktopdir is left as it is for
that.

Run: go test ./internal/desktopdir ./internal/library ./internal/agent
(macOS); TestFindMSIX fails on the old packageDir for the Store family;
desktopdir's Windows test binary passes on the Windows box; go vet for
linux, windows and darwin.
2026-10-10 14:55:16 +08:00
yetone 9e35eef336 gateway: TestKimiHeldBurstTellsNoSpeed waits long enough that one pause of the runner mid-burst no longer reads the held call as a speed (#1469)
The test timed its replies over a wait shortened from John's (first
content at 1.3 s, the end at 4 s). The held call is told held when its
flow (~245 ms for the 236 ms burst) is under a quarter of its window
from its first text (usage.HeldShare); a 2.5 s window put that bound at
~620 ms, so a single pause of ~400 ms that split the burst crossed it.
macOS CI under -race read flow 856 ms, ms 4517 (517 ms late) and
176 tok/s: one ~520 ms pause mid-burst.

Reproduced by injecting the pause (not rerunning): a 520 ms sleep in
the fake before the 25th write of the chat held call read 182 tok/s,
flow 773, ms 4405, as CI did. The classifier is right about such a
stream; the fixture sat too near its threshold. The wait is now first
content at 0.5 s and the end at 7 s: a 6.3 s window bounds flow at
~1.6 s, and the steady 50 tok/s reply stays within its 20% with a pause
as long.

Run:
- injected 1 s pause in the chat held call, held answer, steady, and
  in all nine replies: ok each; a 2 s pause still fails the held call
  (flow 2621), so the bound is where the comment says.
- break by hand: HeldShare check off -> the three held calls read
  84 tok/s, red; ThinkPace check off -> the three held answers read
  ~2,237 tok/s, red.
- go test -race -count=10 -run '^TestKimiHeldBurst' ./internal/gateway/ ok.
- gofmt -l clean; go vet darwin/linux/windows ./internal/gateway/ ok.
2026-10-10 14:52:56 +08:00
yetone 82990126db sessions: a gateway turn cut off mid-write no longer makes every later turn of its session fail on Windows (#1468)
turns.jsonl was opened with O_APPEND. On Windows Go opens such a file
with FILE_APPEND_DATA and without write access, so the Truncate that
drops a cut-off last line was refused ("Access is denied"), and
SaveGatewayTurn failed for every later turn of that session. The file
is now opened read-write without O_APPEND and the line is written with
WriteAt at the end found (gatewayMu already keeps another save from
writing in between). No other file in the repo is both opened with
O_APPEND and truncated.

Run:
- Windows box (xiaomi-gpu), GOOS=windows test binaries of
  internal/sessions under a scratch USERPROFILE/APPDATA/TEMP:
  origin/main (f9113626): TestGatewayConversation* -count=20 fails
  TestGatewayConversationInterruptedAppend every time with "truncate
  ...turns.jsonl: Access is denied"; this commit: -count=20 PASS, whole
  package PASS.
- macOS: go test -race -count=5 ./internal/sessions/ ok.
- gofmt -l clean; go vet darwin/linux/windows ./internal/sessions/ ok.
2026-10-10 14:52:56 +08:00
yetone e04824714d library: a skill's templates and other big files go with a backup and a sync (Crispin)
Crispin on Discord (#help): a skill's template files, a PPT among them,
didn't reach the other computer. Collect read each skill file with a 2 MB
cap and left everything bigger out, so a .pptx template with its pictures
never travelled. Links, copies and WSL copies on one computer already carry
every file (copyDir and hashFiles have no filter); only the backup/sync
carry dropped them.

- A file goes up to 16 MiB, all skills together up to 32 MiB. A sealed
  backup is JSON with the bytes base64'd twice (~x16/9), and a sync reads
  at most 64 MiB of one, so 32 MiB raw is what still fits.
- readSkills fills that budget smallest file first across every skill, so
  one skill's big file never keeps another skill's SKILL.md from coming.
- What is left out (Left) and the computer being put to already has is
  kept (keepLeft): a Put no longer sets the folder aside without it. A Left
  name is checked like a carried one.

Run:
- New tests fail on the old code: TestCarryTemplates (template left),
  TestCarrySmallFirst (zzz's notes.md left), TestCarryKeepsWhatWasLeft
  (the local file went), TestSkillTemplatesTravel (0 bytes of 6291456).
  Pass on the new. TestSkillTemplateReachesEveryAgent covers link, copy
  and WSL copies of a pptx.
- Break check: maxCarriedAll = 48 MiB makes TestSkillTemplatesTravel fail
  ("the backup is 64 MiB, more than a sync reads").
- Real use: built nogui binary, sandbox HOMEs, a real 2.8 MB and a real
  14 MB .pptx in a library skill, `magpie backup` then `restore
  --no-agents` on another HOME: sha256 identical both times.
- go test -tags nogui ./... (temp HOME, pinned caches): all ok.
- go vet (darwin, GOOS=linux, GOOS=windows) on internal/library and
  internal/backup; gofmt -l clean.
2026-10-10 14:52:17 +08:00
yetone 8424a004e2 gateway: a reply whose reasoning loops before it says anything is asked again once in place, not ended (cdredfox on X)
cdredfox (X, 2108799237324701897): WorkBuddy AI's DeepSeek v4 flash through
magpie often stops half way with "magpie ended this reply: the model's
reasoning was stuck in a loop ... ("OK." "Writing." "Now." "Producing."
"Let me output." ...)". That is the #1359 loop, found correctly; nothing
magpie sends causes it (12 direct runs on the real account, effort
high/max, temperature 0/1/unset, a 900-line file: no loop, the same
"OK. Writing." precursors at the tails), so the model does it on its own
now and then, and the agent's turn ended on it.

When magpie translates the reply into the agent's protocol
(streamTranslated: Claude Code, Codex, Gemini clients) and the loop is in
the reasoning, before any text or call and not on a continuation, the
try is let go and the same request asked again once (loopReasks), the
way an empty reply is (#667): the agent keeps the reasoning it has, the
next try's follows in the same stream. A loop on that try ends the reply
with the error as before. holdWriter.onLoop lets streamTranslated take a
trip before cutLoop ends the stream. A text loop, a loop after a text or
call, and a Chat stream passed through stay ended at once.

Run: go test ./internal/gateway (ok); -race on the loop and empty-reply
tests -count=3 (ok); gofmt; GOOS=linux,windows go vet. On old code
TestReasoningLoopIsAskedAgain and the updated
TestLoopEndsTheReplyOnEveryProtocol/TestLoopEndsWorkBuddysReply fail
(the vendor is asked once). Real account: a sandbox gateway in front of
a proxy signing for WorkBuddy AI (deepseek-v4.1-flash); the proxy
injected the reporter's units into the first real stream, the guard let
it go after 257 lines, the second real request answered, and the agent
got one whole message (Anthropic: message_stop, no error; Responses:
response.completed).
2026-10-10 14:51:42 +08:00
yetone a6c34c86b8 tui: a provider of your own (OpenAI, Responses, Anthropic or Gemini compatible) is added from the Providers page, at its base URL, as the app's editor adds it (hezz1891)
The TUI's `a` listed only vendors magpie knows and pointed to `magpie
provider add <name> url=…` for any other, so on a server the TUI couldn't
add one's own relay (hezz1891 on Discord).

`a` now also offers custom-openai, custom-responses, custom-anthropic and
custom-gemini (the line says "or custom for your own API"). Picking one
asks for its base URL (an http(s) address, else said so), then its name
(enter for the host's), then its key, then adds it with provider.Add
and fetches its list, as a preset is added. The API sets the same URL
field as the CLI's url=/responses=/anthropic=/gemini=.

Run:
- TestTUIAddsACustomProvider: an OpenAI compatible provider on httptest
  is added and lists its 2 models; a non-URL is refused; Responses,
  Anthropic and Gemini each set their own URL; an empty name is the host.
  It fails with the custom items taken out of the list.
- go test -tags nogui ./internal/tui (temp HOME, pinned caches): ok;
  the tui tests built for linux/arm64 pass in debian:12-slim
- gofmt -l: none; go vet (darwin), GOOS=linux and GOOS=windows go vet
  -tags nogui ./...: clean; darwin, linux, windows builds
2026-10-10 14:45:47 +08:00
yetone 3ee97def30 claude: a server or container without Claude Code can have magpie download Anthropic's own build, asked first and checked against its manifest (Jorben)
A Claude subscription signs in and answers through the local `claude`, so
on a server or in the Docker image, which has none, it couldn't be used
(Jorben on Discord: could it be downloaded on first use, as Bun is).

- internal/claudecode: Anthropic's native build, as claude.ai/install.sh
  takes it: downloads.claude.ai/claude-code-releases/latest names the
  version, its manifest.json the platform's SHA-256 and size (linux
  glibc/musl, darwin, win32). A build that doesn't match both is not kept.
  It goes to <cache>/claude-code/<version>/claude, one version kept. Find
  takes the machine's own claude first, then the download.
- magpie claude-code [install [--yes]|remove]: says the version, platform,
  size, the URL it comes from, where it goes, that its checksum is checked
  and that it is Anthropic's software under Anthropic's terms, then asks
  [y/N]. magpie accounts add claude offers it when there is no Claude Code
  (not on Windows, where one in WSL may be the one).
- the gateway's claudeBinary and the sign-in's claudeExecutable take
  claudecode.Find; their "not installed" errors name the command.
- Docker docs (en, zh, ja): Claude sign-in needs `docker exec -it magpie
  magpie claude-code install` first; kept in /config/cache.
  The Dockerfile is unchanged: distroless cc-debian12 has glibc.

Run:
- go test -tags nogui ./... under a temp HOME with pinned caches: all ok
- gofmt -l: none; go vet (darwin), GOOS=linux and GOOS=windows go vet
  -tags nogui ./...: clean; darwin, linux, windows builds
- TestClaudeSubscriptionRunsTheDownloadedClaudeCode fails on the old
  proc.FindTool lookup ("Claude Code is not installed"); taking the
  checksum compare out fails TestInstallRefusesABuildTheManifestDoesntName
- real: magpie claude-code install --yes downloaded Claude Code 2.1.296
  darwin-arm64 (230 MB) into a sandbox HOME, checked; it ran --version
- real, in gcr.io/distroless/cc-debian12:nonroot with the image's /config
  layout: install downloaded 2.1.296 linux-arm64 (245 MB) to
  /config/cache/magpie/claude-code, it ran `2.1.296 (Claude Code)` there,
  and magpie accounts add claude went on to print the claude.ai sign-in
  link and ask for the pasted address
- the claudecode tests built for linux/arm64 pass in debian:12-slim
2026-10-10 14:44:57 +08:00
yetone c431225a08 gateway: a browser that reaches loopback under a web site's name (DNS rebinding) is refused unless it sends an enabled gateway key
What changed
- lanGuard refuses (403) a loopback request that carries a browser's
  marks (Origin, Sec-Fetch-Site/Mode, a Mozilla/ user agent) when its Host
  isn't this computer's: an IP, localhost, *.localhost, a container
  runtime's name for the host (host.docker.internal and kin),
  MAGPIE_PUBLIC_URL's or MAGPIE_ADDR's host, or the machine's own name.
  An enabled gateway key still lets it in.
- Why: a page whose DNS answers 127.0.0.1 after it loads is same-origin
  with the gateway. Its GET sends no Origin and its POST one equal to the
  Host, so corsGuard's foreignPage let it by, and on loopback no key is
  asked. The nightly security review reproduced it on origin/main: Host
  and Origin rebind.attacker.example:3425 from 127.0.0.1 got 200 on
  GET /v1/models, POST /v1/chat/completions (spends subscriptions) and
  GET /v1/magpie/quotas (account names, balances). The subsystem doc
  listed it as a known gap.
- Agents send none of a browser's marks and pass under any name; browsers
  and webviews calling localhost or an IP are unchanged.
- Tests: TestCORSKeyThroughTheServer and the Omarchy test now set a real
  Host (httptest's default is example.com). TestForeignPagesRefused's
  "own origin behind a port-443 name" row from loopback with no forwarding
  header now sends a key: without one it is the rebound page.
- docs/subsystems/gateway-routing.md describes the check in place of the gap.

Ran
- TestReboundPageRefused: fails on origin/main ("a rebound page's GET:
  200"), passes with the change.
- go test -count=1 ./internal/gateway/ ./internal/gui/ ./internal/access/
  (macOS, go1.27): ok. gofmt -l clean; GOOS=linux and GOOS=windows go vet
  ./internal/gateway/ clean.

Not tried: a real browser against a real rebinding DNS name.
2026-10-10 14:21:23 +08:00
yetone a21a2b54f1 gateway: a Codex title the model was cut short on before any text goes back as response.incomplete with its reason and usage, not as a completed reply with nothing in it (#1466, Edward4226)
codexTitle read the title model's reply with compactReply, which kept
the output and usage of response.completed and response.incomplete alike
and dropped how it ended, and writeTitleReply always wrote status
"completed". A title cut short at max_output_tokens (all of it spent
reasoning) reached Codex as a finished reply with output [].

What changed:
- compactResult keeps the response's status and incomplete_details
  (from the terminal event's response, or its type).
- codexTitle: when no title can be read and the reply was incomplete,
  writeTitleEnd streams response.created then one response.incomplete,
  status "incomplete", the vendor's incomplete_details and usage. A
  title read from the reply still completes with it; Titles off still
  completes with nothing (writeTitleReply is writeTitleEnd "completed").
- Compaction (codexCompact) is left as it was: a summary cut short with
  text in it is still used.

Codex: codex-rs/codex-api/src/sse/responses.rs takes response.incomplete
with a reason other than interrupted/content_filter as a stream error
("Incomplete response returned, reason: max_output_tokens"), which
core/src/responses_retry.rs retries up to the provider's
stream_max_retries; then the hidden title turn fails and
tui/src/temporary_structured_request.rs sets no title, as it did for
the empty completed reply. This is what Codex does when its own backend
answers so.

Run:
- TestCodexTitleIncompleteStaysIncomplete (Responses upstream, #1466's
  two-field schema) and TestCodexTitleIncompleteFromChat (Chat upstream,
  finish_reason length) fail on the old code (one response.completed,
  status completed) and pass now; the same test's whole title under the
  same schema completes. Existing TestCodexTitle*/TestTitle* pass.
- gofmt -l clean; go vet -tags nogui ./internal/gateway on darwin,
  GOOS=linux, GOOS=windows.
- go test -tags nogui -count=1 ./... under a temp HOME: all ok (with
  ./internal/gateway and ./ rerun after this change).
- Real use: the built binary's gateway in a sandbox HOME with only the
  owner's DeepSeek provider, the issue's title request on
  deepseek-flash. max_output_tokens 16: old response.completed,
  output [], status completed; new one response.incomplete, reason
  max_output_tokens, usage 16 reasoning tokens. 2048: a title and
  description, response.completed, before and after.
2026-10-10 14:14:47 +08:00
yetone ea84c99c78 gateway: a DeepSeek model's tool call the client kept no thinking for goes with reasoning_content, so DeepSeek no longer turns the request away (#1462, Clear8Heart)
DeepSeek's Chat API in thinking mode refuses a whole request with 400
"The `reasoning_content` in the thinking mode must be passed back to the
API" when an assistant tool-call message since the user's last message
comes back without reasoning_content. OpenCode (and any client that
continues a turn another model made, or that dropped the thinking) sends
such turns, and magpie relayed them as they came.

What changed:
- chatReplaysReasoning(host, model) names the Chat upstreams that want
  reasoning back: a DeepSeek host, a model named deepseek wherever it is
  served (OpenCode Go's deepseek-v4.1-flash, OpenRouter), and Command
  Code's plugin. buildChat uses it as before.
- Passthrough (gateway.go, provider.Chat): for such an upstream, not
  Mistral (#494), withReasoningOnToolTurns gives every assistant
  tool-call message without reasoning_content one: its thinking from
  another field (reasoning, OpenRouter's) when it has some, else " ".
  The gate takes the member's model, so a group reaching a DeepSeek
  member is covered.
- Translated (buildChat, Responses/Anthropic -> Chat): a tool-call turn
  with no thinking gets " " too.
- Every tool turn, not only the last: DeepSeek also refuses an earlier
  tool turn of the current user turn missing it.
- " ", not "": DeepSeek's Chat takes "", but relays that omit empty
  fields drop it, and DeepSeek's Responses API refuses an empty
  reasoning_text. A vendor that refuses the field still gets the #494
  retry without it.

Checked against api.deepseek.com (owner's key, deepseek-flash, Chat):
last tool turn missing -> 400; "" -> 200; " " -> 200; real -> 200; an
earlier tool turn of the current user turn missing -> 400; a tool turn
before the latest user message missing -> 200; answers missing -> 200.

Not changed, found on the way: DeepSeek's Responses API refuses a
function_call with no reasoning item (and empty reasoning_text), and its
Anthropic API a tool_use with no thinking block; magpie relays those
natively as they come. OpenCode Go's deepseek-v4.1-flash could not be
tried for real: the owner's workspace answers "requires Global regions".

Run:
- New tests fail on the old code (TestChatToolTurnWithoutReasoningRelayed:
  400 "reasoning_content ... must be passed back"; TestTranslatedToolTurn
  WithoutReasoning: 400), pass now; TestChatToolTurnWithoutReasoning
  NotForMistral and the Mistral/MessageReasoning tests pass.
- gofmt -l clean; go vet -tags nogui ./internal/gateway on darwin,
  GOOS=linux, GOOS=windows.
- go test -tags nogui -count=1 ./... under a temp HOME: all ok.
- Real use: the built binary's gateway in a sandbox HOME with only the
  DeepSeek provider. Old: last tool turn missing 400, earlier one
  missing (stream) 400, plain answer 200. New: all three 200.
2026-10-10 14:14:47 +08:00
yetone 282c9feb23 agent: a packaged Claude Desktop gets magpie's gateway in the real %LOCALAPPDATA%\Claude-3p too, which 2.31226 reads, not only the package's (bg5eau on X)
bg5eau on X: Claude Desktop 2.31226.1, installed as an MSIX package on
Windows, never reached magpie. magpie wrote deploymentMode and its
configLibrary profile only into the package's
LocalCache\Local\Claude-3p, and that build reads the real
%LOCALAPPDATA%\Claude-3p. Copying configLibrary there and setting
deploymentMode 3p fixed it for them.

What the build does: 2.31226.1's asar (index.chunk-Cyt2WTko.js) resolves
the 3p userData with Ou() = path.join(LOCALAPPDATA, "Claude-3p"). 2.7032.0
has the same code (GY()). It reads deploymentMode from
Ou()/claude_desktop_config.json and the gateway from Ou()/configLibrary.
The path didn't change; the package stopped redirecting it into
LocalCache. An install's folders don't say which build it is, so:

- desktopdir.Sets returns every set of folders: Find's choice first, then
  the other one for an MSIX install. Everywhere lists every set's folders.
- Claude Desktop's switch writes and removes deploymentMode, the profile
  and _meta.json in each set. The package's set keeps its own stash keys
  (".package"). A stash that an older magpie wrote for the package's
  folders moves onto those keys, so turning it off still restores them.
- The check runs on each set, names the folder when there are two, and
  checks Claude-3p's deploymentMode as well.
- The siblings: the library's MCP servers go into every Claude-3p file
  that exists, and Cowork skills and Desktop sessions are read from every
  set.

Run:
- go test ./internal/{desktopdir,agent,library,sessions,usage}: green.
- The new tests (TestClaudeDesktopMSIX, TestClaudeDesktopMSIXStashBefore,
  TestClaudeDesktopMSIXBoth3p) and TestCallsCoworkMSIX are red against
  the old code. StashBefore also goes red with desktopAdoptStash disabled.
- gofmt; go vet for darwin, linux and windows.
- On the Windows box, Windows test binaries for the four packages:
  - desktopdir and library pass.
  - agent and sessions fail exactly the set that main's binaries fail
    there (#1388, and newly filed #1468).
  - Every Desktop test passes.
2026-10-10 14:13:05 +08:00
ggbdpq 74c48ac060 agent: the managed daemon's subcommand helpers are no stale Codex copy (#1461)
Codex keeps a long-lived helper next to the managed app-server (codex
app-server daemon pid-update-loop). It matches the codex startsWith
pattern, carries no --managed-daemon, sits under no app bundle or
Application Support folder, so codexCopyKind called it "cli": a helper
that started before magpie's last change to what Codex reads counted as
"A Codex in a terminal" forever, a daemon restart didn't end it, and the
only way to clear the row was killing a process the user can't name.

codexCopyKind now answers "helper" for an app-server daemon <sub>
command line, and StaleCopies skips it: it reads no list and there is no
Codex to quit. The daemon itself (--managed-daemon) is told apart
before, and a terminal Codex still counts.

Red/green: TestCodexDaemonHelperIsNoStaleCopy runs the helper alone from
before magpie's change — FAIL on the old code (one stale "cli" copy),
passes now; a terminal codex from before still counts 1.

Generated-by: GLM-5.3-Flash (ZCode)

Reviewed and landed by the maintainer (cherry-pick of b40a1dd0 from
ggbdpq:fix/codex-daemon-helper-1461, lychjck's report). Run on main at
c98160c0 with this commit on top, macOS arm64:
- On this Mac (ps, read only): the installed codex-cli 0.162.0 binary has
  the subcommand `app-server daemon pid-update-loop` ("[internal] Run the
  detached pid-backed standalone updater loop"): an updater, it reads no
  model list. The managed daemons running here are
  `codex app-server --listen unix:// --managed-daemon` and stay "daemon".
  No pid-update-loop helper was running on this Mac at review time, so the
  reporter's live row was not reproduced here; the fixture is the command
  line from the report. provider's isManagedDaemon already names the same
  helper as no daemon.
- TestCodexDaemonHelperIsNoStaleCopy with main's connectinfo.go: FAIL,
  "the daemon's helper counted a stale copy: [{Kind:cli ...}], want none";
  passes with the change.
- gofmt -l: clean. go vet -tags nogui ./internal/agent for darwin, linux
  and windows; GOOS=linux go vet -tags nogui ./... and GOOS=windows go vet
  ./...: clean. go build, GOOS=linux go build -tags nogui, GOOS=windows
  go build: ok.
- go test -tags nogui -count=1 ./... under a scratch HOME with GOPATH,
  GOMODCACHE and GOCACHE pinned: 44 packages ok, none failed.
- Not run: GUI tests (no GUI change), Windows or Linux hardware (the
  stale-copy check doesn't run on Windows; Linux uses the same ps path).
2026-10-10 14:07:55 +08:00
yetone c98160c045 gateway: a group's rule can pause one of its models at set hours, days or agents, leaving it out of the group then, not first, not a fallback, not kept by affinity (John on Discord)
John asked for a pause in a group's Rules, e.g. a model paused in its
vendor's peak hours. A time rule could only send those turns to another
member first: the paused-for model stayed behind it for failover, went
first when the other was unready, and was kept by affinity mid-turn.

What changed:
- provider: Rule.Pause (`"pause": true`, typed `pause=<model>`). It holds
  by time/days and agents only; cleanRules and ParseRule refuse any other
  condition, and refuse a pause with none. Pause rules are not send-to rules:
  MatchesBesidesIntent, ruledEntry and Ruled() leave them out.
  PausedOut(g, ms, agent, at) drops members a pause holds for, from the
  group's own rules and each nested group's (a group named = all its
  models; a model named = wherever a nested group has it, #625).
- gateway: serve and embeddings run PausedOut on ruleClock before planning,
  on every request. Every member paused -> 503 naming each rule
  (pausedError). The trace's GroupRef carries Paused.
- GUI: the rule editor's "send to" is now an app-menu action button (send
  to / pause); a pause hides the request conditions (clearing them),
  keeps hours, days and agents, and isn't saved without one. The group
  card's rule reads "→ pause <model>", and the Routing trace says which
  model was paused by which rule. Strings in zh, zh-TW, ja, de.
- CLI/TUI: `magpie group rule add <g> pause=<model> time=… days=…`.
- docs: gateway-routing.md step 2; start.html en/zh/ja "Hours and days".

Run:
- gofmt -l clean; go vet -tags nogui ./...; GOOS=linux and GOOS=windows
  go vet -tags nogui on gateway, provider, tui, root; go build ./...
- go test -tags nogui ./... under a temp HOME with pinned caches: all ok.
- rules_pause_test.go (3 tests) fails with the PausedOut call in serve
  removed (the paused model answers mid-turn, as a fallback, and the
  all-paused case returns 200). grouprule_pause_test.go covers clean,
  parse/Line, Ruled, Pauses and nested PausedOut.
- GUI: routing-pause.test.cjs in Chromium and WebKit, en/zh at 1100px
  and en/zh/zh-TW/ja/de at 420px: 14/14; all 7 Chromium cases fail on
  the old routing.js. routing-*, group-*, i18n-keys, gui-ja, gui-de,
  click-scroll: 618 pass, 4 fail under --test-concurrency=4
  (routing-blocked, -idle-frame, -side-calls, -wb-refused), all of which
  pass run alone; routing-side-calls failed 1 of 6 alone and passes
  with the old routing.js too, so it is a load flake not touched here.
2026-10-10 13:51:09 +08:00
yetone 4eb90b69f4 provider: after magpie switches Codex's account, Codex's accounts say the Codex app is still on the one before and to quit and reopen it (CavillZhang on X)
The Codex desktop app (ChatGPT.app 26.1007, bundled codex 0.162) reads
auth.json once, as its app-server starts. codex-rs AuthManager only
reloads it for the same account (reload_if_account_id_matches skips a
different one; a full reload is the app's own login/logout). So after
magpie's switch the running app keeps the spent account's token, its
webview reads /wham/usage with it (straight to chatgpt.com, which magpie
can't intercept), gets rate_limit.allowed=false, and hardBlocked stops
the composer in every thread with "You're out of Codex and Work usage".
Only quitting and reopening the app moves it.

magpie now notes, like the managed daemon, the app's own app-server
(…/CodexCLI.app/Contents/MacOS/codex … app-server, or the Windows Store
app's codex.exe) running at a switch and the account it holds, until that
process changes. Codex's accounts show "The Codex app is still signed in
as …; quit it and open it again" with Got it; `magpie accounts` and the
gateway log say the same.

Run: go test ./internal/provider ./internal/gui (TestCodexAppStale goes
red with the app not noted), the new GUI test in Chromium and WebKit, en
zh zh-TW ja de, 900 and 480px; findCodexApp against the owner's running
ChatGPT.app found its app-server; go vet darwin/linux(nogui)/windows,
go build.
2026-10-10 13:41:59 +08:00
yetone 61e296688e gateway: Codex's model list keeps codex-auto-review when the user picked models on the ChatGPT account, so auto-review runs on it again, not on the conversation's model at low (#1460)
10086gg reported that Codex's auto-review through magpie almost never
used codex-auto-review. It ran on the conversation's own model at low
effort, which costs more.

Cause: codexModels narrows the ChatGPT backend's list to the models the
user picked on the codex provider (CodexNativePicked). Hidden entries
(visibility "hide") never reach magpie's catalog (parseCodexModels), so
no one can pick them, and the narrowing always dropped them.
codex-auto-review is one of those hidden entries. Codex 0.162 picks the
reviewer this way (codex-rs/ext/guardian-reviewer/src/model.rs,
select_review_model):
1. the entry's auto_review_model_override;
2. else codex-auto-review, if the list has it;
3. else the conversation's model, at low effort.
So once the list was narrowed, every review fell to step 3. On a relay
model it was charged to the relay; on a ChatGPT model it ran on that
model. OpenCodex had the same bug: lidge-jun/opencodex#6712 recorded
direct ChatGPT login as codex-auto-review/low and their proxy as Sol/low.

Fix: the narrowing keeps hidden entries. No picker shows them, so the
user's picks still decide what Codex's /model lists. A user who wants
reviews on a relay model can still set Codex's auto-review model
(#938), and that setting still wins (step 1).

Which account pays changes for a user with picks whose conversation
runs on a relay model: auto-review moves from the relay to the ChatGPT
account's codex-auto-review. That is what Codex does without magpie.
OpenAI's help article "Using Codex with your ChatGPT plan" says:
"When you're signed in with a ChatGPT account, auto-review safety
checks are free and don't count toward your plan's usage limits."

New test TestCodexModelListPicksKeepHidden. It fails on the old code:
after the picks only gpt-6-sol is listed, with no codex-auto-review.
docs/subsystems/gateway-routing.md says this.

Run:
- gofmt -l internal/gateway
- go vet -tags nogui ./internal/gateway, on darwin, linux and windows
- go test -tags nogui ./... with a temp HOME and pinned caches: green
Not run: a real Codex signed in to ChatGPT through the gateway. The
reviewer choice was read from the Codex source.
2026-10-10 13:30:43 +08:00
yetone d7186e51b9 gateway: Gemini's thoughtSignature stays on the call and text it signed through translation, and goes back instead of the skip value (#1445, XYenon)
What changed: parseGemini keeps a functionCall's and a text part's
thoughtSignature. geminiParts and buildCodeAssistSent write them back; a
later call of a signed step goes without one, and only an unsigned step
or Claude on Antigravity gets skip_thought_signature_validator. The Code
Assist decoder puts a call's signature in its id (signedID, as chat does
since #687) and a text's in KTextSig; the Gemini encoder and renderer
give them to a Gemini client where Gemini put them, with a clean id.
Chat, Messages and Responses renderers leave a signature-only text part
out.

Why: a Gemini client on a Code Assist or translated path had every call's
signature replaced by the skip value and text signatures dropped.

Run: the new tests in reasoning_state_gemini_test.go (Gemini's
documented thought-signature shapes) fail on the old code and pass now; a
Chat client's call id carries the signature back to Google, and no other
API's client gets the text's. gofmt -l; go vet -tags nogui on darwin,
linux and windows; the full internal/gateway tests in a scratch HOME: ok.
Not tried with a real account.
2026-10-10 13:29:45 +08:00
yetone 13356b9da7 gateway: a Responses reasoning item goes through translation to Responses with its id, summary and encrypted_content, both ways (#1445, XYenon)
What changed: parseResponses keeps a reasoning item that has
encrypted_content raw (Part.Sealed), and buildResponses sends it back as it
came when it has no reasoning text and DeepSeek replay is off
(sealedReasoning). The decoder says where an item begins (KThinkStart with
its id) and hands over the done item (KSealed). The encoder streams the
summary under the upstream's own id and gives the done item and the
completed response's output as the upstream wrote them; renderResponses
does the same for a reply that wasn't streamed.

Why: the old code made its own rs_ id, dropped encrypted_content and
left reasoning out of the next request, so a store:false conversation
through a stream-only account or web search lost its reasoning each turn.
Reasoning with text in it (another vendor's) is still left out for
OpenAI (#1411); withoutBareReasoning and the foreign-reasoning retry still
act on the built body.

Run: the new tests in reasoning_state_responses_test.go (OpenAI's
documented item and stream shapes) fail on the old code and pass now;
the seal isn't handed to Messages, Chat or Gemini clients. gofmt -l; go
vet -tags nogui on darwin, linux and windows; the full internal/gateway
tests in a scratch HOME: ok. Not tried with a real account.
2026-10-10 13:29:45 +08:00
yetone 64179e7383 gateway: Anthropic's redacted_thinking and a thinking block signed with no text go through translation to Messages as they came, each block apart (#1445, XYenon)
What changed: the IR carries a vendor's sealed reasoning (Part.Sealed,
SealedBy) and says where a thinking block begins (KThinkStart) and where a
sealed one is (KSealed). parseAnthropic keeps redacted_thinking raw and
buildAnthropic writes it back raw. decodeAnthropic, the Anthropic encoder,
the collector and renderAnthropic keep each thinking block apart, with its
own signature, including one signed with no text (aBlock still writes
"thinking": "", 12c0550a). Claude's subscription (Claude Code's stream,
whole and streamed) emits the same events. A resumed reply suppresses the
new kinds with the rest of the reasoning.

Why: on a same-protocol translated path (a non-streaming request to a
stream-only account, web search on the agent's own API, a plugin,
failover), the old code dropped redacted_thinking, dropped a signature
that came with no text, and merged two adjacent thinking blocks into one
with both signatures concatenated. Anthropic refuses the next turn for
each of these.

Run: the new tests in reasoning_state_anthropic_test.go (fixtures are
Anthropic's documented block and stream shapes; no captured bytes exist)
fail on the old code and pass now. A sealed block isn't handed to a
Responses client. gofmt -l; go vet -tags nogui on darwin, GOOS=linux and
GOOS=windows; go test -tags nogui ./internal/gateway/ with a scratch HOME
and pinned GOMODCACHE/GOCACHE: ok. Not tried with a real account.
2026-10-10 13:29:45 +08:00
yetone 793af76242 gui: Codex's subagents setting, unset, says the lead picks the model and only falls back to the main model, not just "same as model" (willz on Discord)
willz set Codex's "subagents" to 同主模型 (same as model), and a subagent
still ran on another model. Codex 0.162 lets the spawn_agent call's own
`model` argument win over `[agents] default_subagent_model`
(core/src/agent/child_config.rs:
requested_model.or(agent_default_subagent_model)). The default only
applies to a spawn that names no model, and the spawn_agent tool lists
up to 5 models the lead may name. So "same as model" was false whenever
the lead named a model.

This was reproduced with the real codex-cli 0.162.0 in a sandbox
HOME/CODEX_HOME, against a fake Responses server on port 3683 that logs
each request's model and x-openai-subagent header. The lead ran on
gpt-6.1-sol in every case:
  a. lead names astra, default unset  -> subagent on gpt-6-astra
  b. lead names none,  default unset  -> subagent on gpt-6.1-sol
  c. lead names astra, default sol    -> subagent on gpt-6-astra
  d. lead names none,  default astra  -> subagent on gpt-6-astra

magpie already holds every subagent to one model with the "subagent
model" setting (the gateway's codexSubagentPick, 18958661). So this
relabels Codex's unset "subagents" field rather than changing it:
- The field reads "the lead's pick, else same as model".
- Its picker opens on "Lead's pick", noted "{model} unless the lead
  names another".
- Its title points to the subagent model as the way to make every
  subagent use one model.
The local row and WSL rows both get this. zh, zh-TW, ja and de are
translated. Claude Code's and omp's "subagents" keep "same as model".
Claude Code's case was not checked here: its Explore runs on the haiku
tier, so its label may not be exact either.

The new codex-subagents-lead.test.cjs fails on the old app.js ("subagents:
same as model"). It passes in Chromium and WebKit, in en, zh, zh-TW, ja
and de, at 1100px and 560px.

Run:
- gofmt -l internal/agent
- go vet -tags nogui ./internal/agent, on darwin, linux and windows
- go test -tags nogui ./... with a temp HOME and pinned caches: green
- GUI, in Chromium and WebKit: codex-subagents-lead, i18n-keys, gui-de,
  gui-ja, gui-zh-tw, click-scroll, codex-subagent-model, omp-roles,
  subagent-effort, profile-details and desktop-tiers: 54/54 in each
2026-10-10 11:58:51 +08:00
yetone 338ea97909 provider: moved Cursor users get plugin 0.2.5: an account with a Grok Bot allowance shows it on its Usage card, and a model asked for by its bare name counts in its sized ids' pool (magpie-community/plugins#64, Morganwan90; Will on Discord)
Cursor's Grok Bot has an allowance apart from Cursor's monthly usage that
only cursor.com's dashboard showed. Plugin 0.2.5 reads it from
cursor.com/api/dashboard/get-sand-usage-status (the sign-in sent as the
dashboard's WorkosCursorSessionToken cookie, as CodexBar does) and adds a
"Grok Bot" window, or "Grok Bot (trial)": aside, so a spent Bot never stops
Cursor requests, placed before Total so it is among the card's three rings.
A missing or failed reading leaves Cursor's own windows as they were.
0.2.4 (not shipped to moved users until now) counts a model asked for by
its bare name (grok-4.7) in the pool its sized ids (grok-4.7@256k) are in.

The cursor mover's min goes from 0.2.3 to 0.2.5, so keepMovedCurrent
updates every moved Cursor user.

0.2.5 on npm (registry.npmjs.org, latest) has an index.mjs byte-identical
to plugins main 6bed2c1. The plugin PR was reviewed at 40d4df6 and merged:
- bun test in packages/cursor: 73 pass, 0 fail; whole repo 1033 pass,
  0 fail; bun scripts/check.mjs all 15 pass
- botUsage broken 16 ways: 12 turn usage.test.mjs red; no timeout, no
  Origin, no res.ok check (the catch covers it) and an added bearer stay
  green
- the request and parsing match CodexBar's CursorSandUsage.swift
- not tried with a real Cursor account on the review side (the author saw
  the Bot ring on a Free account linked to SuperGrok)

go test -tags nogui ./internal/provider passes (temp HOME, pinned caches).
go vet -tags nogui ./internal/provider passes for darwin, linux and windows.
gofmt -l is clean. No test pins a mover's min, as for 0.2.3.
2026-10-10 11:52:49 +08:00
yetone 9bb2a467e4 agent: Snow CLI is an agent magpie sets up: a magpie profile in ~/.snow, made the active one, that Snow App takes in with "Sync Snow CLI API config" (#1457)
Snow CLI (npm snow-ai, MIT, MayDay-wpf/snow-cli) keeps one profile per
file under ~/.snow/profiles (or $SNOW_CONFIG_DIR), names the active one
in active-profile.json, and copies it over config.json at every start,
headless `snow --ask` included. Each profile is the whole snowcfg: base
URL, key, request method and models. Snow App (MayDay-wpf/snow-app)
reads ~/.snow/profiles/*.json when "Sync Snow CLI API config" is pressed.

magpie writes profiles/magpie.json from the profile the user was on
(their settings kept, Snow's OAuth sign-in left out): baseUrlMode base
at the gateway's /v1, requestMethod chat, the agent's key, the model as
advancedModel and basicModel, and the catalog's window and output
(200000 and 64000 when not known, the output cut to the window). It
makes that profile active and copies it to config.json. The profile the
user was on, whether there was an active-profile.json at all, and a
magpie.json of their own are stashed; Disconnect, `magpie snow
default`, or picking a model of one of the user's profiles puts those
back. A Snow from before profiles (config.json alone) gets the
default.json Snow itself would make, so its next start doesn't take
magpie's config for the user's. Check reports a base URL, key or request
method moved off magpie's; Sync rewrites the profile on a port move. A
WSL distro's ~/.snow is found and written the same way.

Registered: agents.go, the WSL kinds, SNOW_CONFIG_DIR in agentenv, the
npm update spec (snow-ai), the icon (Snow App's own, trimmed to 96px),
README, docs/reference.md, docs/subsystems/agent-wiring.md, the site's
agent list and start.html (en, zh, ja).

Run:
- go test -tags nogui ./... in a scratch HOME: 44 packages ok.
  New: TestSnow, TestSnowOwnProfile, TestSnowNoProfiles,
  TestSnowUsersMagpieProfile, TestSnowCheckSync, TestAgentSnowDefault;
  snow added to TestOwnProviderNamedAsMagpies, the WSL more-agents tests
  and TestMaxTokensWithinContextWindow. All fail before this commit
  (no agent "snow").
- go vet -tags nogui for darwin, linux and windows; gofmt -l clean.
- Real Snow CLI 0.9.2 in a sandbox HOME against a sandbox magpie
  (`magpie serve` on 127.0.0.1:3571, a fake OpenAI upstream on 3561):
  `magpie snow fake/pro` made the magpie profile active, `snow --ask`
  answered through the gateway (POST /v1/chat/completions, model pro,
  logged as agent snow), and `magpie snow default` left profiles,
  active-profile.json and config.json byte-identical to before.
- GUI: agent-layout, agent-connect, agent-models-picker,
  agent-field-names and models-add-and-icons in Chromium and WebKit;
  a Snow CLI row checked at 520-960px in en, zh, zh-TW, ja and de in
  both engines. No new GUI strings.
2026-10-10 11:46:44 +08:00
yetone 43cb6fe34e agent: Claude Code is told the window of Claude Desktop's models while Desktop is on magpie, so its Code tab no longer shows GPT 272K and Grok 256K as 200K (#1458)
Claude Desktop's Code tab runs Claude Code on ~/.claude/settings.json
and hands it the id Desktop lists a model by (mythos-magpie-<n>,
anthropic/magpie-<n>), with nothing of the model's window. Claude Code
2.1.293 (the one Desktop 2.31226.1 ships) gives an id it doesn't know
200000 unless the id has [1m] or CLAUDE_CODE_MAX_CONTEXT_TOKENS is set:
its catalog rows come from Anthropic's served catalog alone, and gateway
discovery keeps only id, name and description. Desktop sets that env
only for Cowork (yukon-silver-config contextWindowByModel), never for a
Code session, and reads the gateway's max_input_tokens only to add a
1M entry. magpie wrote the env from Claude Code's own main model alone,
and not at all on a [1m] or Claude main model, so GPT-6-Astra and Grok
4.7 ran as 200K. GLM 5.3 Flash is 1M, listed as [1m], so it ran at 1M
and showed magpie's auto-compact window, 300K.

While Claude Desktop is on magpie, the window written is now the least
of Claude Code's own model's and of the models Desktop is shown, leaving
out one listed by its 1M id (1M whatever the env says) and one named as a
Claude model Claude Code knows. Claude Code takes one value for every
model it doesn't know, so it is the least: never more than any of them
takes (an overflow is still turned into "prompt is too long" by the
gateway). A value the user set is still left alone. Sync writes it too,
so a model added or hidden for Desktop moves it, and turning
DesktopLongest on or off tells the agents again (catalog.Touched).
gateway.DesktopListed1M is the one rule desktopModels lists [1m] by, so
bb3b3f14's 1M label is unchanged.

Standalone Claude Code has the same limit: a magpie model picked in
/model runs on the one value; that is not changed beyond the Desktop
case.

Run:
- TestClaudeDesktopContextWindow, TestClaudeDesktopContextWindow1M fail
  on the old claude.go/desktop.go ("" want 256000; "" want 1000000), and
  each exclusion broken by hand turns them red.
- Real Claude Code 2.1.293 (from Desktop 2.31226.1's download), sandbox
  HOME/CLAUDE_CONFIG_DIR, against a fake gateway on 3611, with the
  settings.json magpie's agent code wrote (GPT 272K, Grok 256K, GLM 1M
  under DesktopLongest, main on GLM [1m]): /context shows 256k for
  GPT's and Grok's mythos-magpie ids (200k before), 272k for GLM's
  [1m] id (the compact window).
- gofmt -l; go vet, GOOS=linux and GOOS=windows go vet -tags nogui on
  internal/agent, internal/gateway, internal/gui; go test -tags nogui
  ./... under a temp HOME.
Not run: the Claude Desktop GUI itself.
2026-10-10 11:42:03 +08:00
yetone d7a1b02032 redact: more kinds of personal data, each with its own switch: SSNs, passports, IBANs, birthdays, MAC addresses and serial numbers on by default; home-folder names, user@host, public IPs and cloud buckets off (lc on Discord)
lc asked in #feedback (message 1558300896142696549) for magpie's masking
to cover more of what identifies a person or a machine. Under Settings >
Privacy > Mask personal data, each kind now has its own Off/On
(settings.redactKinds, redact.Options.Kinds). What each one masks:

- US SSNs: dashed numbers, except ones never issued (000, 666 and 9xx
  areas) and well-known examples. Undashed ones only next to "SSN" or
  "social security".
- Passport numbers, next to passport, Reisepass, 护照 or パスポート.
- IBANs: compact or in groups of four, with the country's length and the
  mod-97 check.
- Birthdays: a date only next to birthday, DOB, born, 生日 and the like.
  A date on its own stays.
- MAC addresses, but not documentation examples or all-decimal groups.
- Serial numbers, next to S/N, serial or 序列号.
- The user name in /Users/x, /home/x and C:\Users\x. The name is masked
  and the path is kept.
- User and host in a shell prompt or an ssh/scp/rsync target (user@host
  goes out as two placeholders, not as an email address), and on
  hostname/user lines whose value ends the line. Code such as
  `hostname = socket.gethostname()` is left alone.
- Public IPv4 and IPv6 addresses. Private, loopback, link-local, CGNAT,
  documentation, multicast and well-known resolver addresses stay, and so
  do version numbers.
- Cloud bucket names in s3://, gs://, oss://, cos://, abfss:// and
  storage URLs. The object path is kept.

The last four are off by default. Agents send their working folder, the
prompt and log lines in nearly every request, and the model then has to
copy the placeholder exactly inside a path or URL. The first six are
rare and sensitive, so they are on. Each placeholder is restored in
replies, in streamed text and in tool-call JSON. Context rules run only
in a window around their words (rule.matches), so masking with the
defaults on 400KB of app.js takes 136ms (112ms before; 222ms with every
kind on).

Tests, run at this head:
- gofmt -l is clean. go vet passes on darwin, GOOS=linux and
  GOOS=windows (-tags nogui) for internal/redact, gateway, gui and
  settings.
- go test -tags nogui ./... under a temp HOME with pinned caches passed
  everywhere except internal/proc's TestNoCommandBypassesProc. That
  failure came from 2da8fbe4, and b934d64a fixed it on main. After the
  rebase, the changed packages and internal/proc were run again and
  passed. New tests:
  - internal/redact TestMoreKinds, TestMoreKindsLeaveAlone (versions,
    timestamps, hashes, UUIDs, model ids, code, ordinary dates, URLs in
    code), TestCategoryDefaults, TestMoreKindsKnown, TestMoreKindsJSON.
  - internal/gateway TestRedactedKinds.
  - internal/gui TestRedactKindsAreRedacts.
- I broke the IBAN checksum by hand and TestMoreKindsLeaveAlone went
  red.
- GUI tests pass in Chromium and WebKit:
  - the new redact-kinds.test.cjs (en, zh, zh-TW, ja and de, at 1100
    and 640 px);
  - i18n-keys, gui-de, gui-ja, gui-zh-tw, redact-rule-row,
    privacy-hides-accounts, privacy-usage-stats, settings-groups,
    settings-sections and mobile-web;
  - 29 more files that open the Privacy page (252 tests).
  privacy-usage-stats now scrolls with the wheel to reach Count me as a
  user, which the new rows push below its 760px window. WebKit doesn't
  let a script scroll Settings.
- Real gateway check: a `magpie serve` built from this tree, under a
  sandbox HOME, on 127.0.0.1:3492, with a fake OpenAI upstream on 3491.
  An Anthropic /v1/messages request went to the upstream with each kind
  as a placeholder. Turning SSNs off and home folders on changed what
  was sent the same way. The streamed reply came back with every value
  restored.
2026-10-10 11:29:03 +08:00
yetone 5b008f5a55 i18n: Chinese says 子代理 for subagent everywhere, not 子 agent in some places and 子代理 in others (willz on Discord)
willz asked what the difference between 子 agent and 子代理 is. There is
none: the zh and zh-TW GUI strings used 子 agent for the Agents page's
subagent row and subagent effort ("Subagents", "subagents",
"subagent effort" and its hint), and 子代理 for every other subagent
string (Codex subagents, routing's sealed subagent task, subagent model).
Both locales now say 子代理 throughout; zh-TW already used 子代理 for the
rest, so it takes the same term. The zh site docs (intent.html,
start.html) said 子 Agent / 主 Agent and now say 子代理 / 主代理, matching
the GUI's 主代理. ja already says サブエージェント everywhere; the READMEs
don't name subagents. The four GUI tests that expected 子 agent are
updated.

Run: omp-roles, profile-details, subagent-effort, tier-effort,
codex-subagent-model, routing-sealed-task, agent-tweak-fold,
claude-signin, codex-memories(-settings), gateway-mode, mobile-web,
usage-ledger-detail, gui-zh-tw, gui-ja, gui-de and i18n-keys in Chromium
(148/148) and WebKit (79/79); subagent-effort also at 420px wide in both
engines, with the zh screenshot checked (子代理推理强度).
2026-10-10 11:24:31 +08:00
yetone 9db2ed3dc6 gateway: temperature, top_p and top_k are left out for the Claudes that refuse them, so Haiku 5.5 no longer answers auto mode's classifier "temperature is deprecated for this model" (#1454)
Claude Code's auto mode classifier asks at temperature 0, and magpie's
own classifiers do too. Through the aihubmix relay, claude-haiku-5-5
answered 400 "`temperature` is deprecated for this model." (and the same
for `top_p` and `top_k`). magpie relayed sampling parameters as they
came, and built them from a chat request's temperature or top_p.

samplingAsTaken, beside thinkingOffAsTaken in forwardOnce, so relayed and
built requests and retries all go through it, leaves the three out for
the models Anthropic documents as refusing them:
- Opus 4.7, 4.8, 5 and 5.5, Fable 5 and Mythos 5 refuse them at any
  value.
- Sonnet 5 and 5.5 refuse anything but the default.
- Haiku 5.5 takes only temperature 1 and top_p 0.99, never both, and no
  top_k.
Left out, each model samples at its default. The model is read as the
vendor names it, from the body or, for Bedrock and Vertex, the path.
Older Claudes (Haiku 4.5, Sonnet 4.6, Opus 4.6 and before), other
vendors' models and later lines go byte for byte as they came. A Claude
under a name these rules can't read, answering that a sampling parameter
is deprecated or not supported (samplingDeprecated), is asked again
without them, once.

aihubmix itself took every value for the other models and refused it
only on Haiku 5.5. The other models are left out by the docs' rule, for
direct keys and relays that pass the vendor's 400 on.

Verified:
- Tests:
  - TestSamplingAsTheModelTakesIt covers each refusing model's
    spellings, a Bedrock path, and byte-identical bodies for Haiku 4.5,
    Sonnet 4.6/4.5, Opus 4.6/4.1, Claude 3.7, Sonnet/Haiku 6, deepseek,
    glm, kimi and gpt.
  - TestHaikuFiveFiveSamplingOnTheWire uses a fake vendor answering
    with the relay's literal 400s. It covers the classifier relayed,
    top_p/top_k, magpie's own classifier built from chat, Haiku 4.5
    keeping temperature 0, and an unreadable name retried once.
- Without the forwardOnce call, three wire cases FAIL. Without the
  retry, the unreadable-name case FAILs.
- Live, through this build's `magpie serve` under a sandbox HOME with
  the aihubmix key, in the classifier's shape (temperature 0, stop
  </block>, thinking disabled), all 200:
  - claude-haiku-5-5 at default and max effort and with thinking
    omitted;
  - claude-haiku-4-5, claude-sonnet-5-5, claude-opus-5-5 and
    claude-sonnet-5;
  - chat to claude-haiku-5-5 at temperature 0 and at top_p 0.5.
  Straight to aihubmix, Haiku 5.5 at temperature 0, top_p 0.5 or
  top_k 5 gives the 400.
- gofmt -l, GOOS=linux and GOOS=windows go vet -tags nogui
  ./internal/gateway, and go test -tags nogui ./internal/gateway under a
  temp HOME with pinned caches.
2026-10-10 11:13:42 +08:00
yetone 4a3d7c6f04 gateway: an agent's thinking between_tools, Sonnet 5.5's thinking off, is read as thinking off on a model magpie translates for (#1454)
Sonnet 5.5 takes thinking turned off as {"type": "between_tools"}.
parseAnthropic read only "disabled" as off, so an agent that already
sends between_tools, routed to a model on another API (a group's
member, a fallback), had thinking neither off nor on, and was left to
that model's default. It is now read as ThinkOff, the same as disabled.

Verified: TestBetweenToolsReadAsThinkingOff fails without the change
("between_tools: ThinkOff = false"). gofmt -l, GOOS=linux and
GOOS=windows go vet -tags nogui ./internal/gateway, and go test -tags
nogui ./internal/gateway ./internal/agent all pass under a temp HOME
with pinned caches.
2026-10-10 11:02:38 +08:00
yetone a9cb0ef988 gateway: Claude Sonnet 5.5 is sent thinking between_tools in place of disabled, and Opus 5.5, Fable and Mythos no thinking, so Claude Code's auto mode classifier no longer gets a 400 (#1454)
911Liam's auto mode, on claude-sonnet-5.5 through a relay, got 400 "To
turn thinking off on this model, send "thinking": {"type":
"between_tools"} instead of {"type": "disabled"}". Claude Code turns
thinking off for its classifier and side queries unless it knows the
model refuses that, and a model reached through a gateway can fall
outside what it knows. magpie forwarded the disabled as it came, and
turns thinking off itself too (an effort of none fitted, a budget that
doesn't fit, thinkingOffUnlessAsked for other vendors' models).

thinkingOffAsTaken, in forwardOnce next to adaptiveThinking, so that
relayed requests, count_tokens, magpie's own thinking-off and retries
all go through it, follows Anthropic's rules for each model. It reads
the model's name as the vendor gives it, from the body, or from the path
for Bedrock and Vertex.
- Sonnet 5.5: between_tools with no other field, and xhigh or max asked
  as high (between_tools is a 400 above high).
- Opus 5.5, Fable 5.x and Mythos 5.x refuse disabled at every effort.
  Thinking is left out, effort is low unless one was asked, and
  classifierRoom is added to a reply limit below it, as Claude Code
  itself does for these models.
- Opus 5 and Haiku 5.5: disabled is kept, with xhigh or max asked as
  high.
- between_tools sent to any other Claude goes as disabled, or with no
  thinking where disabled is refused too.
Other vendors' models, Sonnet 5, Opus 4.8 and older, and lines later than
these go byte for byte as they came. A Sonnet 5.5 under a name these
rules can't read is asked again with between_tools, once, on the 400
that asks for it (asksBetweenTools). A refusal of disabled at any effort
(thinkingOffRefused) is asked again with thinking left to the model,
beside alwaysThinks.

TestAdaptiveOnlyModelsNeverGetABudget's "disabled stays" case pinned
disabled for claude-opus-5.5, which the vendor refuses. It now checks an
older Claude keeps disabled and Opus 5.5 is sent none.

Verified:
- Tests:
  - TestThinkingOffAsTheModelTakesIt and
    TestThinkingOffLeavesOtherModelsAlone (deepseek, glm-5.3, kimi,
    Sonnet 5, Opus 5 at high, Haiku 5.5, Opus 4.8 at max, Sonnet 4.6,
    Haiku 4.5, Claude 3.7, dated Sonnet 5 and Sonnet 6 unchanged).
  - TestClaudeFiveFiveThinkingOffOnTheWire: a fake vendor answers with
    the reporter's literal 400, covering the classifier body,
    count_tokens and the retry.
- Without the forwardOnce call, the wire test's Sonnet 5.5, Opus 5.5 and
  count_tokens cases and the Opus 5.5 adaptive case FAIL. Without the
  retry, its unreadable-name case FAILs.
- Live, through this build's `magpie serve` under a sandbox HOME with
  the aihubmix relay key, in the classifier's shape (max_tokens 64,
  temperature 0, stop </block>, thinking disabled):
  - claude-sonnet-5-5 answers 200 at default and xhigh effort (sent
    straight to aihubmix with disabled it gives the reporter's 400);
  - count_tokens 200;
  - claude-opus-5-5 200;
  - claude-haiku-5-5 at max 200;
  - claude-sonnet-5 200.
- gofmt -l, GOOS=linux and GOOS=windows go vet -tags nogui
  ./internal/gateway, and go test -tags nogui ./internal/gateway under a
  temp HOME with pinned caches.
2026-10-10 11:02:38 +08:00
yetone 16539c2ea9 imagemcp: generate_video and the Magpie Image card name every video model the gateway makes videos with, not a Grok subscription alone (#1399)
shen-joe (#1399) wants images and videos from magpie in PI-Desktop and
WorkBuddy. Neither app sends video generation to a custom provider: PI-Desktop
has no video generation at all, and WorkBuddy's VideoGen/ImageGen post to
copilot.tencent.com's /v2/videos|images/generations with the account token.
So the way those agents get videos is Library → MCP → Magpie Image
(`magpie mcp image`). Its generate_video tool told the agent it works only
"with a Grok subscription signed in to Magpie (it has no other video model
yet)", and the Library card said the same, while gateway.Videomakers also
makes videos with a preset plan's videos (Volcengine Ark Agent Plan's
Seedance), another magpie, and a custom provider whose model list marks video
models (#1446). An agent reading that can tell a Seedance user they need Grok.

- imagemcp.go: generate_video's description and the package doc name each
  kind of video model.
- library/market.go: the Magpie Image card says "a video model set up in
  Magpie (Grok Imagine, Seedance)"; zh, zh-TW, ja and de follow.
- Tests: TestVideoToolSaysEveryVideoModel and TestMagpieImageSaysVideoModels
  fail on the old text and pass now.

Run: go test -tags nogui ./internal/imagemcp ./internal/library ./internal/gui
./internal/gateway (ok) and ./... (only TestNoCommandBypassesProc red, from
2da8fbe4, fixed on main by b934d64a); gofmt -l; GOOS=linux and GOOS=windows
go vet of imagemcp and library. A built magpie's `mcp image` answered
tools/list with the new description. A sandbox gateway (scratch HOME,
127.0.0.1:3591, a real OpenRouter key) answered PI-Desktop's exact request,
POST /v1/images/generations {"model":"openrouter/google/gemini-2.5-flash-image",
"prompt":…,"n":1}, with 200 and data[0].b64_json, a PNG.
2026-10-10 10:58:31 +08:00
yetone f2611b7154 accounts: import ChatGPT accounts from Cockpit Tools' and Sub2API's exports, several files at once, from the window or magpie accounts import; the box says the source tool is signed out (#1453)
wlj521 asked for cockpit-tools' import: several auth.json files at once.
magpie's window already imported Codex's auth.json and CLIProxyAPI's
files; this brings it the rest of what Cockpit Tools exports and a command
for it.

What changed:
- login_import.go reads Sub2API's export ({accounts: [{platform, type,
  credentials}]}), so every Cockpit Tools export format comes in (its own,
  auth.json, CPA, Sub2API). An API key, another vendor's account and an
  entry with only an access token (a web session: it can't be renewed and
  would stop within days) are refused with the reason.
- ImportLogins and ImportGoogleAccounts no longer skip one file of several
  that holds no account: it is a failed row with its place
  (ImportedAccount.File), which the window and the command name by file.
- `magpie accounts import <codex|claude|antigravity|factory> <file>...
  [--yes]`: says first what importing does to the tool the file came
  from, imports nothing without a yes, only reads the files, and lists
  each account's outcome.
- The window's import box names Cockpit Tools and Sub2API and says
  plainly that the source tool is signed out of the account and the file
  is only read. Claude's box no longer claims a refresh it doesn't do.
  zh, zh-TW, ja and de for every new string; the retired strings removed.
- docs: reference.md and providers-accounts.md describe the import.

Decision: keep the refresh on import. It checks the file and makes magpie
the token's one holder at a moment the user chose. Storing the token
as-is, as Cockpit Tools does, only delays the rotation conflict and makes
which side gets signed out a race.

Run:
- go test -tags nogui ./... under a sandbox HOME (pinned caches)
- TestImportCockpitExports red on the old login_import.go (Sub2API rows
  unread, the garbage file passed over); TestAccountsImport red with the
  confirmation removed by hand; login-import.test.cjs red on the old app.js
- node --test login-import, factory-key, i18n-keys, gui-ja, gui-de,
  gui-zh-tw (Chromium and WebKit; en and zh, results at 380px)
- gofmt -l; go vet, GOOS=linux and GOOS=windows go vet -tags nogui on .
  and ./internal/provider; darwin, linux and windows builds
- no live ~/.codex: fixtures and a fake token endpoint only
2026-10-10 10:56:55 +08:00
yetone 99009b9333 provider: a plugin's model pool counts a bare id with its @sizes, so a routing group's cursor/grok-4.7 is weighed on Cursor Models, not a used-up Other Models (Will on Discord)
Will's routing group keeps cursor/grok-4.7. Cursor's plugin (0.2.2/0.2.3)
lists Grok 4.7 only at its context sizes, grok-4.7@256k and
grok-4.7@500k, and its usage() builds the Cursor Models window's models
and Other Models' notModels from those ids. quotaOfPlugin matched a model
against those lists by the whole string, so bare grok-4.7 (which the
plugin still runs, at its default size) fell into Other Models: used up,
so the Routing page showed it 0% left and weighRouted ranked it last,
while composer-2.5 showed ~84%.

The window's lists are now an idSet: a bare id is in it when one of its
id@size is, and an id@size when its bare id is. Only the trailing @size
is taken off, so distinct models stay apart: another @size of a model,
another family member (grok-4), and other suffixes (ZCode's -Trial) still
match by the whole id. Every route that asks a plugin window whether it
counts a model goes through w.matches -> Limit.applies (routing's For,
Pace, Count, Renewal, fallback's Pooled, CapHeld, QuotaWindow.Counts for
the usage page's holds), so the one change reaches them all. host.js's
contract comment says so for plugin authors.

The plugin's own lists are also fixed in magpie-community/plugins (cursor
0.2.4 adds each listed id's bare name to its pool), for magpie versions
before this one.

Verified:
- TestPluginPoolCountsBareIDOfSizes (the reporter's literal ids) fails on
  the old code: "grok-4.7: used 100, want 16", "the usage page's holds
  count grok-4.7 in Other Models", "grok-4.8@1m: used 100, want 16";
  passes with the change.
- gofmt -l clean; go vet, GOOS=linux and GOOS=windows go vet -tags nogui
  on internal/provider and internal/plugin; go build for darwin, linux,
  windows.
- go test -tags nogui ./... green but TestNoCommandBypassesProc, red on
  main since 2da8fbe4 and fixed by b934d64a, which this is rebased on.
Not verified: against Will's real Cursor account.
2026-10-10 10:54:22 +08:00
yetone b934d64a4e tests: TestNoCommandBypassesProc leaves out internal/testenv, which only tests import and which can't import proc (its tests import testenv); main was red since 2da8fbe4's testenv.Bun ran bun with os/exec 2026-10-10 10:51:48 +08:00
yetone 54675767f1 sessions: a folder draws its latest 100 sessions and a Show more for the next page, and opening a session draws only that session again (ChildhoodAndy on X)
With thousands of sessions the Sessions page drew every row of the open
folder, 17,000 elements for 1,000 sessions, and drew them all again on
switching to the page or to another agent, on each keystroke in the filter
(every folder opens while filtering: 51,000 elements for 3,000) and on
opening a session. Switching and scrolling stuttered.

Now an open folder draws a page of 100 and a "Show N more" (data-unrolls)
that adds the next page under the rows already drawn. A filter or another
agent starts again at a page. A session opened from the Usage page is drawn
however far down its folder it is. Opening a session replaces only that
item and the one it closes. The folder box and the bar still pick every
session, drawn or not.

Measured with 3,000/2,500 sessions, before → after:
- open the page: WebKit 449 → 159 ms, Chromium 312 → 165 ms
- open a session: WebKit 287 → 67 ms
- filter keystroke: WebKit 546 → 64 ms
- elements drawn: 17,073 → 1,774

Run: sessions-manage-paging.test.cjs (new; Chromium + WebKit, en + zh,
1000px + 380px) and every sessions-*/session-* test, click-scroll and
old-webkit, all green.
2026-10-10 10:50:09 +08:00
yetone e5f04f38e8 plugin: a plugin on a pre-release whose release is out shows the update on its row, as the Plugins dot already did; versions are ordered as semver orders pre-releases, in Go and in the page
The Plugins dot comes from update.Newer, and the Installed rows (chip,
Update, Update all) from plugins.js's own newer(). They disagreed:

- newer() read only x.y.z, so 2.0.0-beta.1 with 2.0.0 out had the dot
  and no row to update it from. Real case: opencode-antigravity-auth,
  whose latest is 1.6.0 after 1.6.0-beta.0..2.
- update.Newer compared pre-releases as strings, so 1.0.0-beta.10 came
  before 1.0.0-beta.9 (and 1.6.0-10 before 1.6.0-9): a missed update,
  and the other way round, an "update" to an older beta.
- update.Newer didn't parse build metadata, so 1.0.1+build.1 was never
  newer than anything.

Both now order as semver §11: no pre-release comes after any;
identifiers compared one by one, numbers as numbers, a number before a
word, and a shorter list before a longer one it starts. Build metadata
is ignored. A version that isn't x.y.z is never newer, as before.

Tests:
- internal/update TestNewer gains the pairs. On the old Newer 4 fail
  (beta.10/beta.9 both ways, 1.6.0-10/-9, +build).
- internal/gui/tests/plugin-prerelease.test.cjs: seven installed
  plugins, the dot's waiting list as update.Newer gives it, and each
  row's chip and Update button, plus the Update all count, must match.
  On the old plugins.js it fails at "v2.0.0 out" in both engines. No
  strings change, so it's English only.

Reproduced in a sandbox HOME with the real package:
`magpie plugin add opencode-antigravity-auth@1.6.0-beta.2`, then Check
for updates. Before: /api/plugins/updates listed it, and the Plugins
nav had the dot, but the row had no chip and no Update, and there was no
Update all. After: the row shows "v1.6.0 out", Update, and Update all (1).
Update all then installed 1.6.0 and the waiting list emptied.

Run: gofmt; go vet -tags nogui for darwin, linux and windows on
internal/update, internal/plugin, internal/gui and the root package; go
test -tags nogui of the root package, internal/{gui,plugin,provider,
stats,update} under a temp HOME; GUI tests plugin-prerelease,
plugin-updates, plugin-card-update, plugin-check-updates, plugin-market,
plugin-release, plugin-git and plugin-local in Chromium and WebKit.
2026-10-10 10:28:49 +08:00
yetone 2da8fbe488 tests: every test that runs plugins on the bun on PATH runs it once first, through testenv.Bun, so a bun that has never run here doesn't time out internal/provider's and internal/plugin's plugin tests (#1306)
65dcc573 (ggbdpq's patch 1) warmed the bun on PATH before the root
package's one-minute plugin contexts. The same lookup of the bun on PATH
is in 40 more places in internal/provider (28), internal/gateway (8),
internal/plugin (3) and internal/gui (1), and in the root package's
TestPluginListSaysClash. Many of them then open a one- or two-minute
context (internal/provider's sign-in, fetch and move tests,
internal/plugin's sandbox), and there macOS's first-exec check of a
just-installed or upgraded bun landed inside the deadline. The rest
are covered the same way, so no test has to remember which is which.

testenv.Bun now does what the root package's pathBun did: it finds the
bun on PATH (skipping the test when there is none) and runs
`bun --version` before the test opens its context, once per bun per test
binary. pathBun is gone; every `exec.LookPath("bun")` + skip block in
the tests uses testenv.Bun instead. Three tests reach for the bun some
other way and are left as they were: TestHostRunsAsMagpieBun (7ddea2c2
covers its copy), the plugin_git test's realBun and netproxy's
TestBunThroughBridge, none of which runs the plugin host under a
deadline on the PATH bun. migrate_test.go needed two `err` declarations
that the removed block used to provide.

What was run (macOS arm64, bun 1.4.2), with a wrapper bun first on PATH
whose first exec sleeps 61s (marker-gated, then execs the real bun):
- Red, old code:
  - internal/provider TestPluginSignInTakesAPastedCallback: FAIL at
    61.1s, "pluginsignin_paste_test.go:36: starting plugins: context
    deadline exceeded".
  - internal/plugin TestFakePlugin: FAIL at 62.0s, "plugin_test.go:66:
    Plugins = [], starting plugins: context deadline exceeded".
  - internal/gateway TestPluginFailover passed on the old code too (its
    calls have no deadline that short), so the gateway is not shown red.
- Green, this change, same cold wrapper: the three tests passed (61.1s,
  62.9s, 61.2s).
- go test -tags nogui -count=1 for the root package and for
  internal/testenv, gateway, gui, plugin and provider, under a temp HOME
  with GOMODCACHE, GOCACHE and GOPATH pinned: all ok.
- gofmt -l on the changed files: clean. go vet of those packages for
  darwin (-tags nogui), GOOS=linux and GOOS=windows (-tags nogui), and
  GOOS=windows without the tag: clean.

Not run: a really cold bun on a loaded machine; the sleep stands in for
the check. Not run on Linux or Windows, where the warm-up is one extra
`bun --version` per test binary.
2026-10-10 10:22:59 +08:00
yetone ba0e2a8aa9 plugin: Update all and magpie plugin update bring a plugin pinned to an older version to npm's newest, so the update prompt clears (ARNO on Discord)
ARNO: 非官方插件安装后就算更新到最新版,magpie依然提示有更新. A third-party
plugin added as name@1.2.3 is pinned. The Plugins page still counts it
under "Update all (n)", its row says "vX out" and the Plugins dot is on.
Update all (and `magpie plugin update`) reinstalled the pinned version,
reported "Plugins updated", and the prompt stayed. The row's own Update
already installs the newest version and unpins the spec (name@latest).

Update now does the same for a pinned plugin when npm has a newer version
than the one installed: it installs that version through add, which keeps
the plugin's options and the provider picked for it. A pinned plugin that
is already at npm's newest keeps its pin. Git plugins, folders and plugins
npm didn't answer for behave as before. The plugin docs (en, zh, ja) now
say that updating a pinned plugin yourself unpins it.

Reproduced in a sandbox HOME with the real opencode-gemini-auth from npm
(magpie web on a spare port, China mirror switch on and off). Pinned at
1.4.15 with 2.0.1 out: before the fix, Update all and `magpie plugin
update` left it at 1.4.15 and /api/plugins/updates still listed it. After
the fix it is at 2.0.1 as opencode-gemini-auth@latest, the waiting list
is empty and Check for updates reports "current". The row's Update
(upgrade) path was checked too: unpinned opencode-gemini-auth, and scoped
@gitlab/opencode-gitlab-auth 1.3.1 -> 1.3.3 with the mirror on. Both
cleared the prompt before the fix as well.

TestUpdateAllUpdatesAPinnedPlugin (real bun, fake registry) fails without
the change: "Update all left opencode-someones-auth at 1.0.0, want 2.0.0".
gofmt -l, go vet (darwin, GOOS=linux, GOOS=windows) on internal/plugin,
and go test -tags nogui of internal/plugin, internal/gui and
internal/provider under a temp HOME with pinned caches all pass.
2026-10-10 10:19:42 +08:00
yetone 47a313f618 library: Pi's MCP servers go to ~/.pi/agent/mcp.json, the file Pi 0.99+ loads, unless pi-mcp-adapter is really loaded; an adapter left installed but not listed no longer sends them to mcp-adapter.json (sydney on Discord)
sydney reported that the Library wrote Pi's MCP servers to
~/.pi/agent/mcp-adapter.json while Pi 1.0 loads ~/.pi/agent/mcp.json.

Pi's own source (npm @earendil-works/pi-coding-agent):
- 0.99.0 to 1.1.0 have built-in MCP: dist/extensions/mcp/config.js
  loadMcpConfig reads <agentDir>/mcp.json (and a trusted project's
  .pi/mcp.json). An extension registering /mcp (pi-mcp-adapter) replaces
  it; "extensions": ["-builtin:mcp"] turns it off.
- 0.87.1 and earlier have no MCP; pi-mcp-adapter 3.x/4.x reads
  mcp-adapter.json only, adapter 5.0.0+ also reads Pi's mcp.json on a Pi
  with registerMcpServer.
- core/package-manager.js resolve() loads only settings.json "packages",
  "extensions" entries and <agentDir>/extensions/. npm/node_modules, git/
  and the global npm root are install places, not loaded on their own.

magpie counted any pi-mcp-adapter folder (npm/node_modules, git, global
root) as loaded, so an adapter left behind with "packages": [] made it
write mcp-adapter.json, which nothing reads, and move mcp.json away
(heliar-k's #1097 is the same case). It also fell back to the adapter file
when Pi's version was unknown.

Now:
- piDetect follows Pi's loader: a package is loaded only when listed (an
  object entry with "extensions": [] loads none), "-"/"!" entries turn one
  off, and <agentDir>/extensions/<name> loads; the other folders only give
  the version.
- An unknown Pi version is taken as one with built-in MCP.
- With no adapter loaded, servers in mcp-adapter.json are moved into
  mcp.json (both backed up first); nothing in either file is dropped.
- With adapter 5+ loaded on a Pi with built-in MCP, mcp.json is left
  where it is (the adapter reads it) and new servers go to the adapter's
  file; with adapter 3/4, or on Pi before 0.99, the old move is kept.

Tests: TestPiMCPUnloadedAdapter, TestPiDetectLoaded and
TestPiMCPAdapter5KeepsPiFile fail on the old pimcp.go and pass now;
older tests that relied on an unlisted adapter now list it.

Run: gofmt -l; GOOS=linux and GOOS=windows go vet -tags nogui
./internal/library/; go test -tags nogui ./internal/library/
./internal/agent/ under a temp HOME. Real Pi in a sandbox HOME:
- Pi 1.0.4, reporter's layout (adapter 5.2.0 in npm/node_modules,
  "packages": []): `magpie library mcp add` wrote mcp.json and `pi mcp
  list` shows both the user's server and the new one connected. The old
  binary wrote mcp-adapter.json and Pi listed no servers. `magpie library
  sync` with pi off PATH (version unknown) moved that home's
  mcp-adapter.json back into mcp.json, with backups, and Pi connected both.
- Pi 1.0.4 with `pi install npm:pi-mcp-adapter@5.2.0`: mcp.json is kept,
  the new server goes to mcp-adapter.json, and `pi-mcp-adapter doctor`
  connects both.
- Pi 0.87.1: still mcp-adapter.json, as before.
Not run: Windows or WSL Pi, project-level .pi/settings.json, a real
interactive Pi session.

Reported by sydney on Discord; same cause as #1097 (heliar-k).
2026-10-10 10:18:16 +08:00
yetone 666c0fa4fd routing: the context window holds still while requests stream in, and the prompt-cache card shows the last request's hit, dimmed, or a dash with the reason instead of "——命中" and an empty bar (dumplings on Discord)
dumplings reported that on Routing, every new request made the context
window jitter and flash, and that the prompt-cache card read "——命中",
glued together, with an empty bar under it.

Causes, measured with a frame sampler while requests stream through
start/read/done:
- A live request's story is shorter than a finished one's. "Is
  answering…" becomes "answered in 4.0 s…", which wraps onto two lines
  at phone width. The log head also gains View usage and Replay once the
  request is done. So the card under it moved up and back down on every
  request: 45px at 420px wide, 6px at 900px.
- The state pill ("Live" or "Counted") changed width, which moved the
  model name beside it by about 11px.
- While a request was under way the cache card dropped to "—" with a
  0% bar, then came back. That was the flash.
- A flex item's leading space is dropped, so " hit" and " / 272K" were
  glued to the figure before them.

Fix:
- routing.js holdStory keeps the log head and steps at the tallest
  height seen while following live, at this width and language. A
  picked request, a replay or a past day is drawn at its own height. A
  ResizeObserver on the steps measures again as soon as the width
  changes. Without it, a scrollbar appearing before the first request
  left a stale height and a 6px shift (seen in WebKit under load).
- context.js stacks all three state words in one grid cell and shows
  only the current one, so the pill keeps one width.
- While the next request is under way, the cache card keeps the last
  request's figure, dimmed (.was), with "The last request's · this one
  is on its way". When there is nothing to show, it gives a faint "—"
  and the reason ("Read once the vendor answers" or "The vendor didn't
  say"). The bar stays hidden but keeps its space. The remembered
  figure resets when the agent or session changes.
- .ctx-big gets a gap. Width and opacity transitions are turned off
  under reduced motion.
- The three new strings are added in zh, zh-TW, ja and de.

Verified:
- The new tests/routing-ctx-still.test.cjs streams two requests and
  checks that the card, model, state, used, cache, figure, bar, grid,
  legend and foot rects never move by more than 0.5px. It also checks
  the figure-to-word gap and both empty states.
- It runs in Chromium and WebKit, at en 420/1100, zh 420/1100,
  zh-TW 900, ja 900 and de 420. It narrows the viewport by 10px after
  load.
- On the old assets it fails 16 of 16 (the card moves 6–52px). It also
  fails with only the ResizeObserver removed.
- routing-live-patch now expects the held figure ("97%") on live steps
  instead of "—".
- Under --test-concurrency=4 in both engines: 578/578 pass across
  routing-*, context-*, click-scroll, group-context, panel-routing and
  gui-de/ja/zh-tw.
- go test -tags nogui ./internal/gui passes.
- GOOS=linux and GOOS=windows go vet -tags nogui ./internal/gui are
  clean.
2026-10-10 10:13:43 +08:00
yetone e3bd766fae providers: a saved provider's editor keeps the add form's Fetch models button, "Fetch models again" once a list was fetched, to re-fetch after a key moves to another group (耍赖天都爱 on Discord)
耍赖天都爱 on Discord: 「provider中模型拉取一次就没有这个按钮了 能不能加上 有
的provider换一个分组要从新拉取模型」. The add form offers "Fetch models"
(获取模型); once saved, the editor's same action was a generic "Refresh"
(刷新) in the models footer, so after the first fetch the button looked
gone, while a relay key moved to another group serves another list.

The editor's button now says "Fetch models", and "Fetch models again"
(重新获取模型) when a vendor list was fetched before. Its title says it asks
with the key typed here and what happens to the picks. The fetch itself is
unchanged: it sends the typed form (asTyped), and Refetch replaces the list,
drops picks the new list no longer has and keeps ids added by hand. The
hints that named Refresh ("from models.dev · …", "magpie's list · …",
"Jev's names · …", Forget's title, the empty-list hint) name the button now,
in zh, zh-TW, ja and de. The TUI's "m fetch models" and `magpie provider
refresh` already re-fetch at any time; the CLI help names the app button.

Verified:
- new internal/gui/tests/provider-fetch-again.test.cjs (Chromium + WebKit;
  en and zh at 900 and 420px, zh-TW and ja at 900, de at 420): a never
  fetched relay shows Fetch models and its hint names it; a fetched one
  shows Fetch models again; with group B's key typed it asks with that key,
  shows group B's list, drops the pick B lacks, keeps an id added by hand,
  the button is still there and asks again; nothing scrolls. On the old
  app.js/i18n.js it fails: "waiting for ... getByRole('button', { name:
  'Fetch models', exact: true }) to be visible" (en), "again has its zh".
- provider-typed, provider-picks-stay (labels updated), gui-zh-tw, gui-ja,
  gui-de, click-scroll: 51 pass, 0 fail, both engines.
- go test -tags nogui . ./internal/tui ./internal/gui under a temp HOME: ok.
  go vet -tags nogui . ./internal/tui, GOOS=linux and GOOS=windows: ok.
2026-10-10 10:13:43 +08:00
yetone e611fc65d6 codex: magpie restarts the codex app-server a change left on the old model list when no codex session is on it, and the Agents page offers Restart when one is (luci on Discord)
luci (Codex CLI 0.162, magpie 0.1.1150): after Codex was wired back to
magpie, codex in a new terminal listed Codex's own models although
config.toml was magpie's.

Cause, reproduced with real codex 0.162.0 in a sandbox CODEX_HOME:
codex sessions attach to a background app-server (the managed daemon)
that is per CODEX_HOME, not per terminal (socket under
$CODEX_HOME/app-server-control). It is detached, has no idle exit and
builds its model list once, at start. A daemon started while Codex was
off magpie keeps the official list for every later session, in this
terminal or a new one, until it restarts. magpie writes only PATH to
shell rc files and doesn't unwire Codex on quit, so nothing
per-terminal is involved. luci's "a new terminal fixed it" isn't
explained by the daemon. Codex's hourly updater restarting it is a
guess, not proven.

What changed:
- provider.KeepCodexDaemonCurrent(ctx, home, changed) compares the
  daemon's start time with when magpie last changed what Codex reads at
  start. The start time is the mtime of a pid file in that home that
  names the running daemon's pid, so another home's daemon (ChatGPT.app,
  other sandboxes) is never acted on.
  - With no codex session attached, it runs
    `codex app-server daemon restart` with CODEX_HOME set to that home.
  - Otherwise it reports Behind and how many sessions are attached.
  - isCodexClient leaves out apps' and editors' own codex, exec-server,
    mcp-server and the daemon's own processes. It reads the whole ps line,
    since ps leaves a path's spaces unquoted ("Codex Computer
    Use.app/...").
- agent: Codex's Notice after a change runs this check first. It says
  either that magpie restarted the daemon, or that the daemon is behind
  and how to restart it (Agents page or the command).
- The GUI backend and `magpie serve` re-check every 15s. A session that
  was open at the change is usually closed later, and that is when the
  daemon can be restarted.
- Agents page: the stale "daemon" copy line has a Restart button that
  POSTs /api/codex/daemon/restart (timeout now 2 min, as a running turn
  is let finish). It reuses translated strings. On Windows, where
  processes aren't listed on each look, the row shows the last check.
- Restarts are serialized by one mutex.
- WSL's Codex is not handled automatically. Its row keeps the advice
  text, which names the restart command.

What was run:
- Real codex 0.162.0 in a sandbox CODEX_HOME, with a fake gateway, on
  luci's sequence: the daemon started on the official config, then
  magpie's config and catalog were written.
  - With a TUI open, /model listed GPT-6.x. The check said Behind with
    a session attached and did not restart.
  - With the TUI quit, the check restarted the daemon. A new codex in
    the same terminal listed Magpie Alpha and Beta.
- Earlier in the sandbox: a daemon restart under an idle attached 0.162
  TUI reconnects, and /model then shows magpie's models. A running turn
  is waited for up to 60s.
- New tests:
  - TestKeepCodexDaemonCurrent(Fails), TestIsCodexClient (with the
    space-in-path case from the real process list) and
    TestCodexNoticeKeepsTheDaemonCurrent. They don't build or fail on
    the old code. The space case failed before the ps-line fix.
  - The GUI test "the daemon's line restarts it" times out on the old
    app.js.
- Results:
  - agent-stale-copies.test.cjs: 60/60 in Chromium and WebKit, in en,
    zh, zh-TW, ja and de, at widths 980 and 440.
  - go test (temp HOME) for ./internal/agent, ./internal/provider,
    ./internal/gui and the root package: ok.
  - gofmt -l: clean.
  - GOOS=linux and GOOS=windows go vet -tags nogui: ok.
- The Windows path is checked only by vet and not tried on a Windows
  box.
2026-10-10 10:05:10 +08:00
yetone bb3b3f1486 gateway: with "only each model's largest window" on, Claude Desktop shows a model of 1M or more as its 1M entry, "1M context window", not as what looked like the plain model (杰多夫 on Discord)
The cause: under settings.DesktopLongest, /v1/models lists such a model by
its "[1m]" id alone (#1272). Claude Desktop 2.7032 (app.asar, read only)
marks an entry as the 1M one only on the entry it adds itself: JLt emits
`${id}[1m]` with description WLt = "1M context window" and
supports_1m_context, and only for a model with supports1m. PIt gives
supports1m to a bare id alone (LC(id) === id), and OIt folds a "[1m]" row
only into a bare one listed beside it. So the lone "[1m]" row was shown by
its name with no description, exactly like the plain entry it replaced: with
the switch off the menu had "DeepSeek V4 Pro" plus "DeepSeek V4 Pro · 1M
context window"; on, only "DeepSeek V4 Pro". The reporter read that as the
switch keeping the plain model and dropping the 1M one.

What changed: such a row now carries description "1M context window", the
words Desktop gives its own 1M entry, and Desktop shows a row's description
(PIt keeps it; JLt prefers it to its fallback). The id is unchanged: the
"[1m]" is what gives a Claude Code session the 1M window, and magpie serves
it as the plain model. Rows under 1M, and every row with the switch off, are
unchanged (no description).

What was run:
- TestClaudeDesktopLongestOnly now checks each row's description, on and
  off. With the description line removed it fails: "on: vend/deepseek-v4-pro
  (...[1m]) described "", want "1M context window"".
- Sandbox HOME gateway on 127.0.0.1:3561 with real providers, /v1/models
  as Claude Desktop asks for it, fed through a script that follows
  PIt/OIt/JLt: on, each 1M model is one entry "<name> · 1M context window";
  off, the plain entry plus Desktop's own 1M one, as before.
- gofmt -l; GOOS=linux and GOOS=windows go vet -tags nogui
  ./internal/gateway; go test -tags nogui ./internal/gateway under a temp
  HOME: ok. No GUI or string change.
2026-10-10 09:58:30 +08:00
yetone d9bf7c1ccf zcode: the API format the user picks for magpie's provider in ZCode's settings stays theirs; a sync no longer puts Anthropic Messages back on every start (杰多夫 on Discord)
What changed: zcodeRules wrote api.type "anthropic-messages" into
provider_config.json, and zcodeProviderJSON wrote kind "anthropic" into
config.json, on every sync (magpie's start, any catalog change). A user who
switched the provider to OpenAI Chat Completions or Responses in ZCode had it
switched back the next time magpie started. zcodeFormat now reads the format
already there and keeps it, in both files: provider_config.json's api.type
(ZCode 3.14 on) or config.json's kind (older ZCode), the one written last
where they differ, mapped as ZCode maps them (anthropic-messages/anthropic,
openai-chat-completions/openai-compatible, openai-responses/openai). A new
provider, or a format ZCode doesn't have, is Anthropic Messages as before.

Why it's safe: magpie's gateway answers all three at the same base address.
Checked with ZCode 0.16.9's own agent CLI (glm/zcode.cjs -p, run from a
copy under a sandbox HOME, never ~/.zcode) against a sandbox gateway with
the DeepSeek provider: anthropic-messages asks /v1/messages,
openai-chat-completions /chat/completions, openai-responses /responses, and
a tool-using turn (Read a.txt) answered 200 on each, to deepseek-flash.

What was run:
- TestZCodeKeepsUserFormat (new): a format picked in provider_config.json,
  for each of the three, survives two syncs and is carried to config.json's
  kind; one picked in config.json is carried to provider_config.json; an
  unknown one isn't kept. On the old zcode.go it fails: "picked
  openai-chat-completions: kind "anthropic", api "anthropic-messages"".
- Sandbox, real binaries: provider_config.json set to openai-responses,
  then `magpie zcode magpie`: the old build wrote anthropic-messages back,
  this one kept openai-responses (config.json kind openai). Then
  openai-chat-completions, resynced, and the real ZCode CLI ran a turn on the
  file magpie wrote, served over /chat/completions.
- gofmt -l; GOOS=linux and GOOS=windows go vet -tags nogui ./internal/agent;
  go test -tags nogui ./internal/agent under a temp HOME: ok.

The "bcww: Field required" 400 on Anthropic Messages is not part of this
commit: ZCode's real requests (captured from its CLI, plain and with web
search) to deepseek/deepseek-flash through magpie returned 200, and DeepSeek
answers schema errors in serde's words, not "Field required".
2026-10-10 09:58:30 +08:00