Files
magpie/internal
TryWorld 0fc6544e53 gateway: a 429 from a key's queue is in the usage ledger, as any other turn-away (#956)
The last candidate turned away by a key's or account's queue answers 429 with
Retry-After, and shows in the Routing trace and the recent-calls ring — but no
lane refusal reached the usage ledger, so Usage, the CSV export and the failure
filter never said it happened.

The final write is guarded by `call.To != ""`, and call.To is only set inside
s.attempt, which a lane refusal never reaches (the attempt is gated on
`err == nil`). Every other turn-away that happens before any provider was asked
goes through turnedAway(), which records it with Rejected: true: an unknown
model, a usage cap, an account barred, a sealed subagent task.

turnedAway's ledger half is now `logged`, and the lane branch calls that alone:
serve still reaches its end and writes the one recent-calls entry it always
wrote, so the refusal is one entry in Recent calls and one row in the ledger.
Calling turnedAway here recorded it twice.

Unchanged: the 429, its Retry-After, which members are skipped, and that nobody
rests for it (#892).

TestQueueRefusalIsInTheUsageLedger fails without the change — the ledger holds
no rejected 429 after a full queue — and passes with it, asserting exactly one
recent-calls entry and exactly one rejected 429 row;
TestUnknownModelRefusalIsInTheUsageLedger is the contrast that shows an unknown
model's refusal was recorded all along. go vet ./... and the builds for darwin,
GOOS=linux and GOOS=windows pass, and the lane tests pass under -race -count=20.
2026-10-06 11:27:57 +08:00
..
gateway: a gateway key can be held to some accounts (#905, LCorleone) — a key could burn any account a provider had: two coding plans of one vendor, and a key given to someone had them both. A key now keeps an optional list of accounts and keys (caller-keys.json "accounts", none = every account, so older files read as before): "<provider>/<account>", an account by its stable id — the login's, set once it is written and kept through renames (logins.json's savedLogin id, written by every write and stood in for by one made of the name until then, so an entry made before stands), a key by its fingerprint — every one a provider has when the list is set, and one gone later (a key rotated, an account signed out) kept as it is, matching nothing, holding the key closer, never wider. The command line and the menu name an account by who is signed in and keep its id. Routing holds a plan to the key's accounts beside its models: a candidate whose account the key may not use is left out — through a routing group the key names no less, for naming a group is not naming its accounts — the plan's order keeping step and its left-out list marking what the key held out (Weighed's held); magpie's own calls for the key — a web search, a picture described, a Codex title — are of the models the user picked, so the models don't hold them, but their spend lands on an account all the same, and the accounts hold them too. When every account or key a model has is one the key may not use, the request is refused with a 403 in the API's own error shape naming the model and the accounts the key may use, logged as a rejected call: after a usage cap's 429 and before an account barred for the model's, itself before the no-member 404. /v1/models and the Anthropic and Gemini lists show the key only the models an account or key it may use serves (a provider's account set not to serve one, #474, is none of the key's); a provider with no account and no key (a local ollama) is one the key names no account of — its models stay listed and its asks pass — while an entry kept after its provider lost its last account or key serves none, holding the key closer. The embeddings, images and videos endpoints hold their candidate to the key's accounts the same way, and counting tokens counts on those alone (the local estimate where it may use none). System One asks its decision model of an account or key the key may use — and sends on that key, of those in use, not the provider's first, which a key held to a later one was spent on; the videos and images keyed paths send on one the key may use too, held to a later key no longer refused for the first alone. Set it in the app's menu the models are picked in — the accounts after the models, an account by who is signed in with their plan in the note, a key by its fingerprint, what the CLI kept that no provider has now kept as "Not signed in now" — or magpie gateway-key accounts <id> codex/me@example.com openai/<key id> (all takes it off); gateway-key list has an ACCOUNTS column. en/zh/ja/de, the German "{n} accounts" main already had left to it rather than written twice. The images API's fan-out over a subscription's accounts is held to the key's accounts as one: one of the key's whose plan refuses to draw no longer hands the pen to an account it may not use, and the second in the order, when it is the key's, draws what the first may not (drawOnAccounts filters the candidates, the account pinned to with the account header told plainly when the key may not use it). The Routing view's left-out list says an account the key held out for what it is, not that its plan lacks the model, in Chinese, Japanese and German too; the key's badge names its accounts by who is signed in, not by the stable ids it keeps. (#932)
2026-10-06 10:51:50 +08:00
awake, gateway, gui: Settings › General › Keep awake while agents work keeps the computer from sleeping by itself while agents work through magpie (xiao_wang24004 on X) — the computer went to sleep in the middle of an agent's task. With the new setting on (off by default, this computer's own: KeepOwn), the magpie serving the gateway holds the system's idle sleep off while a request is in flight or a Claude Code turn waits on its tool results, and for ten minutes after the last request ended (an agent running a build or the tests between requests), looking every 5 s, and lets go once idle or turned off. On a Mac it runs caffeinate -i -w <magpie's pid> (PreventUserIdleSystemSleep, gone with magpie), on Windows SetThreadExecutionState(ES_CONTINUOUS|ES_SYSTEM_REQUIRED) from a thread of its own, on Linux systemd-inhibit --what=idle:sleep (Pdeathsig; said once in the log and nothing else where it is missing). The display may still turn off, and closing the lid still sleeps. en/zh/ja/de. Tested with a real DeepSeek account through magpie web --gateway in a sandbox HOME: a streamed request had caffeinate asserting on behalf of magpie's pid in pmset -g assertions, and turning the setting off through /api/settings released it within a tick. TestKeep (held once while busy and within the grace, let go past it, when off and when Keep ends; -race) and TestKeepAwakeKeptAndOwn are new; keep-awake.test.cjs (WebKit and Chromium, en and zh: off by default, On/Off post keepAwake, another setting's save keeps it, no scroll, no left border, shown on a browser's page) fails without the change; lightweight and click-scroll pass in WebKit. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass.
2026-10-05 19:46:02 +08:00
windows: an account list read while another magpie writes it is waited for, not taken for no accounts (Packing1 on Discord, Windows 11: 早上更新重启后之前登录的Antigravity订阅供应商和账号都没了 … 整行都没了,供应商里面antigravity直接消失了) — on Windows a file being renamed over (every write magpie makes: a temp file renamed onto it) is, for a moment of up to ~2ms, "The system cannot find the file specified" to anyone else looking at it, with MoveFileEx and with POSIX-semantics renames alike. filememo.Read gave that back as an error, readLogins as no accounts, and whatever change came next (any account's read-modify-write) wrote the empty list back: every built-in account gone, its subscription with it. Measured on Windows 11 (10.0.0.132): one process replacing a 200 KB file, another stat-ing and reading it, saw the file "not there" in a few windows each 20 s. Now steady.Stat and steady.ReadThere wait out a moment of a file not there (15 tries a millisecond apart, Windows only), and filememo looks through them, remembering a file found not there so one that really isn't costs the wait once. A magpie started by an update now waits for the one it replaces before its start-up moves write any file (AwaitPredecessor at the top of run, not only in the GUI). TestLoginsKeptWhileAnotherMagpieWrites (Windows: two writers replacing logins.json, readLogins never without the Antigravity account) failed 1 in 3 and 2 in 8 runs on the old code ("read as no accounts 8 times"), passed 11 of 11 on this; filememo's TestReadWhileReplaced the same (1180 misses on the old code); TestStatWaitsOutAReplace everywhere. A real update in a sandbox home on the Windows box, magpie-cli 0.1.880 → 0.1.942 from the release, kept the account and logins.json byte for byte. go vet (darwin, and windows nogui), builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-05 10:57:45 +08:00
gui: a model with a fast mode is sent fast, or not, from its row in the agent's picker (#954, Gyarados4157) — Fast was only a routing group member's (Group.Fast), so a GPT on a ChatGPT account picked straight in Codex or OpenCode could only go fast through a group of one. A model provider.CanFast says has a fast mode (a ChatGPT account's GPT, an OpenAI key's Responses GPT/o-series, Cursor's -fast, Claude Opus on an Anthropic key) now has a bolt beside its star in the agent's picker and in the same picker opened from a provider's row; a click posts /api/agent-fast and switches it for that agent (settings FastPicks, by agent, "<provider>/<model>", moved by a provider rename and kept by the Settings page's save) without changing the pick or closing the picker; a refusal puts it back. The field shows a bolt after a model sent fast. The gateway asks for the vendor's fast mode (service_tier "priority" / speed "fast", as for a group member) on that agent's requests for that entry, every account and key of it, not its fallbacks, another agent's requests or the same model at another provider; the Routing trace marks the try Fast. A routing group is never switched: its members say it. zh/ja/de added. fast-pick (window and 440px panel, en/zh/ja/de, Chromium and WebKit; the bolt only on models with a fast mode, the post, the refusal, the provider row's picker) passes and fails without the fix; TestFastPick (gateway, Chat/Responses/Anthropic in, fails with the gateway hunk off), TestSetFastPick, TestFastPickOptions and the settings-keep test (fails without the carry) pass with -race; i18n-keys, gui-ja, gui-de, favorite-star, agent-models-picker, claude-picker-fold and click-scroll pass; go test ./internal/gui/ ./internal/provider/ ./internal/agent/ ./internal/gateway/ ./internal/settings/ pass.
2026-10-06 11:15:52 +08:00
library, imagemcp: magpie's own image server is given to agents in WSL, started through WSL's interop with the paths crossing over (#900, cjyrainbow) — on Windows the Library names magpie-image by this machine's binary ('D:\tools\Magpie\magpie-windows-amd64.exe' mcp image), and turning it on for "Codex · WSL Ubuntu" or "Claude Code · WSL Ubuntu" failed: a Windows program was refused for every agent in WSL. WSL's interop starts a Windows .exe from inside the distro, so magpie's own server (args mcp image, whatever its binary) is now written for an agent in WSL by the binary's path in the distro, asked of the distro itself (wslpath -u, so a custom automount root is followed; asked once in ten minutes; /mnt/<drive>/ when it can't be asked), with --wsl <distro>, --mount where the drives aren't under /mnt/, --home the distro's $HOME, and WSLENV naming the variables it is given (MAGPIE_ADDR), which a Windows program otherwise doesn't get. A distro whose interop is off (no WSLInterop in binfmt_misc) is told so: turn it on in /etc/wsl.conf. A user's own Windows program is still refused, as before: its paths and variables wouldn't cross over. A Linux magpie in the distro isn't looked for: the Windows one reaches the gateway at 127.0.0.1 and reads this machine's settings, the same version as the app, which a Linux one would need the gateway's address from inside WSL for. With --wsl, magpie mcp image opens the paths the agent gives (its roots, reference images, where to save, ~/) as the distro's — /home/me/x through \\wsl.localhost\<distro>\home\me\x (\\wsl$\ where that isn't), /mnt/c/x as C:\x — answers the saved paths as the distro's (/home/me/app/generated-images/…, /mnt/c/…), and with no project falls back to the distro's ~/Pictures/Magpie. Tested on the Windows 11 test box: in a WSL2 Ubuntu 24.04 distro the library's probe gave /mnt/c/…/magpie.exe and interop:on, and this build, started from the distro as the library writes it (the .exe's /mnt/c path, mcp image --wsl, MAGPIE_GATEWAY through WSLENV to a stand-in gateway in the distro), answered initialize and generate_image with a root of /home/yeton/m900proj: a relative path and an absolute reference image (sent as /v1/images/edits), ~/m900proj/home.png and /mnt/c/… were each saved there and answered as the distro's paths. TestWSLMagpieImage (the /mnt/d command, --wsl, WSLENV, nothing rewritten or asked again at the next sync, a custom mount, interop off as a problem) and TestLinuxHome in internal/library, TestWSLPaths in internal/imagemcp are new and fail without the change; TestWSLServers (npx.cmd still refused) passes. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass (internal/provider failed once under load and passes alone).
2026-10-05 20:09:34 +08:00
plugin, gateway, gui: a plugin can be gateway middleware — onRequest, onEvent and onResponse run in magpie's gateway on what agents send and get back (moejs) — a plugin could only be a subscription provider, run in the Bun host, a process away from the gateway. A plugin may now declare middleware (package.json magpie.middleware, or a *.middleware.js / .mjs file), run in-process by moejs (pure-Go JS engine): compiled once, one runtime per request from a pool, so every hook of a request shares ctx.state. onRequest(body, ctx) sees the agent's body in its own API before routing; what it returns is the body the next middleware and the gateway get, undefined leaves the bytes as they were, ctx.reject(status, msg) answers with an error in the agent's API's shape. onEvent(ev, ctx) sees each SSE event's JSON (export const events = [...] filters by name; an object changes it, null drops it, undefined passes the bytes through untouched; data: [DONE] is no JSON and passes by). onResponse(body, ctx) sees a whole JSON reply, with ctx.status. ctx has protocol, model, stream, path, agent, the plugin's options and state. A hook that throws, returns a non-object or runs past its limit (250ms request/response, 50ms an event, 1s load; interrupted) leaves the request or reply as it was and is counted and logged (60 lines a minute); a compressed reply passes as-is. Files are reloaded within a second of a change. Only agents' requests run through it (serveAgent at the gateway's agent entry points and the Codex backend), never magpie's own titles, classifier or stand-ins; it sees bodies before redaction. A middleware-only package is skipped by the Bun host. The Plugins page marks a middleware with a chip, its hooks, calls and µs per call (JSON in and out counted), failures in red with the last error, and a load error. en/zh/ja/de; docs/reference.md and usemagpie.ai/docs/plugins#middleware (en/zh/ja) with an alias example. Tested with a real DeepSeek account through magpie web --gateway in a sandbox HOME: an alias middleware served "fast" and named it in message_start, a reject came back as 403 permission_error, a Chat stream uppercased; real Claude Code (sandbox CLAUDE_CONFIG_DIR) did Read, Bash and Glob tool turns through three middlewares with 0 failures: ≈1–1.6µs an event, 0.2–0.7ms an onRequest that parses and rewrites Claude Code's ≈80KB body. Real use found two bugs fixed here: [DONE] parsed as JSON (SyntaxError on every Chat stream) and stats leaving out the JSON time. internal/middleware tests (chain, undefined keeps bytes, reject, fail-open on throw/spin/string with the runtime reused, async hook, load errors, off and Bun skip, reload, split/CRLF/filtered/dropped stream events, [DONE] untouched, whole reply and gzip, 64 in parallel; BenchmarkEvent ≈0.9µs, 11 allocs) and TestMiddlewareThroughGateway (alias routed, events by protocol, redaction still upstream, reject in Anthropic's shape; fails without the change) are new; plugin-middleware.test.cjs (Chromium and WebKit, en and zh: chip, hooks and stats, failures with last error, broken and off, no left border) fails without it. The other plugin GUI tests and click-scroll pass in WebKit. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass (internal/provider failed once under load and passes alone).
2026-10-05 19:14:31 +08:00
gui: a model with a fast mode is sent fast, or not, from its row in the agent's picker (#954, Gyarados4157) — Fast was only a routing group member's (Group.Fast), so a GPT on a ChatGPT account picked straight in Codex or OpenCode could only go fast through a group of one. A model provider.CanFast says has a fast mode (a ChatGPT account's GPT, an OpenAI key's Responses GPT/o-series, Cursor's -fast, Claude Opus on an Anthropic key) now has a bolt beside its star in the agent's picker and in the same picker opened from a provider's row; a click posts /api/agent-fast and switches it for that agent (settings FastPicks, by agent, "<provider>/<model>", moved by a provider rename and kept by the Settings page's save) without changing the pick or closing the picker; a refusal puts it back. The field shows a bolt after a model sent fast. The gateway asks for the vendor's fast mode (service_tier "priority" / speed "fast", as for a group member) on that agent's requests for that entry, every account and key of it, not its fallbacks, another agent's requests or the same model at another provider; the Routing trace marks the try Fast. A routing group is never switched: its members say it. zh/ja/de added. fast-pick (window and 440px panel, en/zh/ja/de, Chromium and WebKit; the bolt only on models with a fast mode, the post, the refusal, the provider row's picker) passes and fails without the fix; TestFastPick (gateway, Chat/Responses/Anthropic in, fails with the gateway hunk off), TestSetFastPick, TestFastPickOptions and the settings-keep test (fails without the carry) pass with -race; i18n-keys, gui-ja, gui-de, favorite-star, agent-models-picker, claude-picker-fold and click-scroll pass; go test ./internal/gui/ ./internal/provider/ ./internal/agent/ ./internal/gateway/ ./internal/settings/ pass.
2026-10-06 11:15:52 +08:00
gui: a model with a fast mode is sent fast, or not, from its row in the agent's picker (#954, Gyarados4157) — Fast was only a routing group member's (Group.Fast), so a GPT on a ChatGPT account picked straight in Codex or OpenCode could only go fast through a group of one. A model provider.CanFast says has a fast mode (a ChatGPT account's GPT, an OpenAI key's Responses GPT/o-series, Cursor's -fast, Claude Opus on an Anthropic key) now has a bolt beside its star in the agent's picker and in the same picker opened from a provider's row; a click posts /api/agent-fast and switches it for that agent (settings FastPicks, by agent, "<provider>/<model>", moved by a provider rename and kept by the Settings page's save) without changing the pick or closing the picker; a refusal puts it back. The field shows a bolt after a model sent fast. The gateway asks for the vendor's fast mode (service_tier "priority" / speed "fast", as for a group member) on that agent's requests for that entry, every account and key of it, not its fallbacks, another agent's requests or the same model at another provider; the Routing trace marks the try Fast. A routing group is never switched: its members say it. zh/ja/de added. fast-pick (window and 440px panel, en/zh/ja/de, Chromium and WebKit; the bolt only on models with a fast mode, the post, the refusal, the provider row's picker) passes and fails without the fix; TestFastPick (gateway, Chat/Responses/Anthropic in, fails with the gateway hunk off), TestSetFastPick, TestFastPickOptions and the settings-keep test (fails without the carry) pass with -race; i18n-keys, gui-ja, gui-de, favorite-star, agent-models-picker, claude-picker-fold and click-scroll pass; go test ./internal/gui/ ./internal/provider/ ./internal/agent/ ./internal/gateway/ ./internal/settings/ pass.
2026-10-06 11:15:52 +08:00
windows: an account list read while another magpie writes it is waited for, not taken for no accounts (Packing1 on Discord, Windows 11: 早上更新重启后之前登录的Antigravity订阅供应商和账号都没了 … 整行都没了,供应商里面antigravity直接消失了) — on Windows a file being renamed over (every write magpie makes: a temp file renamed onto it) is, for a moment of up to ~2ms, "The system cannot find the file specified" to anyone else looking at it, with MoveFileEx and with POSIX-semantics renames alike. filememo.Read gave that back as an error, readLogins as no accounts, and whatever change came next (any account's read-modify-write) wrote the empty list back: every built-in account gone, its subscription with it. Measured on Windows 11 (10.0.0.132): one process replacing a 200 KB file, another stat-ing and reading it, saw the file "not there" in a few windows each 20 s. Now steady.Stat and steady.ReadThere wait out a moment of a file not there (15 tries a millisecond apart, Windows only), and filememo looks through them, remembering a file found not there so one that really isn't costs the wait once. A magpie started by an update now waits for the one it replaces before its start-up moves write any file (AwaitPredecessor at the top of run, not only in the GUI). TestLoginsKeptWhileAnotherMagpieWrites (Windows: two writers replacing logins.json, readLogins never without the Antigravity account) failed 1 in 3 and 2 in 8 runs on the old code ("read as no accounts 8 times"), passed 11 of 11 on this; filememo's TestReadWhileReplaced the same (1180 misses on the old code); TestStatWaitsOutAReplace everywhere. A real update in a sandbox home on the Windows box, magpie-cli 0.1.880 → 0.1.942 from the release, kept the account and logins.json byte for byte. go vet (darwin, and windows nogui), builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-05 10:57:45 +08:00