Commit Graph
1487 Commits
Author SHA1 Message Date
yetone be832ff01b gui: pi-default-note's agents are connected, so their row pickers list every choice — #709's test was written against the row before 「接入」, where the model field stood in the row; now the model a connected agent starts on is picked beside its switch, and Default there says Pi picks the model itself, as before 2026-10-04 02:14:54 +08:00
yetone df27f0882a agent: the 接入 switch and a model picked in the row, both (owner: 对于那种想在 magpie 中一键设置 Agent 模型的用户能否也兼容一下,鱼和熊掌都要兼得); a row opens in place and slides as on iOS (owner: 展开的时候,上/下 padding 变了,导致抖动;展开和折叠都没有 iOS 那种丝滑优雅精致的动画)
Every agent row keeps its 接入 switch and now has its model picker beside it. Picking a magpie model on an agent that isn't connected connects it first (agent.Pick, used by /api/set), so one pick sets the agent up; disconnecting still puts back what the agent had. The intro line says both ways.

An opened row shook: the connected row centred its lines in its 56px min-height, and the 3px row-gap couldn't be folded away, so opening moved the name and the switch and changed the space under them. The first line now has its own height and isn't centred, the row-gap is 0 with the spacing in margins, and the opened part starts where the shut row ended (56/56 at 980px, 90/90 at 560px; the name at 8px and the switch at 19px open and shut). Opening and closing animate the part's height, padding, margin and border on iOS's sheet curve cubic-bezier(.32,.72,0,1), 460 ms open and 340 ms shut, and what's in it fades in from 6px above; a row already open slides shut before another opens. With reduced motion it opens at once.

Gemini CLI's .env made by magpie for its key was left holding a newline after a disconnect (main too): DelEnvFile now removes a dotenv file once the deleted keys were all it held.

TestPickConnectsFirst, the only.env case in TestEnvFile, agent-start-pick and agent-expand-steady (name and switch unmoved, the opened part's top at the shut row's end, at least 3 heights between on the way open and shut, growing/shrinking only, chromium and webkit at 980 and 560px) fail on the old code and pass. Tests that open rows wait for the slide. In a sandbox HOME with real accounts: Claude Code and Gemini CLI picked while not connected answer PONG and disconnect back to what they had; Goose's pick → disconnect round trip is byte-identical (goose itself stops at a Linear MCP OAuth, so no request). go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui for edit, agent and gui under a temp HOME pass; GUI tests that fail here fail the same on 82340f41 (account-arrange, cli-update, mobile-web, provider-levels-scroll, provider-proxy, redact-rule-row, routing-compact, provider-detect-models, plugin-move).
2026-10-04 02:05:10 +08:00
yetone f3f055001b agent: a disconnect leaves the user's own Claude effort, and a YAML table emptied by one is filled in block style when switched on again — found on the omarchy VM (Linux): with the connection made by a magpie before this branch (no <id>.connect.was), disconnecting Claude Code took magpie's CLAUDE_CODE_EFFORT_LEVEL max out of the env and with it the effortLevel xhigh under it, the user's own, so Claude Code came back on its default; and Hermes's providers: {} / model: {} and omp's modelRoles: {} left by a disconnect were filled on one line (providers: {magpie: {...}}) by the next connect. Claude's Unwire now takes the env's level away alone when it is the effort magpie applied; setYAML writes into an empty flow table in block style (a flow table that holds something stays as it is). TestClaudeDisconnectKeepsTheUsersEffort and TestSetYAMLFillsAnEmptyTableInBlocks fail on the old code and pass. On the VM in a sandbox HOME (magpie web, nogui, linux/amd64): claude, codex, gemini, opencode, pi, crush, omp, hermes, muse and grok connect, disconnect and connect again twice with the right state, every disconnect preview line is in the real change, a round trip leaves Claude's effortLevel, Hermes and omp as they were; all ten CLIs answer PONG through it (opencode, pi and omp on DeepSeek: the VM's Zhipu and MiniMax keys are refused upstream). go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. 2026-10-04 02:05:10 +08:00
yetone 42a2b1113a agent: switched off and on again, an agent is back on the magpie models it was on, and Goose keeps its own model through a round trip — found running connect/disconnect twice for every agent against real configs in a sandbox HOME: turning one on again picked magpie's default instead of what the user had picked (OpenCode's model and small_model, Codex's model and effort, Pi, dsh, Hermes, Grok), and Goose lost GOOSE_PROVIDER/GOOSE_MODEL (its field clears both keys; on main too). Disconnect now stashes what it took off (<id>.reconnect: the values, which were routed through magpie, and what it left), and Connect puts them back when the agent is still as Disconnect left it — models on magpie first, so Gemini's model signs it in before its provider, and a value the model brought along (Codex's effort) is recorded as magpie's; changed since, Connect picks as before. Connect stashes what the agent was on (<id>.connect.was), and Disconnect sets back a field it left empty when that was the user's own (Goose's anthropic/claude-opus-5-5). TestReconnectKeepsTheLastPick, TestGooseRoundTripKeepsItsOwnModel and the new reconnect check in TestGeminiConnectRoundTrip fail on the old code and pass. Sandbox matrix: all 20 agents with a switch connect, disconnect and reconnect twice with the right state, every disconnect preview line is in the real change; 13 CLIs (claude, codex, gemini, opencode, pi, goose, crush, droid, grok, cline, omp, agy, commandcode) answer through the sandbox magpie. 2026-10-04 02:05:10 +08:00
yetone 661d27b560 agent: the disconnect preview right on Windows, and the Codex panel says what connecting does (owner: 去 Windows 测试机上测一下 / 是不是需要界面上解释一下现在的逻辑) — on the Windows test box Claude Code's and OpenCode's disconnect previews were empty though both were connected. Claude: the dry run's temporary home has no ~/.codex/auth.json, so a codex/… model wasn't magpie's there; the magpie asking now writes the providers it has on into that home and the dry run takes their models as magpie's (TestDryRunKnowsTheAskersProviders). OpenCode: magpie/codex/gpt-6.1-sol, a model its copy of magpie's list didn't have yet, read as not connected and Disconnect left it on magpie; a magpie/… value whose model magpie routes is magpie's (TestOpenCodeOnAModelNotInItsList). The preview pairs a line going back only with one as deep (Claude's model, not a modelPicker row) and doesn't mask …_TOKENS counts. The Codex panel, joined: 「重开 Codex 后,/model 里自己的模型照旧,magpie 的模型也在里面。」, and 新会话默认 says 「和 Codex 的 /model 是同一个选择,在哪边改都行」 rather than 「不设就用 Codex 自己上次的选择」 beside a value (/api/state: joined). On Windows (sandbox USERPROFILE/APPDATA, real accounts): previews for Claude, OpenCode, Codex and Gemini; disconnecting all four restored them (Claude back to opus, Gemini to Google sign-in, Codex keeps its [model_providers.magpie] by design), real ~/.claude untouched; real Claude Code requests via yylx/claude-opus-5-5 and codex/gpt-5.5 answered. The new tests fail on the old code. go vet, builds (darwin, GOOS=linux, GOOS=windows), go test -tags nogui -count=1 ./... under a temp HOME and the 34 agent GUI tests pass but cli-update, menu-scroll and old-webkit, which fail on main too. 2026-10-04 02:05:10 +08:00
yetone c47fee0b26 agents: Codex keeps its own last pick when connected, and every connected line starts 「已接入 ·」 (owner: 让 Codex 记住上次的选择,要统一)
Connecting Codex wrote a start model (it picked stepfun/step-1-32k, a dead one, for a ChatGPT sign-in). Signed in with ChatGPT, Codex now joins magpie by the base URL alone (Agent.Join): openai_base_url to magpie's gateway, magpie's provider table for the threads started on it, CC Switch's tables taken over, the models cache aged — and model untouched, the one last picked in its /model. Agent.Joined keeps it Wired while it is on a model of its own; failover leaves a joined Codex alone; a model of its own picked in magpie keeps it connected; Disconnect leaves Codex on the model it is on rather than the one before connecting. An API key or a provider of its own still connects on one of magpie's models, as its own can't be reached through magpie's provider.

The connected line for agents picking in their own list (Codex, OpenCode, …) read 「120 个模型在 OpenCode 的 /models 里」; now 「已接入 · OpenCode 的 /models 里有 120 个模型」, as the others.

TestCodexConnectKeepsItsPick fails on the old code (connected on fake/m1). Real Codex in a sandbox HOME with the owner's ChatGPT account: connect left model = "gpt-5.4" (Codex's), wired, no drift; codex exec on gpt-5.5 and on deepseek/deepseek-v4-flash both answered through magpie. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass; agent GUI tests pass but cli-update, menu-scroll and old-webkit, which fail on main too.
2026-10-04 02:05:10 +08:00
yetone d0d098c69f agents: the 「接入」 boards filled in — the opened row as designed, real accounts tried
- The opened row follows the boards: what to do in the agent, running copies with the old list, Claude Code's tiers (each with its own effort beside its model again, #536), the model list by whose models they are with what's hidden, the model new sessions start on, what connecting costs (Gemini's Google sign-in), and the files put back on disconnect, shown line by line.
- Disconnecting asks first and shows the lines it puts back. Secrets are masked, and a config of tens of thousands of lines diffs in a fraction of a second.
- Connecting prefers the agent's own subscription when magpie serves it (Claude, Codex).
- The dry run goes through proc.
- Every model hidden still keeps the row connected, with the way back to the list (#356).
- Routing groups are named in Chinese.
- Phone widths: the pickers and squares wrap inside the opened row, and the name stays beside its logo.

GUI tests moved to the new rows: connected fixtures carry refs and open the row from its link. tier-effort and mobile-web each found a real gap and now guard the fix.

Tried in a sandbox HOME with the owner's real accounts:
- Claude Code: connected, a request answered, and disconnect restored the settings exactly.
- OpenCode: a request answered.
- Gemini CLI: connected.
- Codex: connected.
- Model list picks were saved.

go vet, builds (darwin, GOOS=windows, GOOS=linux -tags nogui) and go test -tags nogui ./... under a temp HOME pass. The agent GUI tests pass except cli-update's tight-row case, menu-scroll and old-webkit, which fail on main too.
2026-10-04 02:05:10 +08:00
yetone 4c7ab667e5 agents: Claude Code's /model pick holds for new sessions (owner: 去掉 ANTHROPIC_MODEL,让 /model 的选择对新会话也生效) — magpie wrote the main model as env.ANTHROPIC_MODEL too, which outranks settings.json's model, so a pick in /model lasted one session. The main model is now settings.json's model alone (an alias opus/sonnet/haiku/fable reads as its tier's model); magpie remembers the one it wrote (claude.main), and the tiers and subagents equal to it follow it. Agent.Follow, run when the GUI builds its state, moves the followers onto an in-agent pick and takes an older magpie's ANTHROPIC_MODEL out; it writes nothing with nothing to follow. Subagents that follow leave CLAUDE_CODE_SUBAGENT_MODEL unset and run on the session's model. TestClaudeModelPickedInClaudeCode. Real Claude Code 2.1.288 in a sandbox: a legacy settings.json lost ANTHROPIC_MODEL on the GUI's refresh, /model → m-beta answered from m-beta, a new session started on m-beta, and the tiers followed it on the next refresh. go vet, GOOS=linux/windows builds, go test -tags nogui ./... under a temp HOME pass. 2026-10-04 02:05:09 +08:00
yetone 1b02092718 gui: the Agents view leaves holding the reader to the page, not WebKit's scroll anchoring — with the connect line under each name, a pick redrew the rows and WebKit anchored the list 4–8px down for a frame before the page put it back (claude-direct.test.cjs, under load). Agent GUI tests: no failure on the branch that base lacks, webkit and chromium. 2026-10-04 02:05:09 +08:00
yetone 90d884ee60 agents: a switch connects an agent to magpie, and its models are picked in the agent (prototype, the owner: 在 agent 页面不应该是为 agent 选择某个模型,而是应该有个「接入」的开关) — agent.Connect sets the agent's main field to one of magpie's models (the one serving the model it is on now, else the first) or to magpie itself for an app whose one setting is magpie as its provider; that writes the provider and the whole catalog, so the agent's own list has every model. POST /api/agents/connect/{id}. Each row with magpie models to offer gets a switch and a line under its name (已接入 · 在 Codex 里用 /model 选 magpie 的模型 / 未接入 · Codex 用它自己的设置); off goes through the existing Disconnect dialog. Claude Code's /model lists only its tiers, so while routed magpie also writes modelPicker (2.1.287) with every magpie model, taken out on disconnect; one the user wrote is left alone. Checked in a sandbox HOME against a fake upstream: Codex 0.160 /model lists and uses magpie's models, OpenCode models, Pi --list-models, Crush's providers, Claude Code /model after connect; disconnect puts Codex back on gpt-5.4/high. 2026-10-04 02:05:09 +08:00
yetone 7f2e380f55 search: a Kimi Code plan searches the web for its own models with its own search service, before the Codex account or whatever magpie picks, and Settings may name it to search for every model (Discord: on Claude Code with the Kimi Code plan, why does web search use Codex's model?) — kimi-cli's SearchWeb (kimi_cli/tools/web/search.py, 1.52.0) POSTs {text_query, limit, enable_page_crawling, timeout_seconds:30} to the plan's search_url, which auth/platforms.py makes <base_url>/search (https://api.kimi.com/coding/v1/search) with the same key as the models (setup.py and oauth.py write services.moonshot_search with the plan's key or sign-in), User-Agent KimiCLI/<ver> and X-Msh-Tool-Call-Id, and reads search_results [{site_name,title,url,snippet,content,date}]. provider.KimiCodeSearch finds a plan by api.kimi.com/api.kimi.ai /coding or its kimi-code preset; magpie asks it the same way (no model asked, pages as the search APIs' are, date as page_age). A model served by a plan is given magpie's web_search even with nothing else set up, and its searches go to the plan first; failing, magpie's pick, then the search APIs, as before. For other providers' models a plan is never picked automatically (it would spend the plan's allowance and change today's searcher): Settings › 联网搜索 offers it once, by its web search with no model, and says it searches for its own models first. TestKimiCodePlanSearchesForItsModels fails on the old code (no search tool with nothing else set up; with a searching API and a search API, the old code asked them and never Kimi) and passes; TestKimiCodePlanNamedToSearch and TestKimiCodeSearchAddress cover naming it and the address; searcher-pick.test.cjs (webkit, en/zh) the picker. Against the real account's key from a temp copy of the config: api.kimi.com/coding/v1/search exists (404 on a wrong path, 400 on a bad body) but answers 403 access_terminated_error for search and chat alike, as that Kimi Code subscription has lapsed, so a real search result wasn't seen. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. 2026-10-04 02:00:32 +08:00
yetone 5d126a0db7 agents: Pi's model picker lists the models of its openai-codex sign-in from Pi's own registry, and its Default says it lets Pi pick (#709) — Pi's native options came from models.dev by provider id, and models.dev has no openai-codex (Pi's "OpenAI Codex (legacy)" ChatGPT sign-in), so a Pi signed in to both openai and openai-codex was offered openai/… only. Pi's (and OmO's) own options now come first from the model registry in the package Pi was installed from: the pi command on PATH leads to @earendil-works/pi-coding-agent, and pi-ai's dist/providers/data/<provider>.json (Pi 1.0) is read as is, in its order, chat models only (no gpt-image-2), memoized by the file's stamp — the list Pi's /model offers, which moves with every Pi update. A provider the registry doesn't have keeps models.dev's list; openai-codex with no registry in reach falls back to Codex CLI's models_cache.json (the same ChatGPT backend); models.dev's openai list isn't mapped onto it. The rows are openai-codex/<model> under "OpenAI Codex" with OpenAI's logo, a row each beside openai/<model> of the same name; picking one writes defaultProvider=openai-codex, defaultModel=<model> as before. omp shared the gap for its current model's provider (ownOptions): it now reads omp's own catalog (@oh-my-pi/pi-catalog src/models.json, non-chat kinds left out); omp's signed-in providers that aren't the current one are still #341's. Default in Pi's and OmO's model picker now says "clears the default model; Pi picks one on its own" (zh 清除显式默认模型,由 Pi 自动选择; no ja table exists), other agents' still say what they ship with. TestPiOffersItsOwnOpenAICodexModels (a fake npm-linked Pi 1.0 install, temp HOME and PI_CODING_AGENT_DIR, auth.json with openai and openai-codex oauth test metadata), TestPiOpenAICodexWithoutItsRegistry and TestOmpOffersItsOwnOpenAICodexModels fail on the old code (no openai-codex rows) and pass; pi-default-note.test.cjs (webkit, en/zh) fails on the old app.js and passes; agent-disconnect, agent-models and agent-model-groups GUI tests pass. Read-only against this machine's real installs: Pi 1.0.0 gives openai-codex 9 models and openai 44, omp's catalog openai-codex 8. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestProvidersAnswerWhileListsComeIn, TestClaudeSignedOut, TestClaudeAccountNamedAsItsLogin and TestClaudeIdentityServedWhileAsked, timing tests that pass alone. Not tried in the GUI with real Pi sign-ins, nor with a Pi older than 1.0 (no data folder: models.dev and Codex CLI's list as before) or a Windows npm shim. 2026-10-04 01:53:58 +08:00
yetone 778dc8dc41 gui, mcp: Sign in in an MCP server's editor signs in to the server the form shows, saving it first, and a server that needs no sign-in is said to work as it is (lc on Discord: exa mcp 无法OAuth使用,x-api-key + key也无法使用 agent使用的是omp) — Exa signs in only at mcp.exa.ai/mcp?login (401 with resource_metadata, auth.exa.ai, S256, dynamic registration); plain /mcp answers without one. Sign in sent only the name, so the backend probed the saved URL: a URL just changed to ?login, unsaved, got "this server answers without a sign-in: there is nothing to sign in to". Now Sign in saves the form first when its name, transport, URL or headers aren't the saved server's, leaves the dialog open, and signs in to it; a server being added, or one switched to HTTP, has Sign in too, and a later Save is of the server as saved. mcpauth.ErrNoSignIn says the server already works without signing in, so the agents can use it as it is, a key goes in Headers and a server that signs in at another address (Exa's ?login) needs that URL; the GUI shows it in Chinese. The gateway's /mcp/<name> relay already kept the signed-in URL's query (?login reaches the server) and passed the Library's headers (x-api-key) on, dropping only the gateway's own key: TestMCPProxyKeepsQueryAndHeaders holds it (the fake now records the query). omp reads magpie's mcp.json entry as written (type/url/headers, discovery/builtin.ts) and sends headers on every request (mcp/transports/http.ts): with omp 18.4.9's own client under a temp HOME, mcp.exa.ai/mcp with x-api-key listed Exa's tools and ?login without a sign-in got the 401, so the encoder is unchanged. mcp-signin.test.cjs (webkit, en and zh): an unchanged Exa says it works as it is, with nothing saved; its URL changed to ?login, Sign in saves it then signs in, the dialog stays and no click moves the page; old code started the sign-in without saving. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestProvidersAnswerWhileListsComeIn, a timing test that passes on rerun; library and mcp GUI tests pass in webkit (chromium didn't launch here). Not tried: a real Exa OAuth sign-in in magpie, or omp through magpie's relay with a real account. 2026-10-04 01:52:46 +08:00
yetone b9a38a83ba tui: c on the Usage page presses WorkBuddy's daily check-in (签到) for every WorkBuddy (China) account signed in here at once, and the WorkBuddy lines say how today's went (akic404 on Discord: the TUI had no way to check in) — the app had its Usage card's Check in now and the CLI magpie accounts checkin, the TUI nothing. Now c on the Usage page runs provider.CheckInWorkBuddy, as the CLI does, for all the accounts on this computer at once (the built-in's, or the plugin's when moved or under workbuddy-plugin), and says each in the status line: "旅行者 checked in today +100 · a 4-day streak", "· already", "isn't eligible", "no check-in event now", or in red "couldn't check in: <why>"; with none signed in it says so. The footer lists c, and each WorkBuddy line on the page carries its check-in (签到 ✓ +100 · 4-day streak, 签到 not yet today · c, 签到 failed · c tries again), read from workbuddy-checkin.json via WithCheckins (provider.CheckinDay names the Beijing day) and redrawn after c without asking the vendors again. Local only: a remote magpie's accounts are checked in on that magpie, from its own app, TUI or CLI; no gateway endpoint is added. WorkBuddy AI has no check-in event, so only WorkBuddy (China) is checked in, as before. README says so. TestTUIChecksWorkBuddyIn (none signed in, claimed, already + refused, ineligible + no event, with a stand-in check-in) and TestCheckinOnTheUsageLine pass. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried against a real WorkBuddy account, nor in a real terminal. 2026-10-04 01:52:46 +08:00
werldl517-cyberandwerldl517-cyber 29a14c9a26 quota: pool windows stand in for their models' in the alerts and the CLI (#523)
PooledWindows is how the usage page shows an Antigravity account's windows:
a pool's 5-hour and weekly ones, named with their pool ("Gemini · 7 days"),
in place of the per-model windows drawing on it. dueAlerts, the CLI's
accountRows and QuotaReport walked the raw per-model windows instead: a
pool raised an alert for every model in it and again for its 7-day
window, "magpie accounts" and "magpie quota" (its --json and the
gateway's /v1/magpie/quotas with it) listed a row per model, and whether
a model's name carried the pool in front depended on the name's own
words ("Gemini 3 Flash" did, "Claude Opus 4.6 (Thinking)" didn't). All
three go through PooledWindows now — one alert per pool span, one cell
per pool window, the pool already in the name. A pool's weekly window is
aside (using it up doesn't stop the account) but is still told of; the
pools' 5-hour asides aren't. The alert key keeps its shape: the pooled
name in it keeps two pools apart, and a window with no pool keys as it
did, so what an earlier build already said isn't said again — the pool
suffix the last draft appended is gone.

QuotaReport's card assembly is quotaReport(subs, plans, balances, now),
which tests feed directly, and LoginUsageVia (as UsageClaudeVia) stands
in for LoginUsage's readings, so accountRows is testable too.

TestDueAlertsPoolWindows feeds dueAlerts the windows Antigravity really
reports (the shapes TestAntigravityQuotaPools records): every model's
own, then each pool's 5-hour and weekly ones. On the old code it fails
with six alerts where two belong, per model with the pool sometimes in
front; now the two weekly pools are told of once each, "Gemini · 7 days"
and "Claude & GPT · 7 days", their marks distinct, a second reading
saying nothing, and a mark an earlier build wrote of a window with no
pool still holding. TestQuotaReportPoolsAntigravity fails on the raw
windows with eight spans where four pooled ones belong; TestQuotaCell,
TestAccountRowsPoolsAntigravity and TestQuotaCell read pooled names.
QuotaSpan keeps its Pool for magpie quota --json.

go vet ./... and builds -tags nogui (windows, GOOS=linux, GOOS=darwin)
pass; go test -tags nogui -count=1 ./... under a temp HOME passes for
every package this touches (provider, the gateway and the CLI and TUI
packages among them) — agent, gateway, library and sessions fail a few
of their own tests on this Windows machine the same way on main, paths
and WSL probes its Linux and macOS runs don't see.

Co-authored-by: werldl517-cyber <werldl517-cyber@users.noreply.github.com>
2026-10-04 01:44:27 +08:00
yetone 2719dd6ac3 tui, cli: adding a provider says how many models its list had, or why it had none, and m (magpie provider refresh <id>) fetches the list again (akic404 on Discord: 还是tui cli问题,没有获取上游模型功能,新建之后模型数是0)
The TUI's add fetched the new provider's list and dropped any error, so a remote magpie that couldn't be listed (wrong address, the other magpie not shared, a gateway key it refuses) showed just "added Remote magpie" and 0 models, and the Providers page had no way to ask again: t tests the APIs, the models picker said "t tests it, which asks the vendor for them", which it doesn't. Now the add says "added X · N models from its list · M for agents (↵ picks)", or "added X · no model list · <the first URL's answer> · m asks again" in red; m on the Providers page is the app editor's Refresh (Refetch: picks the new list no longer has are dropped and named), with the same note; the footer lists it and the empty picker points at it. The CLI already said a failed list on add and fetched with provider models <id>; provider refresh <id> (and fetch) is that by its GUI name, and models <id> now goes through Refetch too, naming dropped picks. The ids were never names: a remote magpie's are <its id>/<the other magpie's provider id>/<model> (remote-magpie/custom/qwen3.6-max); the Chinese is the label, "qwen3.6-max · 中转站", the other magpie's provider name it sends as magpie_label, which magpie models and the TUI's picker show beside the id. README says so.

TestTUIFetchesAProvidersModels (add over a fake magpie: 2 models; its list grows, m: 3; it refuses, m: the error and the 3 kept) and TestProviderRefreshCmd fail on the old code; TestTUIAddsARemoteMagpieWithItsAddress now expects the error at a dead address. End to end with two sandbox magpies (temp HOMEs, 3461/3462, a fake upstream on 3463): A with "中转站" over the upstream (id custom), B's TUI in tmux adding A at 127.0.0.1:3461: "added Remote magpie · 2 models from its list · 2 for agents", row 2 models, m again the same; a second at 127.0.0.1:3464: "added Remote magpie 2 · no model list · … connection refused · m asks again", 0 models; provider refresh remote-magpie lists remote-magpie/custom/qwen3.6-max "qwen3.6-max · 中转站". go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-04 01:24:47 +08:00
yetone af046769fb routing: an account rate limited with quota left can go to the back of the order, so several accounts take turns (01huadalang on Discord: with several WorkBuddy accounts, one rate limited rests and then goes back to the front, so it takes everything until it is limited again) — a new opt-in setting, "Rate limited" (zh 被限流时), sits next to Routing for a provider (sink in its JSON) and for a routing group (sink on the group). It has two options: "Keeps its place" (the default, unchanged) or "Goes to the back". With "Goes to the back", an account or key that answers 429 while its subscription still has quota (failRate, not a quota or credit failure, and not a used-up allowance) rests as before. Once the rest ends it stays behind every one not rate limited since; those sunk are ordered by when they sank, so the load goes a,b,c → b,c,a → c,a,b. In a group it covers the members' accounts and keys together. The sink is kept in memory until magpie restarts or the account sinks again. It applies to every routing but In turn (which goes round already) and Manual, and is hidden there.
Where it is set:
- the provider editor's "Rate limited" row, saved with its Save (sink in /api/provider/save, kept by a Save that leaves it out);
- the Routing page's row of providers with several accounts or keys, which saves at once (/api/provider/sink);
- the group editor (/api/groups/save), with the group card tagged "Rate limited to the back".

The Routing page's trace gives a sunk account "rate limited at {time} · at the back" and a line saying why. Weighed carries `sunk` and GroupRef carries `sink`.

Tests:
- TestRateLimitedSinksToTheBack: three accounts rotate over five rate limits. Off or in turn they keep their order. A quota 429 or a full allowance doesn't sink one, and one still resting stays behind.
- TestGroupRateLimitedSinks: ordered and smart groups.
- TestProviderSaveSink.
- rate-limited-sink.test.cjs covers the editor, pool row, group editor, card tag, trace and zh, in en and zh. It fails on the old code and on the old assets.

routing-compact.test.cjs now wheels down to "Add a rule" and Save as the reader would, since the editor's new row puts them past the window.

go vet, builds (nogui, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. In webkit, the routing-*, group-*, quota-pools, panel-routing, click-scroll and settings-groups GUI tests pass, except routing-manual, routing-time and group-from-model-add, which fail the same way on origin/main. Not tried with real accounts against a vendor's 429.
2026-10-04 01:20:51 +08:00
Haogao Gu ba2128b7e5 Explain sealed subagent routing and clarify quota messages (#703)
* routing: explain sealed subagent eligibility and clarify quota messages

* routing: address review wording and document consistency
2026-10-04 01:20:44 +08:00
yetone 00b5879c06 gui: a removed account's row in Add a provider has Add it back and Sign out… side by side, no menu and no hiding (#694, mintonight: 不要点击后再弹一个选项卡面板,最后直接两个按钮就行; hiding is 掩耳盗铃)
Under "Removed from magpie — still signed in", a click on the row opened a menu with Add it back, Sign out… and Don't show here. The row now shows the two buttons inline, across the row. Add it back works in one click. Sign out… still asks first. Don't show here, the "Show N hidden" / "Hide the hidden" link and their strings are gone: hidden, the account stayed signed in, and Sign out is what puts it away. A row hidden before (#116) is listed with the rest. The reminder line on the Providers page keeps Don't remind me. No select, no stripe, and a click doesn't move the page.

removed-hide.test.cjs (two inline buttons, no menu, nothing that hides, a row hidden before is listed, the click leaves the page where it was) and removed-forget.test.cjs (side by side, Sign out… asks and forgets with no menu, Add it back in one click) fail on the old code (10 of 14) and pass in webkit. add-sheet, click-scroll and project-remove-keep pass too. Chromium doesn't launch in this sandbox (icudtl.dat). go vet, builds and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-04 00:56:37 +08:00
yetone 1d3ad6ad78 provider: WorkBuddy's check-in reaches the plugin signed in under its own id, workbuddy-plugin (#694, Dazzle-sys: 0.1.774 签到没看到)
The check-in only took the built-in's sign-ins, or the plugin's once WorkBuddy was moved onto it. A user who installed @magpie-community/opencode-workbuddy-auth while also signed in to the built-in (or who moved back) keeps the plugin under "workbuddy-plugin", which HandOver leaves as it is. Those accounts were never checked in, and their Usage card ("workbuddy-plugin") never got a check-in row, because withCheckins answered only for the card "workbuddy".

wbCheckinAccounts now adds the accounts of a plugin signed in under its own id, and each one remembers its card. withCheckins labels any card whose accounts it holds, by account name, or an unnamed card when it holds just one. An account signed in through both the built-in and the plugin (same uid) is checked in once and listed once.

Qoder has no check-in in magpie. It is a moved plugin subscription, so a check-in would have to come from the plugin, not the built-in.

TestWorkBuddyCheckinPluginUnderItsOwnID fails on the old code (only the built-in's Cat is checked in) and passes. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried with real WorkBuddy accounts.
2026-10-04 00:56:37 +08:00
mofelee cab8a8e450 fix: refresh first-request discovery and retain late Pi timing (#708) 2026-10-04 00:49:15 +08:00
yetone 65e78e459e codex: Settings decides where Codex's thread-title requests go — Codex's own ChatGPT as before, off, or a magpie model (#705, DuGuaiYA: Codex signed in with ChatGPT sends thread_title / thread_title_reconsideration requests that magpie relays through ChatGPT even when the conversation is on a magpie provider) — a new setting codexTitles ("" = as Codex sends them, the default; "off"; or a model's id such as deepseek/deepseek-v4-flash or a group) is read on /backend-api/codex/responses for a request whose x-openai-subagent or turn metadata thread_source is a title kind. Off answers at once with an empty completed response (Codex then keeps no title, quietly, as its source shows), contacts nothing, and is still recorded: a Routing trace "Titles off" and a usage row of Kind thread_title marked rejected (left out of stats). A model gets the request through serve (so it is recorded as a title request on that provider), and its answer is given back as one Responses message holding {"title":…}, since Codex's text.format schema does not reach Chat or Anthropic upstreams. Settings has a row "Codex thread titles" (zh: Codex 会话标题) with the app's model menu (Codex's own, Off, magpie's models and groups), saved on its own through POST /api/settings/codex-titles, which refuses a model magpie doesn't serve; the page's other saves keep it. TestCodexTitlesSetting (gateway) fails on the old code (off still went to ChatGPT) and checks the ledger kinds, rejected rows and trace; TestTitleJSON; TestCodexTitlesSetting (gui) for the endpoint; codex-titles.test.cjs (en/zh, webkit) fails on the old assets: no select, the picks post, the subtitle follows, no click scrolls. go vet, builds (darwin nogui, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried with a real Codex TUI: no end-to-end run of Codex's title generation against magpie. 2026-10-04 00:23:05 +08:00
yetone 7806395c74 antigravity: a Google AI Pro account asks for its models with its plan named, as Antigravity does, and the log says what Google answered (Discord: a Pro account on magpie 0.1.770 had no Claude after Refresh while Antigravity 2.19.1's picker had Claude, 5.5 too) — magpie asked fetchAvailableModels with only {project}. Antigravity's language server (2.19.1, read from its binary) sends FetchAvailableModelsRequest{project, request_id, entitlement{user_tier}, location}, entitlement being the tier it keeps for the account (SetUserTier, with the plan's name) and the only v1internal request that has one. A Pro account's loadCodeAssist (CLIProxyAPI #6186) has currentTier standard-tier (usesGcpTos), paidTier g1-pro-tier "Google AI Pro", project aicode-consumers; asked with no tier, Google most likely serves it as standard-tier, Code Assist Standard, which has no Claude. Google checks the field: on our free account free-tier/bogus ids are INVALID_ARGUMENT, and standard-tier, g1-pro-tier, g1-ultra-tier and the other paid ids are FAILED_PRECONDITION, so paid ids are known and checked against the account. Ruled out on the free account: the project (none, random, aicode-consumers all the same), daily/prod/sandbox (all list Claude), and magpie's filters (only chat_/tab_ and six fixed ids, none Claude). Now fetchAvailableModels sends entitlement.userTier: the paid tier, then the current tier, skipping free-tier and legacy-tier, and on a refusal tries the next and at last none, as before; a free account sends none, so its request is unchanged. The tiers come from loadCodeAssist, which is now asked even when a project was kept with the sign-in (import, magpie accounts project), which used to skip it and so never learned the tier; if that fails the kept project is used as before. Logged: loadCodeAssist's current and paid tiers (with gcp-tos), project and ineligible tiers with reasons; each refresh's endpoint, User-Agent, project, tiers, entitlement sent, models listed and kept, the ids left out and why (not for chat, completion-only), those Google marks disabled (still listed), and the Claude ids; a tier refused, with Google's error. TestAntigravityProAccountNamesItsTier (Pro fixture: Claude only with g1-pro-tier named), TestAntigravityTierRefusedFallsBack and TestAntigravityTierFreeAndKeptProject fail on the old code (Gemini only) and pass. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried with a real Pro account: whether Google serves Claude to a Pro account only with its tier named is inferred from the client and the fixtures, not seen. 2026-10-04 00:23:05 +08:00
yetone bc141ad834 update: install.sh and magpie update take --proxy and --mirror, and magpie update mirror keeps one for the app's updates too (akic404 on Discord: 安装和update如果没有梯子也是麻烦事,能否在install.sh和magpie update后加子选项?--proxy等可自定义或默认ghproxy) — install.sh had no options and fetched with curl alone, and magpie update took none; without a way out to GitHub both failed. Now both take --proxy <url> (http, https, socks5, socks5h; install.sh also MAGPIE_PROXY=) for the feed and the download, and --mirror <prefix> (or MAGPIE_MIRROR=), a GitHub download mirror the release file is fetched from as <prefix>https://github.com/…; only github.com / githubusercontent.com URLs are put through it. No mirror is used unless one is given: magpie names none and has none by default. The feed with each file's SHA-256 is always fetched from usemagpie.ai, never through the mirror, so a file the mirror changed is refused and deleted ("… from the mirror X does not match its checksum from https://usemagpie.ai; not installed"); macOS still checks the app's signing team. magpie update mirror <prefix>|off saves Settings' updateMirror (http(s) only), which the app's own background update follows too; magpie update --mirror off skips it once; the Settings page's save keeps it. The app's update already followed Settings → Proxy (#294); magpie update --proxy overrides it for one run. The environment's ALL_PROXY / all_proxy is now honoured when HTTPS_PROXY / HTTP_PROXY are unset (Go's ProxyFromEnvironment ignores it), with NO_PROXY as curl reads it. magpie update takes --flag value or --flag=value, and an unknown word is an error instead of an update. The help, README and the docs' FAQ (en/zh/ja) say how. install.test.mjs's proxy, mirror and tampered-mirror tests, TestAllProxyFromTheEnvironment, TestUpdateThroughAMirror, TestUpdateThroughAProxyGiven and TestUpdateFlags fail on the old code and pass; TestSettingsSaveKeepsWhatItDoesNotSend covers updateMirror. A dry run of install.sh as Linux amd64 into a temp HOME installed the real 0.1.773 with its SHA-256 checked, refused a local mirror serving a wrong file, and with --proxy to a closed port failed reaching the site through it. go vet, builds (darwin, GOOS=linux, GOOS=windows), sh -n, node --test site/install.test.mjs and go test -tags nogui -count=1 ./... under a temp HOME pass but TestClaudeIdentityServedWhileAsked, a timing test that passes alone. Not tried through a real proxy server or a real public mirror; no install.ps1 exists for Windows, and the app has no field for the mirror. 2026-10-04 00:19:47 +08:00
yetone 38a0c1f053 tui: a Remote magpie is added with its address, and w changes it (akic404 on Discord: tui里的remote magpie…gui里会填写url,但是tui没有填写位置,我只能退化为用指令添加) — the Providers page's a picked a vendor and asked only its key, so a preset reached at the user's own address (Remote magpie, Azure OpenAI) was saved with no URL and refused ("a provider needs a base URL"); only magpie provider add remote-magpie … url=… or the app's editor could add one. Now picking such a preset asks its address first, as the app's editor does (the preset's example, http://192.168.1.20:3425, as the placeholder, its hint under it, nothing typed answered with "The other magpie's address is needed"), then the key; the address goes on Chat and Responses as the editor puts it, and a remote magpie's every API is put on it when saved (host:port, …/v1 or the Anthropic root all work). w on the Providers page changes the address of such a provider, with the one it has in the line; other providers say theirs is the vendor's. The footer names w. README says the TUI adds one too. TestTUIAddsARemoteMagpieWithItsAddress fails on the old code (the key asked first) and passes. Remote magpie has no default target: the address is the other computer's magpie as its Settings → Share on local network shows it; the example is only a placeholder. 2026-10-04 00:19:47 +08:00
yetone 04bc3461cc gui: with no model picked, a click on a model picks just it (#681, #702) — the provider editor's chips, with none picked, draw the models agents are served (the vendor's first 24) as served; a click on one left it out and picked every other served one in its place (#614's choice), so ChenstarMr's click on DeepSeek V4 Flash in Trae CN ticked the other 23 and not it ("我想选其中一个模型,但是点击之后它却选了其他的所有模型"), and Dazzle-sys's Cline (22 models, all served) clicked on each of its 4 free models till it showed ticked ended with all 22 picked, which Save wrote (#702: 选了4个免费模型保存出来显示22个). Now a click picks the model, alone when none is picked, then each next one joins it; a served chip is drawn without the accent (dashed grey border, no tick) so it doesn't read as picked, its title and the hint say a click picks just that one. Nothing changes on the Go side: no picks is still an empty list, Exposed still serves the first 24, and an editor saved untouched writes none, for key and plugin providers alike (same code). zh strings added. model-first-pick.test.cjs (webkit, en/zh: a 54-model plugin provider and a key provider, untouched Save writes [], one click picks it alone with the page not moving, Save writes it alone; Cline's 4 free ones clicked till ticked save the 4) fails on the old code (22 saved) and passes; provider-none-picked and model-rate now expect the click to pick. provider-draft-picks, click-scroll, add-sheet, model-pick, models-free-only, provider-picks-stay/models-rest/add-models/typed/detect-models, model-test-one/why, account-models, editor-short-window, fallback-more, model-api-preset pass. go vet ./internal/... and go test -tags nogui -count=1 ./internal/gui/... ./internal/provider/... under a temp HOME pass. Not tried with a real Trae CN or Cline account. 2026-10-04 00:13:54 +08:00
yetone 8b8f91ba8f usage: a WorkBuddy account's card says how its daily check-in went, and turns it on (#694, Dazzle-sys: 签到设置放到用量卡片上,卡片显示今日是否已经签到;要去设置里看才知道签没签是不对的) — the check-in ran and was kept (workbuddy-checkin.json) but only Settings' Daily check-in said anything of it. It runs in magpie for a moved account too (through the plugin's fetch, wbCheckinAccounts), so the card can say it for both. GET /api/usage/quotas now adds, uncached, to each WorkBuddy (China) account checked in each day (provider.WithCheckins, matched by user name, an unnamed card only when there is one account) checkins and its last record; each such account's meters end in a line with a dot: Checked in today +credit · N-day streak (green), Not eligible for the daily check-in, No check-in event now, Check-in failed; magpie tries again later (red, the vendor's message in its tooltip), or Not checked in yet today (amber) / Auto check-in is off, with · last checked in {day}. The card's first such line has Auto check-in, the same setting as Settings' Daily check-in, which stays (POST /api/settings/workbuddy-checkin, written as Codex's auto reset is), and Check in now while an account isn't in today (POST /api/usage/workbuddy-checkin, CheckInWorkBuddy, then the cards are read again). Another vendor's card has none of it; no native select, no left stripe, the page doesn't move. zh strings added. usage-checkin.test.cjs (webkit, en/zh: three accounts' lines and states, none on Codex, the switch once and off, turned on with no scroll and the line turning amber, Check in now posted and the cards re-read) fails on the old code and passes; TestWorkBuddyCheckinOnTheCard covers the matching and that the cache isn't changed. credit-days, codex-auto-reset, quota-*, usage-arrange/fold/alert, settings-sections, tray-usages, balance-parts, click-scroll, add-sheet, removed-forget/hide and plugin-two-subs still pass. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried with a real WorkBuddy account. 2026-10-03 23:55:18 +08:00
yetone ee0e899f37 providers: a removed account can be hidden from Add a provider too, and found again behind "Show N hidden" (#116, marsxxl on v0.1.768: 「已从 magpie 移除 · 仍处于登录状态」 lists a removed Gemini CLI with only 加回来, 不再提示 is gone) — "Don't remind me" was never gone: it is still on the Providers page's reminder line and still kept (quiet in providers.json, through a refresh or a restart), but choosing it is what puts an account under Add a provider's "Removed from magpie", where nothing could put it away, so the reminder read as having moved there without its button. Now that row's menu (the app's menu from #694) has Don't show here beside Add it back and Sign out…: POST provider/tuck keeps it in providers.json (tucked, quiet as well), the sheet stays open with the page where it was, and the row goes; the section's heading then says "Show 1 hidden", which lists it again, dimmed and marked hidden, where Show here again (provider/untuck) puts it back in the list; a search naming it finds it too, and Add it back clears both, so removing it again reminds again. zh strings added. removed-hide.test.cjs (webkit, en/zh: the menu's three items, tuck posted with the sheet open and no scroll, the row still hidden after a reload, Show hidden without a scroll, untuck, a search finding a hidden one, the reminder's Don't remind me posting quiet) fails on the old code and passes; removed-forget.test.cjs expects the third item; add-sheet, click-scroll and plugin-two-subs still pass. TestAccountsAreProviders covers tuck/untuck through a save and Add it back clearing it. 2026-10-03 23:55:18 +08:00
yetone 643264c24f sessions: the Sessions page and local usage read the agents in WSL too, and resume one there through wsl.exe (lgtm on Discord: magpie can't detect the session lists of agents inside WSL?) — on Windows magpie already finds the WSL distros an agent is set up in (internal/agent's wslDistros, the distro user's home as \\wsl.localhost\<distro>\home\<user>), but the session reader only looked at this computer's own ~/.claude, ~/.codex and ~/.pi. Now internal/agent hands those homes to the sessions package (sessions.WSLHomes, set in init, no second discovery), and the sessions of Claude Code, Codex and Pi in each distro are listed beside this computer's, with Session.WSL naming the distro: a "WSL <distro>" badge on the Sessions page's rows and the management page, a WSL line in the details, " · WSL <distro>" in the API's name. Resume runs in the distro: wsl.exe -d '<distro>' --cd '<cwd>' -e sh -lc 'exec ${SHELL:-sh} -lic ''claude --resume <id>''' (the user's own shell, so the agent's PATH is there; no double quotes, which PowerShell 5.1 mangles). Their calls count in local usage (callFiles). Reading a distro over \\wsl.localhost is slow and opening a file in a stopped distro starts it, so the files are listed in the background, at most every 30 s, the listing kept in sessions-wsl.json; the page waits for it only the first time ever (up to 3 s); a stopped distro's sessions stay listed as last read and nothing in it is opened (the parse cache and call shards keep them). magpie doesn't delete a session in WSL: the checkbox and Delete are off for them, with a note to delete them there, and sessions.Delete refuses. OpenCode in WSL isn't read (its SQLite over \\wsl.localhost isn't reliable). zh strings added. TestWSLSessionsListed, TestWSLStoppedDistroKept and TestWSLNoneWithoutHomes (internal/sessions) and TestWSLSessionsOfTheDistro (internal/agent, FakeWSL: a Claude Code session in a fake Ubuntu is listed with its distro and the wsl.exe resume line) fail on the old code and pass. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried on a real WSL with agent sessions: the Windows test box's distros have no Claude Code, Codex or Pi sessions; the resume line wasn't run in PowerShell, and the pages weren't opened in a browser. 2026-10-03 23:55:18 +08:00
yetone 0ae7330242 plugin: a pi package's sign-in screen on pi's terminal UI is asked in magpie, and an empty reply a package makes of a vendor's error says the error (all pi plugins: Kiro's login hung, pi-zcode's quota ran out as an empty answer) — pi-provider-kiro asks how to sign in through ctx.ui.custom, a component of pi-tui (a SelectList, then Input fields for a start URL and region), and magpie gave pi's runner no UI, so custom came back undefined and the sign-in waited forever. pi's runner now has a UI for the sign-in's questions: select, confirm, input and editor become magpie's sign-in prompts, and custom draws the package's component and asks its list or field the same way, answering through the component's own onSelect/onSubmit, until the package calls done. A component may keep its parts to itself (Kiro's gives only render and handleInput) and each package loads its own copy of pi-tui, so the parts are found by what drawing walks: every container goes through its children, so the arrays iterated while it renders are what it shows, in order; the text above the list is its question. Outside a sign-in the UI answers as pi's no-UI context does. session_start now reaches every package at startup, which Kiro needs to keep the context it asks through. Separately, pi-zcode turns ZCode's 1005 quota error into a reply with no content; magpie keeps the small JSON or text bodies a package's fetch gets while it streams, and a stream that ends empty answers that body's message and code (429 for quota or rate limits, else 502) instead of an empty message. TestPiPackage drives a fake package that asks on real pi-tui classes (a list, then a start URL field) and fakes ZCode's empty quota reply; it fails on the old code (no first prompt) and passes. With real accounts in a sandbox HOME: Kiro's "Kiro Login" (API Key, Web Login, Device Code) came as a select, Web Login gave the app.kiro.dev sign-in, and after signing in kiro-plugin/claude-sonnet-4-5 called get_weather and answered with its result. pi-cursor (1.4.38, latest) answers plain turns but its models say no get_weather tool exists, though the package sends it (toolCount 1, its MCP schema) and Cursor's stream carries fields the package doesn't know; that is between pi-cursor and Cursor. The ZCode quota path wasn't tried with a real account. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. 2026-10-03 23:47:15 +08:00
yetone b5cfca54ad gateway: a conversation too long for one member of a group goes on to a member with room for it, as before (#700: ynky007 — Kimi Code on DeepSeek V4.1 Flash through magpie showed a 176k context after updating to 0.1.768) — magpie still writes max_context_size = 1048576 for it into ~/.kimi-code/config.toml (checked in a temp HOME with the models.dev cache). 176k is Kimi Code's own: on a context overflow it takes floor(0.85 × the request's tokens) as the model's window for the session (observeContextOverflow, OVERFLOW_CONTEXT_SAFETY_RATIO, 2.1.1), so it was told a request of about 207k overflowed. Since 11a09d41 an overflow (a 400/413/422 saying so, which tooLongRe now knows in many more words) is not retryable and goes to the agent at once, and b3ab3e42 tells some unexplained failures as one; before, a 400 with "exceeded" failed over to the next member, which answered. That is right for other accounts of the same model, not for a group member of another model or vendor, whose window may be larger. Now an overflow goes on to the members left that are not the same provider and model and not known (windowOf) to be smaller than the request; nobody rests (Fail "overflow", shown as "too long for its model"), and if they fail on quota the agent is still told the overflow, to compact. With none left the agent is told at once, as now. TestOverflowGoesToAMemberWithRoom fails on the old code (400 context_length_exceeded, the second member never asked) and passes; it also checks a member known too small and a second key of the same model are not asked. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried with the reporter's providers: which member refused at ~207k is not known. 2026-10-03 23:44:06 +08:00
yetone d079dc6223 usage: an OpenCode session's row shows the effort and the model it asked for (#680, Sun1090: 数据不完整,推理强度是有的)
OpenCode rows read from its own data (opencode.db or storage/) had 请求模型 and 推理强度 as —. OpenCode keeps both on each reply (packages/schema/src/v1/session.ts, AssistantMessage): variant, the effort picked in its model menu for the prompt (high, max, low, xhigh, none, thinking … as the provider's variants name them; prompt.ts copies lastUser.model.variant onto every reply), and modelID, the model the reply was sent to. OpenCode 2 (session_message) keeps the variant in model.variant (Model.Ref). It records no model the response named, so modelID is what was asked for, as a Codex rollout's model is.

Now a call's Effort is the reply's variant ("default" or none is no effort picked); a compaction in OpenCode 2 takes the effort last replied with, as it takes the model. In the ledger an OpenCode call is told as a Codex one: requested = its model, served left empty rather than claimed. The call cache moves to calls-v5 so calls read before carry the effort.

TestOpenCodeCallsJSON/DB/V2 (fixtures with variant high, default, xhigh and none) and TestOpenCodeSessionCallIsWhatItAskedFor fail on the old code and pass. go vet ./internal/..., builds (nogui, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried against a real OpenCode database with variants (this machine's OpenCode data predates them).
2026-10-03 23:44:06 +08:00
yetone 5cf4d1dae1 codex: on magpie API, Codex's own model as the default keeps magpie as its provider and magpie's models in its picker (#701, nullburn: Codex 选回原生默认模型后,第三方模型目录消失(已选择 magpie API)) — with Codex's sign-in set to magpie API, picking one of Codex's own models (gpt-6-luna) as its default in the Agents row ran unroute(): model_provider, model_catalog_json, openai_base_url and magpie-models.json all went, so every third-party model left Codex's picker while the row still said magpie API; and choosing magpie API while on Codex's own model wired nothing (login.Set only re-ran set for a magpie model). Now, on magpie API, Codex's own model goes as magpie serves it on the ChatGPT account there (codexOwnViaMagpie: gpt-6-luna -> codex/gpt-6-luna, the entries shown to Codex first), so set() takes the magpie branch and only the default changes; the picker's OpenAI group offers those ids under their own names (Label gpt-6-luna) so the value matches; one magpie doesn't serve (the ChatGPT subscription off in magpie) is refused with a message naming both ways out, the config untouched, rather than quietly unwired; choosing magpie API on Codex's own model wires it at once; Sync wires a config an older magpie left so (#701's: model = "gpt-6-luna", sign-in api). Back on ChatGPT, a codex/… default turns into Codex's own model again, straight to OpenAI as before (codexOwnOf); the ChatGPT sign-in's paths are otherwise unchanged. TestCodexAPIKeepsMagpieOnOwnModel (api on an own model, magpie model then own model again, picker values, refusal, back to ChatGPT) and TestCodexAPISyncWiresOwnModel fail on the old code and pass; TestCodexProviderAlsoSetsBaseURL's api case now expects magpie to stay wired on Codex's own model. End to end in a temp HOME with a copy of a real models_cache.json: api, fake/m1, then gpt-6-luna leaves model = "codex/gpt-6-luna" with model_provider, model_catalog_json and openai_base_url, and codex-cli 0.160.0's codex debug models with CODEX_HOME on that sandbox (gateway ports pointed at :1) lists fake/m1 beside codex/gpt-6-luna and the other codex/ models. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried in the Codex app or with a turn sent through a real ChatGPT account. 2026-10-03 23:40:06 +08:00
yetone de290249bb pi: a model that always thinks has off hidden on the Messages API, and its Chinese refusal of thinking off is asked again (#699, liuweifeng: 走 Anthropic Messages API 的 always-thinks 模型被删掉 thinkingLevelMap 的 off,Pi 仍提供 off 导致上游 400) — piThinkingLevels left off out of thinkingLevelMap for every model on anthropic-messages, which is right for Claude (Pi offers a level left out, and Claude takes thinking disabled whatever its levels) but had Pi offer off for ZCode's GLM-5.3-Flash too, whose levels are low/high/max and which answers thinking disabled with 400 "该模型始终支持思考,不可关闭". Now off is left out only for a model that thinks only when asked: gateway.ThinksOnlyWhenAsked, the gateway's own anthropicModel (the test that decides it leaves a Claude's request as sent and turns another vendor's thinking off unless asked), plus a relay's opus-5.5 spelling. Another vendor's model there gets "off": null unless none is among its levels (GLM-5.2's none still maps off to it), as on Chat and Responses. The gateway's alwaysThinks, which asks a refused thinking-off request again with thinking left to the model, didn't know the Chinese wording, so the 400 reached Pi (and any agent whose request the gateway turned off); it now matches 始终…思考 / 思考…不可关闭. OpenCode's variants, dsh's levels and omp's efforts name only the model's own levels, off only for none, so they don't offer a level the model refuses; omp can't hide its off, and the gateway's retry now covers it. TestPiOffOnMessagesAPI (GLM-5.3-Flash off null and not offered; GLM-5.2 off=none; Claude and opus-5.5 off left out) and TestAnthropicPassthroughAlwaysThinksInChinese (asked again once without thinking) fail on the old code and pass. Pi's models.json generated in a temp HOME with PI_CODING_AGENT_DIR in it: zc/GLM-5.3-Flash has "off": null, low/high/max; claude-sonnet-5 has no off key. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestDevinSignedInOnlyInMagpie, a timing test that passes alone. Not tried with a real ZCode account. 2026-10-03 23:36:50 +08:00
yetone 89d305229b providers: a removed plugin subscription is named as its row was, and can be signed out for good (#694, mintonight: 点了移除,国际版账号没有消失,放在 plugin 里无法彻底删除;再添加回来还是旧账号) — Add a provider's "Removed from magpie — still signed in" listed a removed plugin account by its agent, which for every plugin's sign-in is "plugin", with the generic logo; and its only way out was Add it back, which brought the old account back, since Remove only hides an account and nothing signed it out. Now a removed account is named and drawn as its row was (accountLabel, shared with the provider row: a plugin's by the provider it signs in to, a moved built-in's by the built-in's logo; Exclusion.Name carries it to the CLI's list too). Its row in the add sheet opens the app's menu (openProtoMenu, the page left where it was): Add it back as before, or Sign out…, which asks first and posts provider/forget; the Providers page's reminder line has Sign out beside Add it back and Don't remind me. provider.ForgetAccount signs a plugin's accounts out of plugin-auth.json (the host's signOut, else under plugin-auth.json.lock), takes them out of logins.json and, for a moved built-in, out of the move's backup so moving back doesn't restore them; an agent's own sign-in that magpie only reads is hidden, as an account's Remove hides it; a built-in's saved accounts go through ForgetLogin, the one in use last. Signing in again then starts afresh. zh strings added. TestForgetRemovedPluginAccount (bun fake plugin: removed is named FakeCo, forgotten leaves plugin-auth.json, logins.json and the removed list empty, Add back brings nothing) and removed-forget.test.cjs (webkit, en/zh: the row named Qoder, the menu with no request and no scroll, Sign out asks then posts forget with the provider id, Add it back still posts show, the reminder's Sign out) fail on the old code and pass; add-sheet, click-scroll, plugin-two-subs and saved-signedout-remove still pass. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried with a real Qoder account; the plugins repo needs no change (the Qoder plugin keeps its sign-ins only in plugin-auth.json). 2026-10-03 23:09:13 +08:00
yetone c3cd790446 provider: a Refresh drops the picks the vendor's list no longer has (akic404 on Discord: a remote magpie offering far fewer models, one of its providers renamed from ws-ba5my… to bailian, still showed every old model here after a Refresh, until the provider was deleted) — a Refresh already replaced the fetched list (catalog.SaveLive writes it whole), but the models picked from the old list stayed in the provider's Models, and Exposed serves a pick the list lacks as one typed in by hand (#597), so every "ws-ba5myt9wad852ujp-…/qwen3.6-max" the remote no longer had stayed among the models agents see, drawn in the editor as "Added by hand". Now Fetch (through the new Refetch) drops from the saved picks those the list fetched before had and the one fetched now doesn't; a pick neither list had was typed in by hand and stays, and a fetch that fails or answers no model changes nothing. It applies to providers of a key (a remote magpie, any /v1/models provider), not to signed-in accounts, whose lists have their own rules. The editor's Refresh is told the dropped ids and takes them out of its draft too, or its Save wrote them back. TestRefreshDropsPicksTheListNoLongerHas (a remote-magpie preset against a server whose list shrinks and is renamed, then goes down) fails on the old code (all four picks kept) and passes. End to end with two sandbox magpies under temp HOMEs: A (nogui serve on 3461, a provider ws-ba5my over a fake upstream), B with A as a remote magpie and four picks (three of A's, one typed); after A's provider set ws-ba5my id=bailian and two models dropped, B's provider models left the typed pick alone, and with A stopped a Refresh failed and changed nothing. go vet, builds (darwin, GOOS=linux -tags nogui, GOOS=windows) and go test -tags nogui -count=1 ./internal/... under a temp HOME pass. 2026-10-03 23:01:47 +08:00
yetone 67092380fb plugin: pi packages run as pi runs them — a program a login starts can't take the host's stdin, a select is answered with its option's id, and the extensions get pi's runner, pi's auth.json and session_start (owner: 都补上,用真实账号测) — tried with real accounts, five pi packages had problems that the fake one didn't show. pi-devin-plus runs devin auth login with stdio "inherit" when it finds no Devin sign-in, and the program read magpie's next requests from the host's stdin as its answer ("Login canceled") while writing over its answers; host.js now gives every program started through Bun.spawn and Bun.spawnSync (all of node:child_process goes through them) nothing to read and stderr to write. pi-zcode asks its region with onSelect and options { id, label }, pi's own shape, and checks the id ("bigmodel"); pi.js answered with the label, so the login said "Login cancelled". Packages read their sign-in back from pi's auth.json (readStoredCredential: pi-zcode for its plans, pi-cursor for its token, pi-provider-qoder through the registry, pi-antigravity syncs its accounts through it) and register what they find on session_start (pi-zcode its plans' models, pi-devin-plus and pi-xai-oauth refresh their catalogs), and a provider registered after loading went into a queue nothing read. Now each package is bound with pi's ExtensionRunner as pi's session binds it (bindCore: no UI, tools or session of magpie's), so a later registerProvider reaches the runtime; each pi provider's first account is kept in $PI_CODING_AGENT_DIR/auth.json as pi keeps it, written when it changes; and a provider newly signed in, and each one signed in when the host starts, is sent session_start, after which magpie lists its models again if they changed. A runner that can't be made leaves the packages registered as before. TestPiPackage now has a select answered with an id, a login that starts a program on the terminal, and a session_start that reads the sign-in from pi's auth.json and adds a model; it fails on the old code (the label answered; the model never listed, auth.json empty) and passes. With real accounts in a sandbox HOME: pi-zcode signed in through Z.ai and lists 11 models (none before); its requests reach ZCode but the account's Start Plan answers 200 {"code":1005,"msg":"exceed quota limit"}, which pi-zcode reads as an empty answer (its bug). pi-provider-qoder signed in through Google, 16 models, tool round trips on Kimi-K3 and Efficient. pi-devin-plus signed in, its catalog refreshed on session_start (53 models), tool round trips on swe-2 and claude-opus-5.5; pi-xai-oauth (grok-4.7) and pi-antigravity (gemini-3.8-flash) tool round trips; @rahularya01/pi-cursor answers text (its agent doesn't call a client's tools, upstream). pi-provider-kiro wasn't signed in: it needs pi's terminal, a Builder ID or an IdC login. go vet, builds (darwin, GOOS=linux -tags nogui, GOOS=windows) pass. 2026-10-03 22:51:24 +08:00
Fanbuz b7c41ea2fe feat: 单独索引 Codex 归档会话并去除物理副本 (#698) 2026-10-03 22:45:46 +08:00
Fanbuz c9d0d15b94 fix: 记录网关实际返回的响应 ID (#693) 2026-10-03 22:45:42 +08:00
yetone a27bd9ec96 proc: the desktop app takes the agents' folder variables from the login shell, as it does PATH (atie on Discord: PI_CODING_AGENT_DIR set in the shell, magpie still wrote Pi's config to ~/.pi/agent/models.json) — magpie reads PI_CODING_AGENT_DIR, CODEX_HOME, CLAUDE_CONFIG_DIR and the rest with os.Getenv, but an app opened from the Finder, the Dock or the login item gets launchd's environment, with nothing a shell profile exports, so every agent was wired into its default folder in the home. proc.UserPath already asked the login shell for its PATH (sh -ilc printf with markers); the same single call now prints PATH and every variable in agentenv.Vars between the markers, parted by NULs, and each one magpie's environment doesn't have and the shell gives a non-empty value is set with os.Setenv; one magpie was started with is kept. These are read as soon as the app starts (the library is synced into the agents at once, the backend wires them), so UserPath now waits for the shell's answer up to 2 s instead of not at all; a slower profile still doesn't hold the window up longer, and what it says later is still taken. LoginPath uses the same call. Windows is unchanged (an app gets the user's environment from the registry there). Only the GUI's runGUI/runPanel call UserPath; no test but proc's own does. TestUserPathTakesAgentVars (a fake $SHELL whose profile exports PI_CODING_AGENT_DIR with a space, CODEX_HOME and an empty GROK_HOME) fails on the old code (nothing taken) and passes: Pi's folder is set by the time UserPath returns, magpie's own CODEX_HOME is kept, the empty and absent ones stay unset. Tried with a built binary calling proc.UserPath under env -i (launchd's PATH, a temp HOME): with /bin/zsh, .zshrc's PI_CODING_AGENT_DIR="$HOME/my pi" and CLAUDE_CONFIG_DIR came through, CODEX_HOME=/kept stayed, .zprofile's PATH folder was added, in 19 ms; with /bin/bash, .bash_profile's value came through. go vet, builds (darwin, GOOS=linux -tags nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestClaudeSignedOut, TestClaudeAccountNamedAsItsLogin, TestClaudeIdentityServedWhileAsked and TestDevinSignedInOnlyInMagpie, timing tests on fake CLIs (5 s waits) that pass when the package is rerun; none of them reaches UserPath. 2026-10-03 22:34:36 +08:00
yetone 30c072713d gui: the Gateway's models say what they take — reasoning levels, images, context at each row's end, in full in a tooltip — and the search finds a group by its models (ARNO on Discord: 希望网关里的模型列表支持搜索查找,支持显示模型或者模型组更多的信息,比如该支持的推理强度,是否支持图像等) — each model row on the Gateway page now ends, before its copy button, with small muted chips: the reasoning levels agents are offered ("low–xhigh", or "low / medium" for two; the levels kept, not all it has, and none for a model whose levels aren't known), a picture glyph when it accepts images, and its context ("400K", "1M"; the 1M badge after the name moved there). Their tooltip says it in full: "Reasoning: low, medium, high, xhigh", "Accepts images" or "Text only", "Context: 400,000 tokens". A routing group's are what agents are told of it, as of a model: /api/providers' gateway.groups now carry the catalog entry's efforts, images, context and output, and members (provider/model, in its order); its tooltip says images are every model's, the context the least of its models', its output, and names its models. The search (already there, shown from 8 models) shows from 5, has a tooltip saying what it matches, and also matches a group's models, so "kimi" finds the group sending to it. Strings in zh. TestGatewayGroupsSayWhatTheyTake checks a group's listed members and that its efforts, images, context and output are the catalog's; gateway-model-info.test.cjs (webkit) checks the chips and tooltips of models and a group, the search by id, name, provider and a group's models, Escape, and that a click on the chips picks the model without moving the row. Related GUI tests (gateway-*, click-scroll, list-sort, public-url, request-archive, sse-body, web-page, api-key-theme) pass; go vet, go build -tags nogui ./... and go test -tags nogui -count=1 ./... under a temp HOME pass but TestProvidersAnswerWhileListsComeIn, a timing test that passes alone. 2026-10-03 22:34:36 +08:00
yetone 4c7788424c gateway: a rate limit that comes back as soon as its rest is over rests longer each time, until the account answers (01huadalang on Discord: WorkBuddy 429s a free model while the account has credits, and magpie asks it again every minute) — a 429 the vendor gave no time for rested a minute, every time: an account refused again the moment it was back was asked once a minute forever, a request lost each time before the group's next model took it. The failover is magpie's (restAfter), so this is the same for the built-in and for the plugin, which passes WorkBuddy's 429 and its headers on as they are. Now rateRest counts the rate limits that come as soon as a rest ends (within half an hour of it) and rests 1, 2, 4, 8, 16 then 30 minutes; an answer starts it over (clearRest drops the note it counts by), one long after the last rest a minute again, and one while still resting — the last one left, tried anyway, or one request's own retries — keeps the count where it is. What the vendor says still counts: Retry-After or a reset header, or a reset time in the error as keepRetry notes it, when longer than the backoff. A rest is by the account (restKey) as before, by the model for OpenRouter's shared-pool free models. A usage that says the account has credits never put a resting account first (restLast orders after weighing); the test holds that. The Routing page says "rate limited again as soon as it was back (n times in a row)" for such a rest. TestRateLimitBacksOffWhenItComesBack fails on the old code (no count, a minute every time). go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestUserPath, TestSavedButSignedOutClaudeServed and one internal/gui test, which pass rerun. Not tried against WorkBuddy itself: its 429's body and headers weren't seen. 2026-10-03 22:34:36 +08:00
yetone 88fd366efb gateway: Gemini's thought signatures come back on its tool calls, through Pi and every other client (#687) — Pi on the built-in Google Gemini (an API key, gemini-3.8-flash at high) failed the turn after every tool call with 400 "Function call is missing a thought_signature in functionCall parts … default_api:bash, position 3". Gemini 3 signs the first call of each step it thinks through and wants the signature back; its OpenAI-compatible API gives it in tool_calls[].extra_content.google.thought_signature, which magpie relayed to Pi as it came, and Pi's openai-completions (like the AI SDK) keeps a call's id but drops extra_content, so the call went back unsigned. magpie's own translation (Claude Code, Codex, Gemini CLI on the same provider) never read the field at all. Now a call Gemini signed reaches the client with an id carrying the signature (<id>__ts__<signature as base64url>, letters, digits, _ and - only), which every client sends back as given: a Chat request relayed to Gemini has it taken off the call's and the result's ids and put in extra_content, and parse does the same for a translated request (the signature on the call's Part, given back by buildChat). A turn with no signature at all — another model's calls, or an id a client shortened — goes to Gemini with skip_thought_signature_validator on its calls, the value Google documents and the Code Assist path already sends; a later call of a signed step goes without, as Gemini signs the first. Any other upstream gets the calls' own ids, never the long ones. TestGeminiThoughtSignatureThroughPi (streamed and whole) and TestGeminiThoughtSignatureThroughAnthropic, against an upstream that checks signatures as Google does, fail on the old code with the issue's 400 and pass; TestGeminiCallsWithoutSignature and TestSignedID cover the fallback, other upstreams and the id's round trip. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./internal/gateway/... under a temp HOME pass. Not tried against generativelanguage.googleapis.com: there is no Gemini API key on this machine. 2026-10-03 22:34:36 +08:00
Kagari Mimiandhazukisendai 763eb60478 gateway: let a searching relay be named (#686)
* gateway: let a searching relay be named

A provider marked as searching by itself is still not picked automatically, so a relay quota is not spent for other models, but Settings can now name it as the searcher. The row and tests say both halves of that rule.

* gateway: keep named search relays out of fallback

Search ranking is automatic and fallback ranking again, while Settings additionally accepts a relay said to search. Manual-only choices come last and modelSearch skips them, so a failed automatic search never spends a relay quota; a provider on a searching host keeps its automatic rank even when its box is ticked.

---------

Co-authored-by: hazukisendai <251835060+hazukisendai@users.noreply.github.com>
2026-10-03 22:34:05 +08:00
Leo 39ec205faa fix: refine usage day selection and CSV filenames (#689)
* fix: refine usage day selection and CSV export

* fix: suppress default outline on usage day bars
2026-10-03 22:33:59 +08:00
mofelee ee68b96788 fix: avoid duplicate Pi interaction and gateway traces (#692) 2026-10-03 22:33:54 +08:00
Snowyy d62fecd468 plugin: reuse config parsing and avoid request body string copies (#696) 2026-10-03 22:33:49 +08:00
SadWood 6c46e5561c fix(gateway): 修复 Anthropic 图片来源字段混用 (#677)
图片来源按 base64 或 URL 输出各自字段,避免空的互斥字段导致 Factory 拒绝请求。
补充图片编码及已迁移 Factory 插件链路的回归测试,覆盖历史截图和后续文字请求。
2026-10-03 22:33:43 +08:00
yetone e8e317d419 gateway: a Chat Completions keepalive is a comment line alone, with no blank line after it — Crush (openai-go v2.7.1) failed every reply through magpie that had a keepalive in it with "Agent processing failed: failed to start agent processing stream: unexpected end of JSON input": its SSE reader takes every blank line as an event, data or not, and parses the empty data as JSON. The comment and blank line were ": keepalive\n\n"; now ": keepalive\n", which every reader skips (a blank line with no data is no event in the SSE spec, and openai-go's own reader skips a comment line). TestTranslatedStreamKeepsClientAlive now also wants no event without data (old code: 25). Found while testing every agent CLI through a sandbox magpie: crush run on magpie/codex/gpt-6-astra failed and passes now; claude, codex, gemini, opencode, pi, goose, droid, grok, cline, omp, agy and commandcode answered too. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestProvidersAnswerWhileListsComeIn, a timing test that fails on main without this change too. 2026-10-03 22:26:47 +08:00
yetone 57a87f0ab9 gui: code text on Windows is Cascadia Mono / Consolas with Chinese in YaHei UI, not SimSun (#690) — on a Chinese Windows 11 the Library's MCP tags (远程 · 需登录), the routing page's model chips (gpt-6.1-sol) and other small code text drew thin, unhinted and not round: the tags take --code, whose stack (ui-monospace, SF Mono, Menlo, Consolas, monospace) has no Chinese face, so their Chinese fell to the generic monospace, SimSun on a Chinese Windows; and routing.css's model chips and step lines, the sign-in sheet's link and code, the import textarea, the headers editor and What's new named stacks of their own (ui-monospace, SFMono-Regular, Menlo, monospace, or var(--mono, …) with no --mono anywhere) with no Windows face at all, so all their text was SimSun. Now every monospace text takes var(--code), and on Windows (html.win, as --font already is) --code is "Cascadia Mono", Consolas, "Microsoft YaHei UI", "Microsoft YaHei", monospace; the Mac's and Linux's stack is unchanged (the Mac's ui-monospace is SF Mono either way). win-fonts.test.cjs (no sheet names a monospace stack of its own; the badge, the id, the model chip and the sign-in link take the Mac's stack off Windows and Cascadia Mono / Consolas / YaHei UI with html.win) fails on the old CSS and passes in webkit and chromium; the routing, library, omarchy and signin-link GUI tests pass as before but routing-compact, routing-manual and routing-time, which fail the same on main. Not seen on a real Windows machine; the light grey of the ids (--faint) is unchanged. 2026-10-03 22:23:17 +08:00