376 Commits
Author SHA1 Message Date
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 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 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 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 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 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 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
yetone c65d9fb473 codex: magpie-models.json keeps the keys the user added by hand to a model, and says supports_reasoning_summaries itself, so Codex before 0.145 sends the effort and shows thinking (heng91, #1450)
What was wrong: every write of ~/.codex/magpie-models.json rendered the
catalog from magpie's models alone, so a key the user had put into an
entry was gone after the next model switch, catalog sync (Refresh, a
provider added, a gateway start) or a switch to Codex's own default,
which also deleted the file outright. The key heng91 added,
supports_reasoning_summaries, is one Codex before 0.145 needs: its
client.rs build_reasoning sends no `reasoning` (no effort, no summary,
so no thinking shown) for a model whose entry doesn't say true, until
openai/codex#32206 (first in 0.145). 0.144 also refuses to load a
catalog an entry of which lacks the key at all.

What changed:
- codexcat: every entry says supports_reasoning_summaries, true for a
  model with effort levels, false otherwise. Later Codex ignore it.
- codexcat.Keep: before a write, the keys of an entry on disk magpie
  doesn't write are carried over to the entry of the same slug, and a
  true of supports_reasoning_summaries stays over magpie's false. A key
  magpie writes keeps magpie's value; a model magpie no longer serves
  drops out; an unreadable file keeps nothing.
- agent/codex.go: the write when magpie becomes the provider and the
  catalog sync go through Keep; the three places that removed the file
  remove it only when nothing of the user's is in it.

Run:
- New tests TestCodexCatalogKeepsWhatTheUserAdded,
  TestCodexCatalogRemovedWhenNothingAdded (agent, from the file magpie
  writes, edited by hand) and TestCodexCatalogReasoningSummaries,
  TestKeepUserKeys (codexcat). Each fails on origin/main's code.
- Real Codex in a sandbox CODEX_HOME against a capture server:
  0.144.0 fails to load the old catalog ("missing field
  supports_reasoning_summaries"), loads the new one and sends
  reasoning {effort: high, summary: auto}; with the user's keys kept
  it sends summary "detailed". 0.162.0 loads both and sends the same.
- gofmt -l clean; go vet, GOOS=linux and GOOS=windows go vet of
  ./internal/agent ./internal/codexcat; linux and windows builds;
  go test -tags nogui ./... under a temp HOME, all ok.
2026-10-10 09:07:44 +08:00
yetone 1134c7a8af codex: the restart advice after a model or catalog change names the app-server codex sessions share ("codex app-server daemon restart"), on Windows and WSL too (TJHHHH, luci on Discord)
TJHHHH and luci (magpie 0.1.1137, codex-cli 0.162.0): the Codex app
listed magpie's models while the codex CLI showed only the official
list, on and off. Codex 0.162's TUI attaches to a background
`codex app-server --managed-daemon` (feature daemon_auto_start, stable
and on by default on every OS; skipped on Windows only when elevated or
detached launch is restricted, on WSL only when CODEX_HOME is on drvfs).
That daemon builds its model list once, when it starts, and restarts
itself only when its binary or version changes. So closing every codex
session and opening a new one keeps the list from before magpie wrote
model_catalog_json, while the desktop app's own app-server, restarted
with the app, has magpie's.

Reproduced with the real codex-cli 0.162.0 in a sandbox CODEX_HOME: a
daemon started with no catalog kept listing Codex's 8 models in model/list
after config.toml switched to a magpie catalog and every client closed;
`codex app-server daemon restart` made it list the catalog's models.
Nothing since v0.1.1137 changed this.

magpie's advice only said "restart the Codex app (and open codex
sessions)", which doesn't reach the daemon. On macOS and Linux the
Agents row already flags a stale daemon copy (StaleCopies), but that
returns nil on Windows and WSL, where the reporters may be. The Notice,
and the WSL codex kind's restart text, now name the shared app-server
and provider.CodexDaemonRestart. magpie still doesn't restart it itself:
that ends every session attached to it (as 03dbde12 decided).

Test: TestCodexAdviceNamesTheSharedAppServer (codexRunning stubbed)
fails on the old texts and passes now; it also checks nothing is said
when no codex runs.

Run: gofmt -l (clean); go vet -tags nogui on darwin, GOOS=linux and
GOOS=windows for ./internal/agent ./internal/gui ./internal/tui and the
main package; go test -tags nogui of those under a scratch HOME (all
ok). Not run on Windows or a real WSL distro, and the reporters haven't
confirmed their codex had a daemon running from before the switch.
2026-10-10 06:27:40 +08:00
yetone 5aca7d0924 codex: effort left at Default stays Default and says the level Codex takes then ("default (medium)"), and isn't written over with medium when a model is picked or the catalog synced (lgtm on Discord)
lgtm set Codex · WSL Ubuntu's 推理强度 to 默认 and it came back as 中.
Two things did that, on every platform (the WSL row is the same codexIn
code as the local one):

- The effort field's Get answered an unset model_reasoning_effort on a
  magpie model with codexcat.DefaultEffort (medium), so right after the
  pick the slider and the row read 中, not 默认.
- settle() counted an unset effort as one the model doesn't take and
  wrote DefaultEffort into config.toml: on every Sync for a magpie
  model, on Join, and on a model pick, Codex's own models included. So
  Default was pinned as medium, and on Codex's own models whose default
  is low (gpt-6.1-sol, gpt-6-astra, gpt-5.6-sol in 0.162's models.json)
  magpie overrode Codex's own default.

With model_reasoning_effort unset, Codex takes the catalog entry's
default_reasoning_level (core/src/client.rs build_reasoning). Checked
for real with codex-cli 0.162.0 in a sandbox CODEX_HOME: a
model_catalog_json entry with default_reasoning_level "low" and no
model_reasoning_effort sent {"reasoning":{"effort":"low"}} to a stand-in
Responses endpoint.

Now settle replaces only a set effort the model doesn't take, Get
returns the unset value, and the effort control's options start with
Default carrying Takes: the level Codex then takes, from the entry it
reads (codexcat.TakesEffort: Codex's own models_cache entry for its
models and a ChatGPT account's codex/ ones, else the one Entries writes).
The GUI shows it as "default (medium)" / 默认(中) / 預設(中) /
デフォルト(中) / Standard (mittel). A stopped WSL distro's options get the
same Default (codexEfforts).

Tests: TestCodexDefaultEffortStaysDefault fails on the old settle
("Default written over: model_reasoning_effort = \"high\"", "Default
shows as \"high\"") and passes now. TestCodexCustomProviderModelHasEfforts
now expects Default first with Takes "high" and an unset value read as
"". codex-default-effort.test.cjs (window at 560 and 360px, tray panel;
en, zh, ja, de; Chromium and WebKit) fails on the old effortName and
passes now.

Run: gofmt -l (clean); go vet -tags nogui on darwin, GOOS=linux and
GOOS=windows for ./internal/agent ./internal/codexcat ./internal/gui
./internal/tui and the main package; go test -tags nogui of those and
./internal/catalog under a scratch HOME (all ok); the GUI effort, gui-ja,
gui-de, gui-zh-tw and i18n-keys tests (all pass), and the whole GUI suite
in both engines: 4711 pass, 8 fail, all in library-servers-bulk
(chromium, ".lib-everyon moved from 290 to 288") and purpose-filter
(webkit), which fail the same way run alone on origin/main c1c88779.
Not run on a real WSL distro.
2026-10-10 06:27:40 +08:00
yetone 7f62dbf3ed gateway, agents, gui: a model's reply limit is published within the window beside it, on /v1/models, Cursor's and Muse's lists, the Gateway page and magpie model output (#1438, yhong91)
#1438 (yhong91): GET /v1/models listed grok/grok-4.7 with context_window
256000 (the Grok backend's own) and max_output_tokens 500000 (models.dev's
x-ai entry, which catalog.Decorate fills in when the backend gives no
output). A client that reads both sizes its replies past what the window
holds. The agents' files already kept output within the window (maxTokens,
#338/#347), but the gateway's lists, the GUI and the CLI did not.

What changed:
- provider.OutputWithin(window, output) and Entry.PublishedOutput(): the
  output is kept within the window; an unknown window or output (0) is left
  as it is.
- Every place that publishes the pair uses it: modelObject (/v1/models),
  cursorLocalModel (Cursor Private Inference), Muse Code's list, the
  Gateway page's models and groups (providerInfo, providersState), the
  agents' maxTokens, and `magpie model output` (which also says
  "500k, kept within its 200k window" when it lowers one).
- Entry.Output stays the model's own limit. The request-path cap
  (outputLimit/withMaxOutput) and the user's Max output setting are
  unchanged, so no request is lowered further than before. This is why the
  clamp is not in replyLimit/entryFor as the issue suggests, and why the
  commenter's patch (modelObject only) was not taken as is: it was right
  but missed Cursor, Muse, the GUI and the CLI.
- docs/subsystems/gateway-routing.md: the rule and its routes.

Tests (each fails without the change):
- gateway TestModelListsKeepOutputWithinTheWindow: models.dev 500000 over a
  backend 256000 window, plus output within the window, unknown window and
  unknown output, on /v1/models, Cursor's capabilities and Muse's limit; a
  group request with max_tokens 300000 still reaches upstream as 300000.
- provider TestOutputWithin, gui TestProviderModelOutputWithinTheWindow,
  TestModelOutputCmdWithinTheWindow.
Breaking it by hand: OutputWithin returning output unclamped turns all of
them red, plus agent TestMaxTokensWithinContextWindow; gateway sites back
on e.Output turn the gateway test red on all three lists; moving the clamp
into Entry.Output turns the fixture and the group-request guard red.

Real data: an offline dump of the owner's catalog (copies of
~/.config/magpie and its caches, 203 entries) has 3 models with output
over their window, grok/grok-4.5, 4.6 and 4.7 (256000 vs 500000), now
published as 256000; 0 are not Grok. The live gateway on 3425 was not
running, so /v1/models itself was not read, and no real Grok request was
made.

Run: gofmt -l; go vet -tags nogui ./... on darwin, linux and windows;
go test -tags nogui ./... -count=1: all green except
TestLoopEndsWorkBuddysReply/plugin/anthropic ("let go only after 3207
lines", bound 2000) at load average ~35 with other suites running; it
passes 5 of 5 alone and does not touch the output limit or request path.
2026-10-10 01:30:29 +08:00
yetone 189586612e agents, gateway: Codex's subagents go on a model set in its row, a ChatGPT account's (willz on Discord)
Codex's lead names each subagent's model itself in spawn_agent, and
Codex's [agents] config can't overrule it. The Agents page's Codex row
now has a "subagent model" square (the app's model picker, Default
first). Its options are only models a ChatGPT account in magpie serves,
because a subagent's task is sealed for ChatGPT accounts.

- settings.CodexSubagentModel holds it. It is an agent Field
  (subagent_model), so the TUI/CLI, profiles, davsync's "agents" part
  and WSL twins carry it too. The setter refuses another provider's
  model. It keeps a ChatGPT account's model that isn't served now, so a
  backup restored on another computer still applies (ApplyFields stops
  at the first failed Set). POST /api/settings keeps it.
- The gateway (codexSubagentPick) moves a collab_spawn request onto it.
  codexBackend does this for Codex's own bare models; serve() does it
  for any other model. When no ChatGPT account on in magpie serves it,
  or a held gateway key may not use it, the request stays on the model
  asked for. Route.Subagent records asked/to/kept. The lead's turns,
  titles, guardian and the rest are never moved. Off (the default)
  changes nothing.
- Routing page: kindWhy says the subagent was put on the model set, or
  stayed on its own and why. "only account on for" names the model it
  went on. Strings added in zh, zh-TW, ja and de.
- docs/subsystems: gateway-routing.md and agent-wiring.md describe it.

Run (darwin, rebased on origin/main):
- gofmt -l internal/: clean.
- go vet -tags nogui on gateway, provider, agent, gui and settings:
  darwin, GOOS=linux and GOOS=windows.
- go build ./...
- HOME=<temp> go test -tags nogui ./...: all ok.
  TestSettingsSaveKeepsWhatItDoesNotSend flagged the new field; it is
  now set there.
- GUI tests, Chromium + WebKit:
  - codex-subagent-model.test.cjs: 20/20 (en, zh, zh-TW, ja, de at
    1100 and 560 px).
  - Also green: routing-kind, subagent-effort, codex-memories,
    codex-memories-settings, i18n-keys, gui-ja, gui-de, gui-zh-tw and
    click-scroll.

Failing without the change (codexSubagentPick returning nil):
- TestCodexSubagentPutOnTheModelSet
- TestCodexSubagentOnAnotherProviderPutOnTheModelSet
- TestCodexSubagentModelNotServedKeepsTheAsked
- TestCodexSubagentModelHeldKeyKeepsTheAsked
TestCodexSubagentModelLeavesOtherRequests guards the other side: the
lead's turns, titles and guardian stay untouched.

Not tried with a real Codex CLI and ChatGPT account. A subagent spawn
needs a signed-in Codex home, and this session may not touch the live
~/.codex. The fake ChatGPT backend tests cover both routes.
2026-10-09 23:38:50 +08:00
yetone 7953c7ef9b agent: a config folder holding only skills/ no longer counts as the agent installed, since skill installers make one for every agent they know (qtwaiter on X)
qtwaiter's Agents page listed Goose, Crush, Command Code, fx, Devin,
Hermes and Droid, none installed. Skill installers (npx skills and the
like) write <folder>/skills for each agent they support, and a config
folder alone made an agent detected. agentDir now takes a folder for the
agent's own only when it holds something besides skills/ (.DS_Store
aside); the default rule, Goose's and fx's detect use it.

Run: go test ./internal/agent ./internal/library ./internal/gui
./internal/profile ./internal/backup ./internal/tui;
TestSkillsOnlyFolderIsNotInstalled fails on the old code for all seven.
gofmt, go vet darwin/linux/windows clean.
2026-10-09 22:26:05 +08:00
yetone 603dc0c44f dsh: Disconnect keeps the model picked in dsh 0.2, which saves it into magpie's agent-default-model entry (#1392)
What dsh 0.2.0-rc.2 changed (read in @deepseek-ai/dsh-settings and
dsh-agent-default-model from npm): ~/.dsh/settings.yaml is gone. On its
first boot dsh renames it to settings.yaml.imported and merges its sections
into the booted profile's patch list (profiles/<name>/cordis.patch.yml);
nothing reads the .imported file afterwards. A /model pick is no longer
kept in settings.yaml either: agentDefaultModel.saveSelection goes through
ConfigEditor.edit, which replaces the config of the profile's last
agent-default-model entry. That entry is magpie's, so the user's pick sits
under magpie's "# magpie" mark. Profile paths, the home layer, .env and
.credentials.yaml are unchanged.

What broke: Disconnect took that entry for magpie's own, dropped it and put
back what magpie stashed from before it connected, so the model picked in
dsh (or imported from settings.yaml) was lost. Seen with real dsh
0.2.0-rc.2: after the import, Disconnect left `[]` and dsh started on its
shipped deepseek-flash instead of the imported deepseek-v4-flash.

Fix: dshKeepPick, run per profile before Disconnect takes magpie's wiring
out, hands a marked agent-default-model that is off magpie's route back to
the user (mark removed, old stash discarded). An entry on magpie's route,
or one on the pre-0.1.5 wiring, is still magpie's. dshStart still reads a
settings.yaml not yet imported, which dsh will put over the booted profile.

Run: TestDshPickSavedIntoMagpiesEntryOutlivesDisconnect (fixture from the
patch real dsh 0.2.0-rc.2 wrote) fails on the old code ("the pick made in
dsh was lost", the stashed deepseek-v4-pro came back) and passes. Real dsh
0.2.0-rc.2 under a temp HOME/DSH_HOME, fake gateway on 127.0.0.1:3561:
`dsh headless "say hi"` sent POST /v1/chat/completions, Bearer magpie,
model deepseek/pro to it, before and after a settings.yaml import; after
Disconnect `dsh headless --dump-config` keeps deepseek-v4-flash;
reconnecting sends deepseek/flash to the fake gateway. go test -tags nogui
./... (temp HOME, pinned caches) passes, -race -count=5 -run TestDsh
passes, gofmt -l clean, go vet and GOOS=linux/windows go vet -tags nogui
./internal/agent pass.
2026-10-09 22:26:00 +08:00
yetone dec951229c agent: Grok Build installed by xAI's installer shows its newest version and offers grok update, and its version is read again after each update (xiaoxiaofeixz on Discord: Grok at C:\Users\<user>\.grok\bin\grok.exe)
Grok Build had no cliSpec and no Bin (grok is also Homebrew's regex
tool), so the Agents page showed nothing about its CLI: no version, no
newest, no update.

xAI's installers (read from https://x.ai/cli/install.ps1 and install.sh):
- install.ps1 copies grok.exe (and agent.exe) into %USERPROFILE%\.grok\bin
  and puts that on the user PATH; no junction, no versioned folder.
  grok update copies the new grok.exe over it (the running one renamed to
  grok.exe.old), so its size and time change.
- install.sh links ~/.grok/bin/grok to ../downloads/grok-<platform>;
  grok update downloads ../downloads/grok-<version>-<platform> and relinks,
  a new real path per version.
- The newest version of a channel is the body of x.ai/cli/<channel>
  (stable, alpha, enterprise; GCS fallback), the channel kept under [cli]
  in ~/.grok/config.toml.

cliSpec gains `at` (where the vendor installer puts the CLI under home,
read there instead of PATH for an agent with no Bin) and `latest` (the
newest its own updater installs, for a CLI not on npm). grok's spec: at
.grok/bin/grok(.exe), self = grok update for a real path under
.grok/bin or .grok/downloads, latest = grokLatest (channel pointer, 6h
memo per channel).

TestGrokInstalled builds the platform's installer layout under a temp
HOME/USERPROFILE with a fake grok on PATH, a served channel pointer and
fake version/update runs: PATH's grok isn't taken; 1.0.46 shows 1.0.50 and
"grok update" (grok.exe update on Windows); the update reads 1.0.50; grok's
own update outside magpie (auto_update) is read again; the alpha channel's
newest is alpha's. It fails without the grok spec (macOS and the Windows
box: "before: {Version: ...} false") and passes with it.

Real use: macOS, a sandbox HOME with the real grok 1.0.46 in install.sh's
layout: CLI() said 1.0.46 / 1.0.50 / grok update, UpdateCLI ran the real
grok update and read 1.0.50 (~/.grok untouched). Windows box (10.0.0.132,
sandboxed USERPROFILE/APPDATA/LOCALAPPDATA): the real grok-1.0.46 exe at
home\.grok\bin\grok.exe, same result through a real grok update; the real
profile's .grok, LocalAppData\grok and user PATH unchanged. There
TestGrokInstalled, TestCodexInstallPs1Junctions and
TestHowInstalledWindowsShim pass.

Siblings: Claude's install.ps1 lands in ~/.local/bin/claude.exe (already
matched); Cursor's and Qoder's Windows installers make no junctions or
links, and those agents have no cliSpec (not in scope); opencode has no
install.ps1 (404); Factory's refused the fetch (403), droid has no
cliSpec either.

go test -tags nogui ./internal/agent (temp HOME), gofmt -l, go vet, and
GOOS=linux / GOOS=windows go vet -tags nogui ./internal/agent pass.
2026-10-09 22:25:24 +08:00
yetone 3691c2047d plugin, agent: bun is given a SOCKS proxy through the bridge, so a plugin installs with only a SOCKS system proxy (#1409)
With only a SOCKS system proxy on macOS (127.0.0.1:4401), every plugin
install failed: netproxy.Env put socks5://… in bun add's *_PROXY, and Bun
takes only http:// and https:// proxies, so it stopped with
"UnsupportedProxyProtocol downloading package manifest". The plugin
host's own fetches already went through netproxy.Bridge (hostEnv), but
bunCommand, which runs bun add, update and remove, did not.

- netproxy.EnvForBun rewrites a SOCKS5 HTTPS_PROXY/HTTP_PROXY/ALL_PROXY
  (any case) to the loopback HTTP bridge in front of it; others as they are.
- bunCommand uses it, so add, update, remove and the host's run all get it.
- Sibling: an agent CLI installed with bun is updated by `bun add -g`
  with the same env (updateEnv in cliupdate.go); npm is left as it is.
- provider-plugins.md says so.

Run:
- TestInstallThroughSOCKSProxy (real bun 1.4.2, fake registry, SOCKS
  proxy only): on the old bunCommand it fails with the reporter's exact
  "error: UnsupportedProxyProtocol downloading package manifest"; passes
  with the fix, through the SOCKS proxy.
- TestEnvForBunBridgesSystemSOCKS (system SOCKS proxy via Env) and
  TestUpdateWithBunBridgesSOCKS fail with EnvForBun / the bun branch
  broken ("bun is given …socks5://…"), pass with it.
- Real use: a throwaway test with a real SOCKS5 server and only
  *_PROXY=socks5://… installed @magpie-community/opencode-cursor-auth
  0.2.2 from registry.npmjs.org through it; on the old code the same
  failed as UnsupportedProxyProtocol. Scratch HOME, not committed.
- gofmt -l clean; go vet ./...; GOOS=linux go vet -tags nogui ./...;
  GOOS=windows go vet ./...; go build; GOOS=linux go build -tags nogui;
  GOOS=windows go build; go test -tags nogui ./... (scratch HOME) all ok.
2026-10-09 19:35:35 +08:00
yetone 0b5b10da2a agent, gateway, gui, tui: a Codex that goes through magpie only to fail over to the user's other ChatGPT accounts says so on the Agents row and keeps to its own models (#1385, shenghsi)
With several ChatGPT accounts on, failover writes openai_base_url into
~/.codex/config.toml so a turn the signed-in account can't pay for goes on
to the next account. With Codex's switch off, the row said only "not
connected", but every request still went through the gateway, and the
gateway's /backend-api/codex/models answer added all of magpie's models
(62 for the reporter).

- agent: Agent.FailingOver means not wired, while the stash's codex.failover
  (or a CC Switch mirror's codex.mirror_failover) base URL points at magpie.
  FailoverSaid is the line the CLI list and the TUI show. CodexOwnOnly is
  true for this machine's Codex when it is failing over and no WSL Codex is
  connected, since a WSL Codex shares the gateway path. Sync ages a
  models_cache.json that isn't tagged with the own-only list.
- gateway: codexModels leaves out provider.CodexListed when
  gateway.CodexOwnOnly (set by main to agent.CodexOwnOnly) says so. The
  ETag and X-Models-Etag carry provider.CodexOwnListTag. Neither tag
  contains the other, so Codex fetches the list again when it is connected
  or disconnected.
- gui: /api/state carries failover. The row says "Not connected · goes
  through magpie only to fail over to your other ChatGPT accounts". Its
  title says what turns it off. A "ChatGPT accounts" button opens
  Providers › Codex. Strings are in zh, zh-TW, ja and de. openProvider no
  longer draws the Providers list before it has loaded. It was only safe
  once the tab had been opened, and this is its first caller from another
  page.
- docs: agent-wiring and gateway-routing describe the failover-only Codex
  and list the new tests.

A user's hand-written openai_base_url (#856) isn't failover's, so it stays a
plain not-connected row.

Verified:
- TestCodexFailoverOnlyKeepsOwnModels (agent) builds the reporter's state in
  a temp HOME: Codex signed in, two more ChatGPT logins on, Sync, so the
  stash has codex.failover=1 and no joined/beside. It checks that the row is
  not wired and is FailingOver, that FailoverSaid names Providers › Codex,
  and that the real gateway handler's /backend-api/codex/models lists only
  the backend's gpt-5.5. It also checks that the cached full list is aged,
  and that after Join magpie's fake/m1 is back.
- TestCodexModelListOwnOnlyForFailover (gateway) checks the list and both
  ETags with the hook on and off.
- agent-failover.test.cjs checks the row text, title and button in en, zh,
  zh-TW, ja and de, at 980px and 420px, in Chromium and WebKit (20 pass).
  It also checks that the button opens Codex's editor, that the page
  doesn't scroll sideways, and that no click moves it.
- Broken by hand, each test goes red:
  - codexModels ignoring the hook fails both Go tests.
  - failingOver returning false fails the agent test.
  - dropping the Sync cache case fails the agent test.
  - removing the GUI branch fails the GUI test.
  - drawing the list before it has loaded (the old openProvider) fails the
    GUI test.
- go test -tags nogui ./internal/agent ./internal/gateway ./internal/provider
  ./internal/gui ./internal/tui ./internal/codexcat . in a temp HOME: all ok.
- node --test agent-*.test.cjs (254 pass) and i18n-keys, gui-ja, gui-de,
  click-scroll: all pass.
- gofmt -l is clean.
- go vet -tags nogui on darwin, linux and windows: clean.
- go build, and GOOS=windows go build: ok.
2026-10-09 14:10:13 +08:00
yetone 1bc0ed404a minimax-code: magpie's models have one slash in the whole ref, so SubAgents take them (#1387, worksyang)
MiniMax Code 3.1.0 checks a SubAgent's model against /^[^/\s]+\/[^/\s]+$/
(uQn in @minimax-ai/code's chunk-2XPMNZVR.js), and magpie wrote its models
under custom_provider.magpie by catalog id, so
custom_provider:magpie/bb-codex/gpt-6.1-sol was refused
(AGENT_CONFIG_INVALID).

- The writer keys each model by its catalog id with "~" for each slash
  (bb-codex~gpt-6.1-sol, provider.FlatID), when exactly one served id
  spells that way (provider.Flattened); else it keeps the slashed id.
- The gateway takes such an id back to that one provider's model or group
  in validateModel and unprefixed (unflat / provider.Unflat): only an id
  with "~", no slash, that resolves as nothing else; two ids spelled alike
  are neither. Never a bare-name first hit. Key limits, middleware and
  usage see the catalog id, so a key held to bb-codex/* is refused
  other~gpt-6.1-sol on every route, count_tokens too.
- A config an older magpie wrote moves on the next sync: the user's keys on
  a model go to its new key, defaultModel, defaultLightModel and
  memory.captureModel/consolidationModel follow, and the slashed key stays
  with enabled: false, so a session begun on it still resumes (checked
  with mcode exec --continue). Get still reads magpie/<catalog id>, and
  Disconnect still puts back the stashed default and variant.

Other agents: no installed agent bundle here (cline, command-code,
qwen-code, deepseek, pi, omp, codebuff, openclaw) has this regex; native
agents were not checked.

Verified:
- go test -tags nogui ./internal/agent ./internal/gateway ./internal/provider
  (temp HOME), gofmt -l, go vet darwin/linux/windows.
- New TestMiniMaxCodeOneSlash uses the reporter's regex and ids;
  TestFlatModelIDsReachTheirProvider has two providers serving
  gpt-6.1-sol. Breaking unflat, mcodeKey, the key carry-over, the legacy
  enabled:false and the ref rewrite each turned the tests red.
- Real run: built magpie serve in a temp HOME on 47812, two fake upstreams;
  `magpie mcode magpie/other/gpt-6.1-sol` wrote other~gpt-6.1-sol; real
  mcode 0.6.5 exec reached other's upstream with gpt-6.1-sol (usage:
  other/gpt-6.1-sol); its agent loader accepted an agent.md with
  custom_provider:magpie/other~gpt-6.1-sol and refused the two-slash one;
  `magpie mcode default` put minimax/MiniMax-M2.7 back.
2026-10-09 14:08:07 +08:00
yetone e66fa16593 codex: magpie's actor header goes into a [model_providers.magpie.http_headers] table the config already has, instead of failing every model switch (wztlink1013 on Discord)
wztlink1013 (v0.1.1134, Windows, Codex 0.162.0) could not switch Codex's
model on the Agents page: "config.toml: edited TOML is invalid: toml: key
http_headers should be a table, not a value".

Cause: putProvider rewrites [model_providers.magpie] whole with
http_headers = { "x-openai-actor-authorization" = "magpie" }. A config
that holds the headers as their own [model_providers.magpie.http_headers]
table keeps that table (a separate block the rewrite leaves), so the file
defined http_headers twice and writeTOML refused it. Reproduced in a temp
dir with exactly that message, in LF and CRLF files alike, so line
endings are not the cause. With only the subtable and no parent table the
same edit failed as "key http_headers is already defined". Dotted keys
inside the table (http_headers."x-org" = …) did not fail but were
dropped, the user's other headers with them.

Fix: edit.Inline, an inline table SetTOMLTable merges into the spelling
the file has: in [name.key] each entry is set with SetTOMLKey's in-place
edit (other headers and comments kept); among dotted keys their lines are
kept as written and each entry set there; otherwise written inline,
byte for byte as before. putProvider passes the header as an Inline.
SetTOMLKey's body moved into setTOMLKeyLines so both share it.

Siblings checked: no other agent writes an inline table with
SetTOMLTable (kimi's providers.magpie and Codex's thread tables are
scalars only); the MCP writer (putCodex) already removes a server's
subtables before writing it. Not changed: a magpie provider spelled by
dotted keys outside its own table ([model_providers] magpie.x = …, or at
the top) still fails as "table magpie already exists"; that is not this
report's shape.

Run:
- TestSetTOMLTableInlineMergesUsersSpelling (internal/edit): own table,
  own table without the header, own table only, dotted keys (with and
  without the header), inline, absent; each LF and CRLF, checked by
  decoding the result. Red with the subtable branch disabled (the
  reported error) and with the dotted branch disabled (user's x-org lost).
- TestCodexMagpieHeadersAsOwnTable (internal/agent): the reporter's shape
  through codex().Fields[0].Set, LF and CRLF. Red with putProvider back
  on edit.Raw: "key http_headers should be a table, not a value".
- gofmt -l (clean); go vet, GOOS=linux go vet -tags nogui, GOOS=windows
  go vet (with and without nogui) on internal/edit and internal/agent;
  go build for darwin, windows and linux (nogui).
- go test -tags nogui ./internal/edit ./internal/agent ./internal/library
  ./internal/provider under a temp HOME with pinned caches: all ok.
- codex-cli 0.161.0 (CODEX_HOME in a temp dir, codex mcp list) loads the
  merged config. Not tried on the Windows box or Codex 0.162.0.
2026-10-09 13:50:53 +08:00
yetone 98236edd77 agent: Codex installed by install.ps1 on Windows shows its newest version and offers codex update, and its version is read again after each update (xiaoxiaofeixz on Discord: 不会自动检测最新的版本)
Codex's install.ps1 puts %LOCALAPPDATA%\Programs\OpenAI\Codex\bin on PATH.
That folder is a junction to ~\.codex\packages\standalone\current\bin, and
current is a junction to releases\<version>-<target>, retargeted by each
update. filepath.EvalSymlinks leaves junctions alone since Go 1.23, so
magpie saw the binary only as ...\Programs\OpenAI\Codex\bin\codex.exe:

- howInstalled didn't find .codex/packages/standalone in its path, so the
  Agents page showed the version with no newest version and no update.
- installedVersion's stamp was that unchanged path plus size and mtime.
  npm's archive, which install.ps1's legacy layout copies from, gives every
  version's codex.exe the same 1985 mtime. So a new release was asked again
  only if its size happened to differ, else after the 24h memo.

proc.RealPath is now the path Windows opens (GetFinalPathNameByHandle),
junctions followed, and filepath.EvalSymlinks elsewhere. cliupdate's
howInstalled and installedVersion and proc.Version use it.

TestCodexInstallPs1Junctions (Windows) builds install.ps1's layout with
mklink /J, two releases whose codex.exe have the same bytes and mtime. It
checks that codex is recognized as self-updated (codex update) and that
retargeting current makes the version asked again. On the Windows box
(10.0.0.132, sandboxed USERPROFILE/APPDATA/LOCALAPPDATA) it fails on the old
code (howInstalled nil; "0.161.0" after the update) and passes on the new.
The rest of the agent package fails on Windows with the same 17 tests
before and after (TestFolderVarsAreListed, TestAside*, TestVSCodium, ...),
which CI never runs there. `codex update` on Windows is in codex.exe 0.162.0
(win32-x64): it runs install.ps1. The layout was read from
https://chatgpt.com/codex/install.ps1.

Not tried with a real install.ps1 install (it rewrites the user's PATH in
the registry). go test -tags nogui ./internal/agent ./internal/proc
./internal/provider ./internal/library (macOS), gofmt, and go vet for
GOOS=linux and windows pass.
2026-10-09 13:44:50 +08:00
yetone 8e0e7d3c40 dsh: the desktop app's profile made while magpie runs gets magpie's route within a round, not at magpie's next start (star on Discord)
star on Discord reported DeepSeek Harness Desktop with the red
"已不经过 magpie" line. One way there that a desktop user meets: the
CLI's web profile wired, then the desktop app opened for the first time
while magpie runs. dsh makes profiles/desktop/cordis.patch.yml as its
empty template ([]), dshCheck says the desktop profile has none of
magpie's models (Drift "unwired"), and only dshSync — the catalog sync,
at magpie's start or a catalog change — filled new profiles, so it stayed
so until Reapply or a restart. The reporter's exact detail is cut off in
the screenshot, so this is a likely cause, not a confirmed one.

dshWiredOnce, the 30s serving round, now runs dshFillNewProfiles over a
profile that is dsh's empty template, beside the wired ones: magpie's
route and start go in as the catalog sync would put them. A profile with
entries of its own and no route is the user's and is still left alone.

Run: TestDshDesktopProfileMadeWhileServing fails on the old code ("the
round left the desktop profile bare"), passes with the fix, and checks a
user's own unrouted profile is not written. go test -tags nogui
./internal/agent ./internal/gui under a temp HOME with pinned caches: ok;
go test -race -run TestDsh -count=3: ok. gofmt clean; go vet darwin,
GOOS=linux -tags nogui, GOOS=windows; go build darwin, linux nogui,
windows: ok. Not tried with the real desktop app live.
2026-10-09 12:56:08 +08:00
yetone ee3f83b59d agents: DeepSeek Harness Desktop installed is no missing dsh CLI — its version shows instead of 未找到 CLI (star on Discord)
star on Discord: "DeepSeek Harness 就是现在已经有桌面版了,但是他还是非要找CLI".
With DeepSeek Harness Desktop in use and no dsh on PATH, the Agents page
said 未找到 CLI and Install another agent offered npm's dsh: CLIMissing
(#843) took ~/.dsh without a CLI for an uninstall. The desktop app
(DeepSeek Harness.app, com.deepseek.dsh, 0.2.0-rc.2 from
download.deepseek.com) keeps the same ~/.dsh — its own profile,
profiles/desktop/cordis.patch.yml, and .credentials.yaml — and needs no
CLI; its terminal command (Manage dsh Command…) is optional.

- desktopapp.go: dsh's app is known by its bundle id on macOS and by
  %LOCALAPPDATA%\Programs\DeepSeek Harness\DeepSeek Harness.exe on
  Windows (electron-builder per-user NSIS, productName DeepSeek Harness,
  getWindowsInstallationDirName); HasApp says either is installed.
- install.go: CLIMissing is false with the agent's desktop app installed,
  so the GUI row, Install another agent and /api all follow; the app's
  version shows through the existing CLI().App path.
- dsh.go: the restart notice also sees DeepSeek Harness.app running.

The red "已不经过 magpie" line in the report is a separate dshCheck
finding, not caused by the missing CLI: a desktop-only ~/.dsh shaped
from the app's real new-profile template wires cleanly (Check "", no
Drift; TestDshDesktopOnlyWired). The reporter's exact detail is cut off
in the screenshot and is not reproduced.

Run: TestDshDesktopAppIsNoMissingCLI fails on the old code ("with the
desktop app: missing true, offered true"; with the Windows path removed,
"with the Windows app: missing true, offered true"), passes with the
fix. go test -tags nogui ./internal/agent ./internal/gui under a temp
HOME with pinned caches: ok. gofmt -l clean; go vet darwin, GOOS=linux
-tags nogui, GOOS=windows on both packages; go build darwin,
GOOS=linux -tags nogui, GOOS=windows: ok. Not tried: the real desktop
app running live (not installed here; its userData isn't redirected by
a sandbox HOME), a real Windows install.
2026-10-09 12:56:08 +08:00
yetone 4dc7222fdc kimi: each of magpie's models in Kimi Code has its name (display_name) and, where magpie knows it, its most output (max_output_size); a value the user set in the table stays (Sorghum on Discord)
What: kimiModelTables writes display_name (the name magpie shows for
the model, left out when it is just the id) in every [models."magpie/…"]
table, and for the new Kimi Code max_output_size (the catalog's output,
capped at the window by maxTokens; left out when unknown). kimi-cli gets
display_name only: it has no max_output_size. What magpie last wrote in
each table is kept in the stash (kimi:<path>:wrote); a value there that
isn't it is the user's and stays, while magpie's own follows the catalog
and goes when the catalog no longer knows it. The record goes when magpie
steps out.

Why the keys are safe: Kimi Code (MoonshotAI/kimi-code) has had both
maxOutputSize and displayName on its model alias since 0.2.0, its model
record passthrough on 2.x (agent-core-v2 kosongConfig/configSection.ts);
max_output_size becomes the request's max_completion_tokens for the kimi
type. kimi-cli (MoonshotAI/kimi-cli) added display_name to LLMModel in
1.37.0 (0bc7928, #1948); its config never sets pydantic extra, so older
ones ignore the key.

Run: TestKimiNamesAndOutput (new; both config dirs, with hand-set values)
fails three ways with the change broken: user values not kept
(display_name "Big One · Relay", want "My Big"), magpie's stale value kept
(max_output_size 32000, want 64000), neither key written (display_name "").
go test -tags nogui ./internal/agent/... (temp HOME, caches pinned) ok;
gofmt -l clean; go vet + GOOS=linux/windows go vet ./internal/agent ok;
go build, GOOS=linux go build -tags nogui, GOOS=windows go build ok.
Real use in a temp HOME: Kimi Code 2.1.1 (npm) `kimi doctor config` OK on
the written config, and `kimi -p` against a local fake server answered,
its request carrying max_completion_tokens 32000; kimi-cli 1.52.0's own
load_config read display_name and ignored an extra max_output_size.
2026-10-09 12:51:30 +08:00
yetone 9fbdd584ed codex: a thread started on a provider config.toml no longer has opens again, through magpie (#1372, obstrux: "ChatGPT 无法加载 config.toml … Model provider custom not found")
The Codex app resumes a thread on the provider it was started on, kept in
Codex's state database (app-server thread_processor.rs:
merge_persisted_resume_metadata), whatever config.toml's model_provider is
now, and loads no config for it when config.toml has no table of that id
(core config: "Model provider `{id}` not found"). CC Switch puts every
third-party thread on "custom" and rewrites config.toml whole when it
switches, so a switch to its official provider leaves those threads with no
table, and the app can't open them. magpie never removes [model_providers.custom],
but nothing put it back either.

While magpie is wired into Codex (takeTables: set, Join and Sync), each
provider a thread in state_*.sqlite names that config.toml has none of
(table, inline or dotted) gets a table pointed at magpie's gateway, as
CC Switch's own tables already are. Codex's built-in providers (openai,
ollama, lmstudio, amazon-bedrock[-runtime], ollama-chat) and magpie's own
are left out, and a provider the user has stays as it is. The table stays
when Codex steps off magpie, as magpie's own does, for the threads that
name it. Not wired, nothing is written. sessions.CodexThreadProviders reads
the distinct model_provider of the threads table, read-only.

Run:
- Real Codex 0.161 in a sandbox HOME: `codex exec` recorded a thread under
  a [model_providers.custom] pointed at a fake Responses server, the table
  was removed, and the app-server's thread/resume answered "failed to load
  configuration: Model provider `custom` not found" (the reporter's error).
  With this build's `magpie codex fakerelay/m1` (sandbox HOME, gateway on
  127.0.0.1:3546), [model_providers.custom] was written to the gateway, the
  same thread/resume succeeded, and a turn on it completed through magpie to
  the provider (its request reached the fake). The thread kept "custom".
- TestCodexWritesTablesThreadsName (signed in and not) fails without the
  call ("custom not written through magpie"), and its inline case fails
  without the parsed check ("edited TOML is invalid: key inl should be a
  table"); TestCodexThreadTablesOnlyWired.
- gofmt -l, go vet, GOOS=linux/windows go vet -tags nogui on agent and
  sessions, GOOS=linux go build -tags nogui, GOOS=windows go build, and
  go test -tags nogui ./... under a temp HOME. One full run failed
  TestSetAnswersWhileDevinAndProvidersHang (internal/gui, set took 1.6s
  against its 1.5s wall-clock bound; Claude only, Codex not wired, so this
  code isn't on its path); it passes alone 5/5 (0.19-0.42s) and in a rerun
  of gui, agent and sessions together.
2026-10-09 11:55:33 +08:00
yetone 58ba737811 remote magpie: a remote magpie's model prices are this magpie's list prices for its models, so both Usage pages count a call at one cost (Sorghum on Discord: 希望本地 magpie 可以自动集成 remote magpie 手动设置的模型价格)
What changed
- The local magpie asks a remote magpie's /v1/models with a new header,
  X-Magpie-Prices (provider.PricesHeader, beside the Drawers and
  Videomakers headers in listHeaders).
- The remote answers each model's "magpie_price": what its own Usage page
  counts the model at (provider.EntryPriceIn: the price its user set by
  hand, else its provider's list price at its price rate; a routing group
  only when every member costs the same). EntryPriceIn is agent/route.go's
  entryPrice moved into provider unchanged, so the agents' cost and the
  remote's price are one rule.
- It tells prices only to a request from its own computer or one let in
  with a gateway key (local || sharedWith), and only when asked: a machine
  let in without a key (MAGPIE_ADDR open, not shared) and an older magpie
  or an agent that sends no header get nothing new.
- The local keeps the price with the fetched list (catalog.Model.Price; read
  raw, so an odd value or a negative part drops the price and keeps the
  model and the list) and uses it as that provider's list price
  (ListPrice -> remotePrice). A price the local user set (provider/model,
  provider/*, */model) still comes first, and the local provider's own
  price rate still applies on top.
- Old remote <-> new local: no field, models priced as before.
  Old local <-> new remote: no header, nothing new sent (and an unknown
  field would be ignored by encoding/json anyway).
- The model editor's price tooltip and `magpie model price` say the greyed
  price is what the other magpie counts the model at (remoteList; new
  string in zh, zh-TW, ja, de).
- Side effect: smallModel/cheapest picks over a remote's models (image
  description, web search) now see these prices, where every remote model
  was unpriced before.
- docs/subsystems/providers-accounts.md: a row for it.

What was run
- New TestRemoteMagpiePrices (real gateway handler behind lanGuard as the
  remote, a local remote-magpie provider fetching it): the remote's hand-set
  price is the local's price; a real chat call through it costs $9 on both
  the local's and the remote's usage records; the local's own price wins
  and survives a refetch; an old remote's list leaves the model unpriced.
  New TestRemoteMagpiePricesTold: priced for loopback asking, not without
  the header, not for a keyless LAN caller with MAGPIE_ADDR open, priced
  with a gateway key. New TestLiveMagpiePrice in catalog.
- Broken by hand, each red: no remotePrice in ListPrice (FAIL: priced
  false); remote price over the user's (FAIL: remote's price won); the
  local/sharedWith gate removed (FAIL: keyless machine priced); the header
  gate removed (FAIL: told to a list that didn't ask); sane() always true
  (FAIL: a/negative priced). A string-typed magpie_price first failed the
  whole list ("not a model list"), hence the raw read.
- gofmt -l clean; go vet ./..., GOOS=linux go vet -tags nogui ./...,
  GOOS=windows go vet ./...: clean; go build, GOOS=linux -tags nogui and
  GOOS=windows builds OK; go test -tags nogui ./... green under a temp HOME
  with pinned caches.
- GUI: model-price-edit (new remote-title check), model-price-tiers,
  provider-price-rate, gui-ja, gui-de, gui-zh-tw, i18n-keys: 18/18 in
  Chromium and WebKit; the title check goes red with the remoteList branch
  removed.
- Not tried between two real magpie binaries on two computers, nor in the
  real window.
2026-10-09 09:43:54 +08:00
yetone ae24bcfcfb agent, gui: Codex's row on the Agents page shows its desktop app's version beside its CLI's ("CLI 0.130.0", "App 26.930.31730"), and the app's alone where no CLI is on PATH (#1334, laacmilan)
The app's version is read from /Applications or ~/Applications
ChatGPT.app (or Codex.app) Contents/Info.plist, taken only when its bundle
id is com.openai.codex, so the ChatGPT chat app (com.openai.chat) isn't
taken for it. macOS only: the Windows Store package isn't read yet. The
app chip's tooltip says it reads the same settings as the CLI, so
connecting Codex connects both. At narrow widths the chip ellipsizes, and
below 250px it gives way so the name keeps its room.

Verified: TestCodexAppVersion fails without the CLI() change ("the app
alone, no CLI"); agent-app-version.test.cjs fails 12/16 without the app.js
change and passes 16/16 with it, Chromium and WebKit, en/zh/ja/de, at 980,
560 and 440px. agent-cli-missing, agent-connect, agent-field-names,
agent-layout, agent-update-all, cli-update, click-scroll, gui-ja, gui-de,
i18n-keys pass in both engines. internal/agent passes in Linux Docker
(non-root); full ./... on macOS; vet darwin/linux/windows. Read
26.930.51102 from the real /Applications/ChatGPT.app on this Mac. Not
tried on the reporter's machine.
2026-10-09 00:02:40 +08:00
小七 172f45a5e6 agent: dsh's check stays quiet when its key store holds a caller key of this gateway's (#1332) (#1339)
The drift check asked only whether refs.MAGPIE_GATEWAY_KEY equals the key
magpie writes into .env, so a caller key the gateway itself issued — which
dsh's store prefers and the gateway authenticates — was named as "another
MAGPIE_GATEWAY_KEY" and the user was told to remove it there, while
delete → disconnect → re-apply wrote the very same state back. The check
now asks the gateway's own Authenticate first: a key it takes means dsh is
going through magpie, and only a key it would refuse (a foreign one, a
switched-off one) is drift. The .env side needs nothing: the serving round
rewrites .env to magpie's key, so a stray value there heals itself.

TestDshStoreHoldingAGatewayCallerKey (a key from access.Update add-key in
the store's refs) failed without the change with the "holds another
MAGPIE_GATEWAY_KEY" drift; with it the check is quiet, and a key the
gateway doesn't take still reports.

Verified: go test -tags nogui ./internal/agent -count=1; the three dsh
store tests under -race -count=10; gofmt -l clean; go vet -tags nogui
./internal/agent; go build and GOOS=linux/GOOS=windows go build -tags
nogui.

Generated-by: GLM-5.3-Flash (ZCode)
2026-10-08 23:51:34 +08:00
yetoneandClaude 0feffaa7f9 agent: TestFolderVarsAreListed knows MAGPIE_PARTNERS is magpie's own partner-list source, not an agent's folder (red on main since 10578ce6)
Run: go test -tags nogui ./internal/agent under a temp HOME: FAIL before
("MAGPIE_PARTNERS, read by ../provider/partners.go, is not in
agentenv.Vars"), ok after.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-08 23:03:19 +08:00
yetone 1d1c9e07d2 agent: without dsh, the dsh round every 30 s doesn't build magpie's whole catalog for no patch list (H20 on Discord: magpie idles near 500 MB)
dshWiredOnce built magpieModels("dsh") before looking for a profile, so every magpie, dsh or not, built the catalog every 30 s and threw it away: 93% of what a magpie at rest allocated (pprof heap?gc=1 delta over 120 s, a linux nogui `magpie web` in Docker on a copy of a real config with 13.5k Codex and Claude Code sessions mounted read-only). That garbage kept the heap at its GC goal, about twice what it keeps. It now returns before the catalog when there is no profile; with one, nothing changes.

Same measurement, this commit and the warm-up one together: idle allocation 73 MB per 2 min -> 6.8 MB (3.8 MB of it the profile read itself); RSS at rest after the first minutes ~250 MB -> ~203 MB, the heap no longer climbing 125 -> 192 MB between GCs. TestDshRoundWithoutDshBuildsNoCatalog fails without the change (475 allocations a round) and passes with it. go test -tags nogui ./internal/agent (Docker golang:1.26.8, non-root, temp HOME), go vet -tags nogui ./internal/agent for linux, windows and darwin, and gofmt -l pass. Not measured on macOS: a fresh build there hangs at exec behind syspolicyd today.
2026-10-08 21:21:01 +08:00
易良 ae39bc8ec8 agents: Qwen Code (qwen) as a managed agent (#1311)
* agents: Qwen Code (qwen) as a managed agent (#1279)

Qwen Code (QwenLM/qwen-code, a Gemini CLI fork) keeps custom model
providers in ~/.qwen/settings.json modelProviders.<protocol> entries
({baseUrl, envKey, id, name}, envKey naming a variable in the same
file's env object) and its session model in settings.model
{name, baseUrl}.

magpie writes one modelProviders.openai entry per catalog model —
wire id the gateway takes, name magpie/<provider>/<model>, every one
on env.MAGPIE_API_KEY holding the gateway's key, which is how an entry
of magpie's is told from the user's own — and points settings.model at
a pick. Stash, Check/drift, Sync, port-move Rewire and disconnect work
as for the other JSON-config agents; on disconnect the user's entries,
model and key are restored, the file byte-identical to before.

Verified: go build, GOOS=linux nogui + GOOS=windows builds, go vet
./...; go test -tags nogui ./internal/agent (TestQwen* pass;
TestDriftUnwiredEveryAgent/qwen and TestPortMovesEveryAgent pass after
binding drift identity to the entry, not the address), go test -race
-count=3 -run TestQwen, go test . (TestAgentQwenDefault: set then
'default' restores the settings byte-for-byte). TestQwen fails with
'unknown agent' when the registration is reverted. Real use: a copy of
a live 341-line settings.json in a sandbox HOME — wire, then
'qwen default' left it byte-identical. Not run: GUI suite (no
gui/assets change); TestReasonixCLI* and TestPluginHTTP2TakesTheProxy
fail on main too (fixture/network), unchanged by this.

* agents: qwen honors QWEN_HOME, sets auth type, owns a qwen-specific key name, carries context windows (#1279 review)

- $QWEN_HOME stands for ~/.qwen, resolved as the other agents'
  (via place.getenv, appdir.Getenv; a distro's isn't read), and is in
  agentenv.Vars so the sandbox clears it
- wiring sets security.auth.selectedType openai so qwen skips the
  /auth prompt; the user's own (including a pre-existing "openai",
  told from magpie's by a stash marker) is restored on disengage
- envKey is MAGPIE_QWEN_API_KEY: a shell-exported or ~/.qwen/.env
  value wins over the settings' env, so the magpie-generic name is
  not used
- entries carry generationConfig.contextWindowSize from the catalog
- a settings.json without modelProviders reads as no models of anyone,
  instead of gjson's 'cannot use null as iterable'

Verified: TestQwen* incl. TestQwenAuthOpenAIOfTheUsersOwn and
TestQwenHomeOverride, TestAgentQwenDefault (CLI), -race -count=3;
unwired-drift and port-move generic tests pass. Real end-to-end:
dev gateway on 127.0.0.1:3999 in a sandbox HOME, real qwen binary in
a sandbox QWEN_HOME ran a prompt through the gateway (PONG), and
wire → default left the settings byte-identical. Reasonix fixture
tests fail on main too (environment).

* agents: qwen's requests are attributed by their QwenCode/<version> User-Agent
2026-10-08 20:33:11 +08:00
yetone 7611c36e6f agent: wiring Alma also makes the model magpie draws with Alma's image generation model, when Alma has it on Auto (Sorghum on Discord)
Alma has one: Settings › Image Generation, settings.imageGen.model,
"<providerId>:<model>" like chat.defaultModel, "" for Auto (Alma's
src/services/settingsApiService.ts defaults). Its `alma image gen` and
in-chat image requests send an openai-type provider's model to
{baseURL}/images/generations, which the gateway draws with.

Choosing one of magpie's models for Alma now also sets imageGen.model to
magpie's provider and the model magpie draws with (gateway.Drawer: the
Settings' Image generation model, else AutoDrawer's), only when it is
Auto: a model the user picked, theirs or magpie's, is left. Sync leaves
an Auto alone (the user may have gone back to it) and only replaces one of
magpie's that magpie no longer offers. That model is listed on magpie's
provider with capabilityOverrides.imageOutput, as Alma's picker lists only
a provider's models that make images and shows another as "unavailable".
Going back to Alma's own model puts an image model of magpie's back to
Auto with the provider. Nothing changes when magpie draws with nothing.

Verified: TestAlmaImageGen against the fake Alma (settings kept whole,
aspectRatio untouched): Auto filled with p1:img/gpt-image-1 and the model
listed with imageOutput; the user's own pick and a magpie chat model kept
by Apply and Sync; Auto kept by Sync; a stale magpie drawer replaced; Auto
again on going back. It fails without almaSetImage ("image generation:
\"\", want \"p1:img/gpt-image-1\""). TestAlma checks nothing is written
when magpie draws with nothing. Go tests ran on Linux (golang:1.26 in
Docker). Not tried with the real Alma: Alma's settings page reading the
override as an image model, and an image drawn through it, are unverified.
2026-10-08 20:11:05 +08:00
yetone cf10394527 agent: alma-server on Linux is found and wired as Alma, so its requests show as Alma, not "ai-sdk" (Lutra.x on Discord)
alma-server (Alma without a desktop, Linux x64/arm64) keeps its data in
$ALMA_DATA_DIR, else $XDG_DATA_HOME/alma, else ~/.local/share/alma
(Alma's electron/headless/electron-shim.ts getDefaultUserDataPath), and
answers on the same localhost:23001. magpie looked only in the desktop's
folder (os.UserConfigDir()/alma), so on such a machine Alma's Sync never
ran, its magpie provider kept the old shared key, and the gateway read its
requests by their User-Agent alone, the AI SDK's
("ai-sdk/openai/4.0.15 ai-sdk/provider-utils/5.0.10 runtime/node.js/22",
@ai-sdk/openai 4.0.15 and provider-utils 5.0.10 as alma-server bundles
them; Alma sends no header of its own). almaDir now also looks in
alma-server's folders on systems other than macOS and Windows, the
desktop's first; ALMA_DATA_DIR is in agentenv's Vars.

Verified: TestAlmaServerKeyed (a Linux home with only
~/.local/share/alma and a fake Alma whose magpie provider has the old
key): Sync re-keys it to magpie-alma; XDG_DATA_HOME and ALMA_DATA_DIR are
followed, the desktop's folder wins, and macOS/Windows look only in the
desktop's. It fails without the change ("Alma's folder:
.../.config/alma, want alma-server's .../.local/share/alma").
TestAgentOf has alma-server's real User-Agent bytes: Alma with magpie's
Alma key, "ai-sdk" without. Go tests ran on Linux (golang:1.26 in Docker;
this Mac's syspolicyd held every new test binary at launch).
Not tried with a real alma-server, and an alma-server on another
computer, reaching magpie with a gateway key, still reads as the AI SDK:
its User-Agent says nothing of Alma.
2026-10-08 20:11:05 +08:00
yetone 169c23c18e stats: the day's event says which agents, providers and models magpie is used with, by magpie's own ids (a provider of the user's own, and its models, only "custom"), with how many of each and of routing groups: one "magpie uses" event per id in the same PostHog batch, so unique users break down by agent, provider and model. Settings → Privacy → Share the agents, providers and models I use turns it off (on by default; gone while Count me as a user is off); no names, URLs, accounts, keys or usage. go test ./internal/stats (payload, off switch, labels), the Privacy switch in Chromium and WebKit (en, zh), go test -tags nogui ./... in a temp HOME, go vet, GOOS=linux/windows builds pass; read against the real config: 9 agents, 24 providers, 25 groups in 0.75 s 2026-10-08 19:26:13 +08:00
Junhao Bai 78d90571cf agent: each named omp profile is an agent of its own, on Windows and in each WSL distro (#1233)
* agent: each named omp profile is an agent of its own, on Windows and in each WSL distro (#1187)

The Agents tab's omp row only ever wrote the one folder ompDir picks, so a profile started with omp --profile kept the model it had. ~/.omp/profiles/* is now listed per location: omp#<name> on Windows, omp#<name>@wsl:<distro> in a distro, each with the omp row's model, role and thinking controls writing that profile's own config.yml and models.yml. The profile the default row already points at (OMP_PROFILE) is not listed twice, and a stopped distro is not started to list its profiles: the names come from the probe and stay in wsl.json.

* gui: an omp profile's id is encoded in the Agents page's requests (#1187)

omp#<name> went into /api/agents/connect and the rest raw, so a browser read #work as the fragment and connected, disconnected or previewed the default omp row instead. api() now encodes each path segment, which is what agent-models and agents/cli already did for the id alone.

* agent, gui: an omp profile name is one omp takes, and only its # is encoded (#1187)

api() encoded every character of a path, so a query's ? = & became part of the path and Usage, quotas and Sessions 404ed. Only # is encoded now, which is what omp#<name> needs; a query stays a query. ompProfileOK also refuses the Windows device names omp refuses on every platform (aux, nul, com1, lpt9.x, con.work), and work. is checked through that rule rather than by making the folder, which Windows would turn into work.
2026-10-08 19:22:14 +08:00
yetone d2241a5798 agent: Copilot in JetBrains IDEs takes magpie's models as a Bring Your Own Key custom endpoint provider, written into the Copilot language server's byok.json beside the user's own (KamToHung on Discord) 2026-10-08 18:12:10 +08:00
yetone d7bdb0765f agent: with magpie Codex's provider, Codex's own models on the Agents page say what picking them does: one magpie serves on a ChatGPT account is picked as magpie serves it and Codex stays on magpie; any other reads "direct, not via magpie" and asks before taking Codex off magpie (#1269, shenghsi)
Picking Codex's own gpt-5.5 tagged "via magpie" while magpie was Codex's
provider took Codex off magpie at once, with no question: the tag came
from routed(), but set() unroutes for a model of Codex's own unless it is
joined beside a ChatGPT sign-in or on magpie API.

Verified: TestCodexProviderOwnModelSaysItsPath fails without the change
(gpt-a reads Via:true with no ChatGPT account; Value gpt-a, not
codex/gpt-a, with one), passes with it. Reproduced and checked in a
sandbox HOME with Playwright against `magpie web`, on macOS and on Linux
(WSL2 Ubuntu 24.04, codex-cli 0.161.0): before, picking gpt-5.5 left
Codex "Not connected"; after, it reads direct and opens "Take Codex off
magpie?". go test -tags nogui ./... green; go vet for darwin, linux,
windows on internal/agent.
2026-10-08 17:06:48 +08:00
wizzy-yangandwizzy-yang e5e87d980a 支持 Ante:把 magpie 作为 provider 写进 ~/.ante/catalog.json (#1210)
* agent: Ante takes magpie as a provider in ~/.ante/catalog.json

Ante (antigma.ai, AntigmaLabs/ante) is asked for in #1179. Its catalog is
~/.ante/catalog.json ($ANTE_HOME moves the folder), merged on top of the
providers it ships: a providers map keyed by the id --provider takes, each
with base_url, wire_style (AnthropicMessage, OpenAiCompatible, OpenAiResponse
or Gemini), an optional auth, and preferred_models, the ids its model picker
shows (docs.antigma.ai, reference/catalog-reference). magpie adds the entry
"magpie" at the gateway's /v1 on OpenAiCompatible, which is the wire style
Ante speaks Chat Completions with and what the gateway takes every model on,
and lists magpie's models in its preferred_models. The user's own providers
and the models section are left as they are, and magpie's entry goes again
when magpie steps out; a file magpie made goes with it.

No auth block is written. The gateway listens on loopback and lets any token
in from this machine (gateway.Token's own comment, lan.go's identifyCaller:
"Loopback takes any token"), and Ante counts a provider with no auth as
authenticated. A bearer env_key would mean putting the gateway's key into
Ante's environment, which the user would have to do themselves -- the same
decision Empryo's entry makes, which sends no key of the user's either. A
gateway shared on the local network is another matter, and the same one every
agent magpie wires has.

Two things are deliberately left off a model's entry. A routing group is not
told what it reasons at: which member answers, and what that member takes, is
the group's own to decide per turn, and magpieModels sets no APIs for a group
for the same reason. And support_vision is never written: Ante takes a model
as seeing images unless told otherwise, and what magpie knows is only as good
as a provider's own list -- a relay's models are often not described at all,
so writing false would have Ante drop the reader's images before the gateway,
which describes them to a model that can't see them itself
(provider.Described). No other agent magpie wires declares it either.

Ante ships macOS and Linux builds alone and suggests WSL on Windows, so the
Windows machine's Ante is usually the one in a distro: wslKinds gains it
beside Empryo, which also means the gateway address Ante is pointed at is the
one that distro reaches magpie on (place.base), and a distro not known to be
in mirrored networking gets the notice it already gets for the others.

TestAnte covers a user's own providers and models section kept as they are,
magpie's entry added and taken away, Sync repointing it, and a file magpie
made going again. TestAnteKeepsTheUsersFile covers a catalog magpie can't
parse costing magpie's provider and not the user's. TestAnteGroupSaysNoCapab
ilities covers a group declaring neither effort nor support_vision.
TestAnteEfforts covers the ladder: Ante's has no "none" or "minimal", its two
lowest, so both are its "min", and the rungs come out in Ante's order
whatever order magpie lists them in.

Tried with a real Ante 0.2.9 in WSL Ubuntu: magpie wrote the catalog, and
`ante catalog` then read 16 providers with magpie's among them, 39 models,
and Ante filled in support_vision: true for each itself. Ante then ran a real
turn through the gateway — `ante --provider magpie --model
group/auto-deepseek-v4-1-flash -p …` answered, and magpie's own usage log has
the request (ep /v1/chat/completions, req the group's id, served by one of its
members, out 10) — and so did a second model on another provider. The gateway
was reached over the Windows host's address from the distro (172.17.128.1),
this machine's own being on 127.0.0.1 alone, which is what notMirrored tells
the user about.

go vet, gofmt -l and go build (linux nogui, windows) pass. go test -tags
nogui ./internal/agent: 12 failures here, the same 12 on unmodified main (the
Windows ones AGENTS.md lists). ./internal/gui and ./internal/provider:
TestGatewayFixQuitsOnlyAMagpie, TestPurposeAssetMatchesUsage,
TestUpdateThatWontStart and TestLoginUsageClaudeAskedMeanwhile, all four on
unmodified main here too. docs/reference.md's adapter table gains Ante.

* agent: Ante keeps the user's own keys, reads ANTE_HOME, sends no effort it wasn't told to, and starts Ante on magpie (#1210)

Seven points from the review, each with a test that fails without it.

1. ANTE_HOME was in the file's own comment and in docs/reference.md but never read:
the path was fixed to $HOME/.ante, so a user who set it had magpie write a catalog
Ante does not look at. anteDir reads it through place.getenv, the way MiniMax Code
reads MINIMAX_DATA_DIR, so a distro's own ANTE_HOME - which magpie can't read -
still means ~/.ante there. "ANTE_HOME" joins agentenv.Vars so a sandbox clears it.

2. providers.magpie was written whole, so every key of the user's that magpie
doesn't write - the auth block a gateway shared on the LAN or reached from WSL
needs to send a key, http_headers, extra_body, stream_idle_timeout_secs,
thinking_display - went at the next sync, and an unchanged catalog was rewritten
every round. It goes through theirsKept and syncJSON now, as #1103 did for the
others: the user's keys in magpie's entry stay, preferred_models is magpie's list
alone, and a file that says the same as it does is left as it is.

3. The two files were written in separate steps, so a change that failed halfway
left Ante's catalog without magpie's entry and its settings still naming magpie.
Each field's Set and its Sync are wrapped in atomic(), as empryo, minimax and kimi
do.

4. The comment said Ante's "min" was the rung it sends no effort at, and folded
both magpie's "none" and its "minimal" into it. Ante's reference says what min is:
"OpenAI-compatible providers send the selected level as the same-named
reasoning_effort (min is sent as minimal)" - so min is a sent level, and magpie's
"none" has no rung of Ante's to fold into. Ante's "min" is magpie's "minimal"; a
model magpie knows only "none" of comes out as the empty list, which is Ante's own
way of saying it takes no effort setting at all.

5. The default was the highest rung the model takes, so every model magpie listed
ran at Ante's slowest and dearest level. No effort is written: "Omit for models
with no reasoning knob (Ante then sends no effort parameter and hides the effort
slider)".

6. Having magpie in the catalog never meant Ante used it. What a session starts on
is the provider and the model of ~/.ante/settings.json; with neither set Ante
"uses the first authenticated provider in catalog order", and Ante's own presets
come before magpie's entry - so while the user has any of those signed in, that is
the one picked and magpie is not. (Measured: nothing named in settings, two keyless
providers ordered aaa_other then magpie, and the request went to aaa_other's port;
settings provider=magpie and it went to magpie's.) magpie sets that pair now and
saves what it found there, so Disconnect hands it back; a provider the user picks in
Ante's own /providers while magpie is wired is theirs and stands, and Check says so
instead of keeping quiet. Ante's settings name the model apart from the provider, so
magpie writes the catalog id it put in preferred_models, un-prefixed, and reads a
bare id as magpie's only beside magpie's provider.

Tests lean on the package's own testenv, not a real ~/.ante.
TestAnteSyncKeepsTheUsersKeys is modelled on ad3b7fe5's
TestAirProviderWritesKeepCustomFields and goes both ways - sync and pick - and
wants an unchanged file to keep its mtime.

Tried with a real Ante 0.2.9 in WSL Ubuntu on this machine (Ante ships macOS and
Linux builds alone; there is no Windows Ante here). The strongest of these is the
whole of it: magpie's own Go code - ante(home).Connect(), then the model field -
wrote a catalog.json and settings.json into a temp folder, that folder was handed
to the real Ante as ANTE_HOME, and Ante ran a turn through it with no --provider,
no --model and nothing typed by hand.

- `ante doctor` read `home ok /tmp/ante-e2e` and `16 providers (1 authed), 108
  models` from ANTE_HOME, and magpie's entry came back with the base_url it was
  given: ANTE_HOME really moves the folder, and a base_url already ending in /v1
  is not doubled (the request went to /v1/chat/completions).
- What a session starts on is settings.json's provider, measured against two
  keyless providers each answering on its own port, so the logged port names the
  one Ante asked. Catalog ordered aaa_other then magpie, nothing named in
  settings: Ante asked 44331, aaa_other - the first authenticated in catalog
  order, exactly the review's "很可能选不到 magpie". settings provider=magpie: it
  asked 44220, magpie. settings provider=aaa_other: 44331 again, so a pick the
  user makes in Ante's own /providers overrules what magpie wrote, which is why
  stepping out leaves it rather than handing back the one magpie found. And with
  magpie the only authenticated provider, auto-detection does reach it - so the
  catalog entry is not useless, it is simply not enough while the user has any
  other provider signed in, which is every real install.
- What Ante sends for effort, from the same files: no reasoning_effort at all for
  a model with no effort written (item 5, as its reference says omitting it sends
  none); `--effort min` on a model sent `reasoning_effort: "minimal"` - Ante's min
  is magpie's minimal, exactly item 4; and a model Ante was told
  `"supported_efforts": []` for sent no reasoning_effort even when asked `--effort
  low`, which is what a none-only model now gets instead of a level that refuses
  it.
- The user's own keys inside magpie's entry are ones Ante reads, not decoration:
  `ante catalog` hands back the entry's auth, http_headers, extra_body,
  stream_idle_timeout_secs and thinking_display unchanged, and a bearer env_key
  there makes `ante doctor` count the provider as the one authed. That is item 2.
- The no-auth decision holds on Ante's side, isolated by doctor's counts: Ante's
  own presets alone read `15 providers (0 authed)`; a magpie with no auth block
  reads `(1 authed)`; the same magpie given a bearer env_key that isn't set drops
  back to `(0 authed)`; with the key set, `(1 authed)`. So an entry with no auth is
  authenticated and one that asks for a key the user doesn't have is not - writing
  an env_key, which the first version's comment considered, would have made magpie
  the one provider Ante refuses to auto-detect.
- Ante fills `support_vision: true` in for itself, as the PR's own note said, and
  a routing group declared with no capabilities at all still worked.

Not covered by that run: the stand-in server answered inside the distro, so this
proves Ante's config handling end to end, not a live magpie gateway relay (the
first commit's message records a real turn through one). Every probe used
ANTE_HOME into a temp folder; this machine's real ~/.ante gained no catalog.json
or settings.json.

What each fix failed without it, one test each: taking ANTE_HOME out ->
TestAnteHome "$ANTE_HOME ignored"; theirsKept out -> "the user's auth lost or
changed"; syncJSON out -> "an unchanged catalog was rewritten"; atomic out ->
"settings.json kept half of a change that failed"; the old min mapping ->
"anteEfforts([minimal high]) = [high], want [min high]"; a written default ->
"deepseek/pro: effort written"; settings never pointed at magpie -> TestAnte; a
model never written -> TestAnteModelField; Check left quiet -> "moved off magpie:
\"\""; handing magpie back over a pick the user made while wired ->
TestAnteKeepsWhatTheUserPickedWhileWired; never restoring the model ->
TestAnteConnectDisconnect "not put back"; Connect picking the model field instead
of magpie as provider -> "Connect left the user's model beside magpie's provider";
ANTE_HOME kept out of agentenv.Vars -> TestFolderVarsAreListed. Taking
preferred_models out of the whole list is the one mutation no test turns red on:
mergeTheirs cannot merge a JSON array (sync.go returns want for it), so the file
behaves the same either way and the argument is there for intent and for the other
keys, not for behaviour.

go vet ./... and go build ./... pass, as do GOOS=linux go build -tags nogui ./...
and GOOS=windows go build ./..., and gofmt is clean on LF content (this clone has
core.autocrlf and no .gitattributes, so gofmt -l reports every file).
go test -tags nogui ./internal/agent fails twelve tests, and an untouched worktree
of origin/main run the same command on this machine fails the same twelve, test for
test; ./internal/agentenv, ./internal/appdir and ./internal/edit pass, and every
Ante test passes at -count=5. The whole ./... was run on both: the failing sets are
alike, origin/main having one more, TestLoginUsageClaudeAskedMeanwhile, already
filed as #1228. None of those twelve is this change's, and none is fixed here.

* docs: agent-wiring records that Ante keeps the user's keys in magpie's entry and starts on settings.json's provider (#1210)

The two rules the review asked for, written where the repo keeps them - the same lines ad3b7fe5 (#1239) added for Air and 2d9db1b6 (#1242) for malformed configs. The Verification block gains the package's own Ante run, as #1239 added Air's.

---------

Co-authored-by: wizzy-yang <wizzy-yang@users.noreply.github.com>
2026-10-08 16:58:41 +08:00
yetone 1b29cb81a2 agent, wslrun, sessions, gui: Settings → General has Detect agents in WSL; off, magpie asks wsl.exe nothing and shows no WSL agents or sessions (#1264)
On Windows magpie lists the running distros and probes each one for agents,
their sessions and Claude Code. Etsuya233 asked for a way to turn that off.
settings.NoWSLAgents (per machine, kept by KeepOwn) now gates every
auto-discovery path: agent.wslAgents/wslHomes/WSLRunning, sessions'
wslFiles/wslDirs (via the new sessions.WSLOff hook), and wslrun.Find, which
the Claude subscription uses to run Claude Code in WSL. The toggle is shown on
Windows only and is on by default.

Verified:
- red without the change: TestWSLDetectionOff (1 WSL agent with detection
  off; with only sessions reverted, a distro's session is listed),
  TestFindWithDetectionOff (Find returned Ubuntu after asking wsl.exe).
- go test -tags nogui ./... in a sandbox HOME: every package ok except
  TestPluginLoginTakesTheLapseOff, which timed out at 63.7s under the full run
  and passes alone (-count=3) and with its package (-count=1); it doesn't
  reach WSL.
- go vet -tags nogui on agent, wslrun, sessions, settings and gui for darwin,
  linux and windows.
- GUI: wsl-agents-setting (en/zh/zh-TW/ja/de, 900 and 440px) 30/30;
  gui-ja, gui-de, gui-zh-tw, i18n-keys, keep-awake, lightweight,
  settings-sections, settings-groups 44/44 in Chromium and 44/44 in WebKit.
- Windows box (10.0.0.132), nogui magpie.exe in a sandbox profile with a
  logging wsl.exe first on PATH: on, `magpie agents` listed Claude Code · WSL
  magpie-desktop after 3 wsl.exe calls; off, `agents`, `ls` and `sessions`
  made no wsl.exe call and wrote no wsl.json.

Not tried: the toggle in the real Windows GUI window, and Claude Code in WSL
through the Claude subscription with the setting off (unit-tested only).
2026-10-08 16:57:39 +08:00
yetone 811681bc72 agent: on a Claude model that is no Fable, Claude Code's fable tier is left unset instead of following the main model, so Opus is no longer taken for Fable (Zhenzhen on Discord)
`magpie claude claude/claude-opus-5-5` wrote ANTHROPIC_DEFAULT_FABLE_MODEL=
claude/claude-opus-5-5[1m]. Claude Code 2.1.293 takes a model id equal to
that variable for a Fable model (Sye/Vle in its bundle), so on a Claude Pro
sign-in without Fable it asked for usage credits and fell back to Haiku.

follow() now gives the fable tier no model on a Claude model that isn't a
Fable; writeTiers leaves such a tier out of the env. On a Fable main model,
and on another vendor's model (Claude Code knows nothing of it), the fable
tier follows as before. Claude Code's own /model fable still runs on the
main model at the gateway (claudeStandInAt, asserted in the test).

Kept for the user: a fable pick of their own (even the main model) stays;
a fable effort they set keeps the tier on the main model at that effort;
a non-magpie value they wrote while routed stays as written; their own
pre-magpie value is still stashed and given back on `model default`.
Follow takes out a fable tier an older magpie wrote on a Claude model.

Verified: TestClaudeFableTierOnClaude fails on the old claude.go with
"on Opus: fable tier written as \"anth/claude-opus-5-5\", want it left
out"; breaking the Follow migration, the user-value keep and the effort
fallback each fails it too. go test -tags nogui ./... green under a temp
HOME with pinned caches; go vet + GOOS=linux/windows go vet of
internal/agent; go build, GOOS=linux -tags nogui and GOOS=windows builds.
Not tried with a real Claude Pro account.
2026-10-08 14:55:56 +08:00
yetone 5d0c1dcad7 agent, library, sessions: a Claude Desktop installed as an MSIX package on Windows is found, wired, given MCP servers and skills, and its Cowork calls read, in the package's folders (Kilig on Discord, Windows 10 LTSC 21H2)
Desktop from its setup.exe there runs packaged, so Windows keeps what it
writes under %APPDATA% and %LOCALAPPDATA% in
%LOCALAPPDATA%\Packages\Claude_<publisher>\LocalCache\{Roaming,Local}.
Nothing was in %APPDATA%\Claude or %LOCALAPPDATA%\Claude*, so magpie didn't
recognise Desktop at all. The packaged app reads its own copy before the
real folder's, so writing the usual folders wouldn't reach it either.

New internal/desktopdir is the one resolver for Desktop's folders: Data
(its own userData: %APPDATA%\Claude, the package's LocalCache\Roaming\Claude),
Mode (the Claude folder deploymentMode goes into, %LOCALAPPDATA%\Claude as
before) and ThreeP (Claude-3p). The package is found by globbing
Packages\Claude_* (the publisher id isn't assumed) and used unless Desktop
runs in the usual folders: it has run there (Chromium's "Local State"; an
%APPDATA%\Claude holding only another tool's claude_desktop_config.json
doesn't count) and not later in the package's. Every place that built
these paths goes through it:

- agent: desktopDirs (its 3p switch's writes, DesktopConfig3p, Claude
  Code's desktop check in claude.go) and detection, which also looks at
  Data (replacing the separate %APPDATA%\Claude check). windowsDesktopDir
  moved into desktopdir unchanged.
- library: apps() (Desktop's MCP file, was os.UserConfigDir) and
  desktopSkillRoots (Cowork's skills-plugin).
- sessions: desktopDataDirs, a copy of agent's resolver, now desktopdir's
  All(); it also adds %APPDATA%\Claude on a usual Windows install, where
  Desktop keeps 1p Cowork sessions (only %LOCALAPPDATA%'s were read). usage's
  request_page and session_provider read it through sessions.DesktopDataDirs.

Tests: TestFindMSIX/TestFindWindowsClassic/TestFindOthers (desktopdir),
TestClaudeDesktopMSIX (agent: detected, switch writes LocalCache\Local\Claude
and Claude-3p, nothing in the usual folders), TestClaudeDesktopMSIXLibrary
(MCP file and skills-plugin in LocalCache\Roaming\Claude),
TestCallsCoworkMSIX (sessions). With the call sites put back to origin/main
the last three fail: "an MSIX Claude Desktop isn't detected", "no Claude
Desktop target: <nil>", "dirs: [.../Claude .../Claude-3p]".

Run: go test -tags nogui ./... under a temp HOME with pinned caches (all
ok); GOOS=darwin|linux|windows go vet -tags nogui on desktopdir, agent,
library, sessions, usage; GOOS=windows go build, GOOS=linux go build -tags
nogui. On the Windows 11 box (Squirrel install, no MSIX package): the
windows test binaries pass every Desktop/MSIX/Cowork test; the full agent
and sessions binaries fail the same 16 and 76 tests (run outside their
testdata) as origin/main's; a read-only probe of desktopdir.Here() gives
the usual %APPDATA%\Claude, %LOCALAPPDATA%\Claude and Claude-3p, Package "".
Not tried against a real MSIX Desktop: none on the box, and Claude-3p's
place in the package (LocalCache\Local, as Local\Claude is in Kilig's
listing) is inferred, not seen.
2026-10-08 12:47:48 +08:00
Alex 2d9db1b60d edit: malformed JSON configs stay untouched when agents connect (#1242)
Validate the complete comment-stripped document before object and array edits, including previews. Regression tests fail on the old implementation because edits and VS Code Connect succeed and alter malformed files. Targeted edit and VS Code tests pass in an isolated HOME.
2026-10-08 11:52:20 +08:00
Kenoia ad3b7fe5c2 agents: keep Air provider fields on sync and model picks (#1239) 2026-10-08 11:20:35 +08:00
yetone 6d7b574b5a agents: a Codex another app ships and runs as its app-server is named by that app, so the row says to reopen Agents Anywhere, not a terminal (miaopasi on Discord: the copy left was Agents Anywhere's connector's codex app-server) 2026-10-08 11:09:37 +08:00
yetone 2f74a963d7 agents: a Codex left on the old list is named, with how to reopen it — the Codex app (⌘Q: closing its window keeps it running on macOS), an editor's Codex, the CLI's background app-server, a terminal's codex (miaopasi on Discord: "已接入 · 重开 Codex 后生效" stayed after restarting Codex)
Agent.Stale counted every process matching (^|/)codex( |$) that started
before magpie's last change, and the row only said "reopen Codex". Several
of those don't end when Codex is "reopened": ChatGPT.app's main process
registers window-all-closed so a packaged app on darwin doesn't quit
(read from /Applications/ChatGPT.app/Contents/Resources/app.asar), so
closing the window leaves its app-server (CodexCLI.app/.../codex
app-server) running; an editor extension's codex app-server, the CLI's
--managed-daemon (ppid 1) and a codex in another terminal run on through
a reopened app. One app also counted twice: its app-server and its
`codex exec-server --remote` both match (this Mac's ps).

Agent.StaleCopies gives each copy with its kind (app / ide / daemon /
cli, from the command line) and start, one app's processes as one copy;
Stale is its length. /api/state carries staleCopies, and the opened row
under "N running Codex still have the old list" says, per copy, what it is,
when it started and how to end it: ⌘Q for the app, the editor's window
reload, `codex app-server daemon restart` for the daemon, quit for a
terminal's. Strings in zh, zh-TW, ja, de.

TestCodexStaleCopiesSayWhichIsLeft (an app with its exec-server, VS
Code's Codex, this home's daemon, an old and a new terminal codex: app,
ide, daemon, cli; Stale 4) fails on the old logic (5 copies, no kinds).
agent-stale-copies.test.cjs (Chromium and WebKit, en/zh/zh-TW/ja/de,
980 and 440px) fails on the old app.js (no such lines).

Run: go vet -tags nogui (darwin, GOOS=linux, GOOS=windows) ./internal/agent
./internal/gui; go test -tags nogui ./internal/agent ./internal/gui (temp
HOME); node --test internal/gui/tests/agent-*.test.cjs gui-ja gui-de
gui-zh-tw i18n-keys codex-model-order: 202 pass, both engines. Not tried
with the reporter's Codex: which copy is left on their Mac isn't known.
2026-10-08 10:40:45 +08:00
yetone eeee883f11 agent, wslrun: an agent in a WSL distro under consomme networking is pointed at 127.0.0.1, not the distro's default route (#1230: Codex in WSL 3.0.2 consomme written with 198.18.0.2)
Consomme (WSL 2.9's name for virtioproxy) relays the distro's 127.0.0.1 to
Windows' own while localhostForwarding is on (microsoft/WSL
ConsommeTests::LoopbackGuestToHost); its default route is Windows' next
hop, a TUN proxy's 198.18.0.2 for the reporter, never Windows.
wslrun.HostLoopback names the modes where 127.0.0.1 reaches Windows
(mirrored, consomme, virtioproxy); every agent magpie writes into a
distro (d.base), Claude Code's stand-in, and wslrun's proxy rewrite follow
it. An unknown mode, or a WSL without wslinfo whose .wslconfig says
Consomme, is not taken for one. An agent an older magpie left on the
route is offered 127.0.0.1 (wslMovedFrom); localhostForwarding=false
under consomme gets a notice.
2026-10-08 09:54:11 +08:00
yetone ca387c3ce3 agent: a Claude Code tier's _MODEL_NAME, _DESCRIPTION and _SUPPORTED_CAPABILITIES go with its model: the user's own, written for the model before magpie, are kept and given back when magpie steps out, and one written for a model of magpie's stays while the tier is on it (#1227: every tier on opencode-go/deepseek-flash, shown in /model as grok-4.5)
Claude Code 2.1.293 shows ANTHROPIC_DEFAULT_<TIER>_MODEL_NAME and
_DESCRIPTION as the tier's label and description in /model, and takes
_SUPPORTED_CAPABILITIES as what the tier's model can do. magpie never
touched them, so the user's own, written for their relay's model, stayed
on magpie's. A Claude Code an older magpie routed is tidied on the next
catalog sync, its names kept to give back.
2026-10-08 09:11:41 +08:00
forever ac142bb1d2 agent, gateway: Claude Code through magpie can keep its claude.ai sign-in (#1218)
* gateway: Claude Code signed in to claude.ai is told a provider's 401 as a 502, not as its own sign-in refused

Claude Code signed in to claude.ai, its base URL magpie's and no key set,
sends its own sign-in as its key (Authorization: Bearer sk-ant-oat…),
which magpie never passes on. A 401 it reads as that sign-in refused: it
renews the sign-in before each of its retries (ten for one request) and
then asks to /login; a 403 saying "OAuth token has been revoked" the
same. Whatever magpie answers such a request with is never about that
sign-in: a provider refused magpie's credential, or one of magpie's own
Claude accounts lapsed (the bridge's 401 or revoked 403). keepsSignIn,
on /v1/messages, /messages and /v1/messages/count_tokens, tells such a
request's 401 as a 502, and a 403 too when its body says the token was
revoked; the body is the provider's message as it was. Recent calls and
the usage log keep the provider's status. Any other 403, and a request
with magpie's key or an API key, is answered as before; no
x-should-retry is added, so Claude Code retries a 502 as it does a 5xx.

Verified: TestClaudeSignInNotToldItsSignInFailed (401 on /v1/messages,
/messages and count_tokens; revoked 403; another 403; magpie's key; an
API key; a reply and a stream untouched; Recent calls' status) fails
without the route change ("401: 401 … want 502", "revoked: 403 … want
502", "401 counting: 401 … want 502"); TestSignInWriterReleasesA403. go
test -tags nogui ./internal/gateway, go vet and gofmt are clean.

* gateway: a provider never gets the oauth beta of Claude Code's claude.ai sign-in

Claude Code signed in to claude.ai asks oauth-2025-04-20 in
anthropic-beta with its sign-in. magpie relayed the agent's betas to
every Anthropic-protocol provider, so that one went too, naming a
credential that never leaves magpie: a provider got a request claiming
an OAuth sign-in it was not sent, beside magpie's key for it. askedBetas
leaves the agent's oauth- betas out of what forwardOnce asks, so a
provider gets the request as Claude Code with magpie's key sends it. A
beta the user set on the provider (header.anthropic-beta, fitUserBetas)
still goes, oauth- or not.

Verified: TestSignInBetaStaysWithMagpie (the oauth beta among others,
alone, and the user's own on a provider) fails without the forwardOnce
change (sent ["claude-code-20250219,oauth-2025-04-20,…"], and
["oauth-2025-04-20"] where none was wanted); TestUserBetas* and
TestClaudeCodeHeadersReachAnthropicRelay pass. go test -tags nogui
./internal/gateway, go vet and gofmt are clean.

* agent, gui: Claude Code through magpie can keep its claude.ai sign-in

Claude Code's new sign-in field is magpie's key (the default) or claude.ai.
claude.ai writes ANTHROPIC_AUTH_TOKEN empty, which Claude Code reads as no
key: it stays signed in to claude.ai (/usage plan limits, connectors, voice,
/teleport) and sends that sign-in to magpie, which never passes it on. Every
later write (a model, a tier, the subagents, a /model pick followed, Reapply,
a port move, Disconnect and Connect) keeps it so, and a key put there by hand
is reported by Check rather than taken for magpie's. A token emptied or taken
out by hand in key mode is reported with the way to keep it so.

It is offered only where the gateway takes any key (loopback, a WSL distro
mirrored), not from a distro under NAT while magpie is shared on the network.
The GUI draws no square for a Claude Code with no other sign-in to take.

Verified: go test -tags nogui ./internal/agent (new claude_login_test.go,
each test fails with its change reverted) and -run 'SignIn|Claude|Beta'
./internal/gateway; go vet; GOOS=linux/windows builds; claude-signin.test.cjs
(fails without the app.js change) plus i18n-keys, agent-field-names,
agent-layout, claude-direct, claude-effort, codex-signin-always,
subagent-effort and menu-scroll, Chromium and WebKit, en/zh/ja/de, 1100 and
440px. Real Claude Code 2.1.292 with the token empty sent Bearer sk-ant-oat
and the oauth beta to the base URL, took a 502 as a server error, and stayed
signed in to claude.ai.

* agent: Claude Code given a magpie model at an older gateway address keeps the endpoint it had before magpie

When the gateway moved while magpie wasn't running to follow it, a magpie
model picked next found Claude Code at the older address and stashed what
was there as the user's own: the dead gateway URL, magpie's key or the
empty one of a claude.ai sign-in kept, and magpie's own tier models. That
overwrote the endpoint and token stashed when magpie was first wired in, so
switching off put the dead address back, and with the claude.ai sign-in
kept "model default" left it and five magpie tiers too.

What's there at an older address is now taken for magpie's (keyOurs: its
key, or an empty token with the claude.ai sign-in kept), and the stash made
when magpie was wired in stays. The same test replaces "model default"'s
guess from the model's name, which could take out a user's own endpoint
with an empty token.

Verified: TestClaudeAtAnOlderGatewayAddress (both sign-ins, Disconnect and
model default; fails on the previous commit, the key-mode case on main
too); go test -tags nogui ./internal/agent; -race -count=5 on the Claude
and WSL Claude tests; go vet; GOOS=windows build.

* gateway: Claude Code signed in to claude.ai is told a provider's refusal in magpie's words, never mentioning x-api-key

Claude Code 2.1.293 tells any error whose message mentions x-api-key as
"Not logged in · Please run /login", whatever its status, so the 502 alone
still signed it out (yetone on #1218). A 401, or a 403 saying the OAuth
token was revoked, now reaches it as an api_error "<provider> refused
magpie's credential (HTTP 401): invalid API key", and a stream's error
after its 200 says "API key" too. Recent calls keep the provider's status
and message.

Checked with Claude Code 2.1.292 signed in to claude.ai, through the
gateway to a provider answering 401 invalid x-api-key: it shows "API
Error: 502 Fake refused magpie's credential (HTTP 401): invalid API key.
This is a server-side issue" and stays signed in.

* agent: an empty token at an endpoint of the user's own stays theirs, though Claude Code once kept its claude.ai sign-in through magpie

keyOurs took any empty ANTHROPIC_AUTH_TOKEN for magpie's once the sign-in
was kept, so after Disconnect a user's own https://corp.example with an
empty token lost both to the model default (yetone on #1218). The empty
token is magpie's only beside the address magpie last wrote, stashed as
claude.wired_at, which Rewire carries to the gateway's new port.

* gateway: no error Claude Code signed in to claude.ai is told mentions x-api-key, whatever its status

Claude Code reads any error naming x-api-key as its own sign-in refused,
so a provider's 403 "invalid x-api-key" or 400 "x-api-key header is
required" still told it "Not logged in · Please run /login" (yetone on
#1218, Claude Code 2.1.293). Every error such a request is told is held
until its body and says "API key" there, at the status it had; a 401 and
a revoked 403 stay the 502 in magpie's words.
2026-10-08 08:58:21 +08:00
TryWorld d5cf133b47 agent tests: a fake omp a Windows PATH lookup answers to, and the distro probe is skipped where a Windows shell calls the temp folder a drive (#1180)
TestOmpDetectedWithPiDir plants a #! script under the bare name omp, which
is the one command a Unix PATH answers to. Windows answers to a name with a
PATHEXT extension, so Detected() found nothing there and the check failed
with "omp's command is not". The fake is now planted as omp.cmd on Windows;
the test never runs it, it only asks PATH to find it.

TestWSLProbeScriptSaysWhere runs the distro probe under sh, which on a
Windows box is Git Bash's own. A Windows shell mounts the folder it is
given (/tmp, and / and /bin) as drives of its own, so the probe reports
them as win: lines, and a program on a drive is not the distro's -- which
is right for the product and not what this check is about. It is skipped
there, as it already was with no sh.

Both were the last of the internal/agent checks a Windows host runs red;
the rest are covered by #1069 and #1080, which take the platform facts and
the shared %APPDATA% sandbox. CI never runs either: the Windows job filters
./internal/agent to ^Test(Dsh|KeepDsh) and ^TestVSCodeDetected$.
2026-10-08 07:31:15 +08:00