Files
gateway: each gateway key can have its own limit, a token total and/or an estimated cost per day, week or month, and a spent key is refused before any provider is asked (margbug01 on #585, v0.1.671: 每个网关密钥都能设置自己的用量上限) — a gateway key named who called and the Usage page grouped its calls, but nothing held a key to an amount, so one client shared on the network could spend a subscription's whole window. A key now carries limit {period day|week|month, tokens, cost, cacheReads} in caller-keys.json, set with the new "limit-key" change. Windows are calendar ones in local time (from midnight, from Monday, from the 1st) and a call counts in the window it started in. Tokens are a call's uncached input, output and cache writes, plus cache reads when the limit says so; cost is an estimate at the Usage page's effective prices (usage.NewCoster), a call with no known price adding none and counted as unpriced. What a key used is summed from usage.jsonl (usage.Visit), so it survives a restart; the new internal/budget keeps the sums in memory per window and appendUsage writes a key's record through budget.Append, which appends and counts under one lock so a window read meanwhile never counts a call twice. Before a keyed POST reaches the mux, keyLimited reserves for it: its body's size in tokens (4 bytes a token) plus the key's mean output per call this window, at the window's cost per token or the named model's input price; it is let in while used + reserved is under the cap, so requests sent at once overshoot by about one call, and the reservation is released when the request ends, a streamed reply being settled then with the usage its vendor reported. A spent key gets a 429 in its API's error shape (Anthropic, OpenAI chat/responses, Gemini) naming the key, the cap and the reset time, with Retry-After, X-Should-Retry: false and X-Magpie-Limit-Reset, recorded as a rejected call (gateway_key_limit); other keys go on. Requests from this computer without a gateway key are not limited; a key used from this computer is. count_tokens and bridge callbacks are never held. GET /v1/magpie/limit tells a key its own status. Gateway keys rows show a Limit badge (what is used of it, per what, Spent in red) and a line under a limited key (used of limit, left, resets / refused until, in flight; unpriced calls in its title); Limit opens an editor under the row (window, tokens with k/m/万/亿, cost in US$, Count cache reads too, what is counted) staged until Save, marked unsaved, Cancel/Escape dropping it, a bad amount disabling Save, No limit removing it; en and zh; no left-border accent; no click scrolls. The CLI gains magpie gateway-key limit <id> [off | day|week|month --tokens N --cost USD --cache-reads] and a LIMIT column in list. TestKeyLimitRefusesBeforeUpstream (two calls within 700 tokens, the third a 429 the fake upstream never sees, message/type/headers, the rejected record, Anthropic's shape on /v1/messages, another key and a keyless loopback request let in, still refused after budget.Forget as after a restart, a raised and a removed limit let in, /v1/magpie/limit), TestKeyLimitHoldsParallelRequests (20 at once against 2000 tokens: some refused, at most one call over, no reservation left; without the reservation all 20 got in and 7350 were used), TestKeyLimitWindowResets, TestLimitWindow, TestLimitValid and TestGatewayKeyLimitCLI are new; gateway-key-limit.test.cjs (WebKit, en and zh) is new and passes, as do gateway-caller-keys, gateway-connect-keys, api-key-theme and click-scroll (whose request-pick case flaked once under a parallel run and passes alone, as on main). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState's known TempDir-cleanup flake aside).
aside: magpie picks Aside's model, its thinking level, its four task models and its picture model (#789)
agent: AtomCode support — one provider account, one model table per catalog model, as its own sign-in writes (#736)
portable: a portable magpie on Windows keeps WebView2's profile in data\webview2 and makes no Start-menu shortcut, and an installed one makes the shortcut once and never puts back one the user deleted (#508, suixinex: 便携模式时还是会创建 C:\Users\<username>\AppData\Roaming\magpie.exe\ 文件夹; 开始菜单的快捷方式删除后再打开magpie会重新添加) — the AppData\Roaming\magpie.exe folder is WebView2's user data folder: Wails v3 passes WindowsOptions.WebviewUserDataPath to go-webview2 as its DataPath, and left empty it is %APPDATA%\<exe name>, which portable mode (appdir) never set. The app's Windows options (windowsOptions) now give it appdir.WebView(): data\webview2 when portable, "" (WebView2's default, so the pages' storage and cookies there are kept) when installed. The shortcut came back because shortcut.Ensure ran at every start of the desktop app and wrote Magpie.lnk whenever it was missing or stale, with nothing recording that it had been made. Ensure now does nothing when portable (no shortcut, no App Paths entry); installed, it makes the shortcut on the first start and records that in start-menu-shortcut in magpie's folder (with the exe), keeps one that is still there pointed at the exe (moved exe, update), and makes none once the file is recorded and the shortcut is gone. A user who deleted it under an older magpie isn't given it back on updating either: a missing shortcut with no record but an App Paths\magpie.exe entry already there (every older magpie wrote it at each start) counts as deleted and is recorded as such. The Start menu, the shortcut writer and App Paths are behind function variables, so the decision is tested with fakes on any OS. Checked for other leaks: autostart only refreshes an entry the user turned on (Refresh never enables), the magpie:// link registration is needed for import links and stays, as appdir's doc says, as do temp folders; the update stages beside the exe, caches already follow appdir. Not changed: the Mac's WKWebView and Linux's WebKitGTK keep their own data in the profile (Wails has no option for it), which #508 didn't report. Nothing ran on Windows (the test box isn't reachable): no real WebView2 was started with the data path and no real shortcut was made or deleted. TestEnsureOnce, TestEnsureAfterOlderMagpie, TestEnsurePortable (fake Start menu in a temp folder), TestWebView and TestWindowsOptionsWebViewData fail without the change. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build and vet ./..., GOOS=windows go test -c for shortcut, gui and appdir, go test -tags nogui -count=1 ./... under a temp HOME pass (TestUsageKeepsTimeToFirstToken and TestUserPath failed once on timing, pass rerun alone; untouched).
routing: the groups are put in an order of the user's, as the Agents page's rows are, and /v1/models lists them so (#779)
gateway: each gateway key can have its own limit, a token total and/or an estimated cost per day, week or month, and a spent key is refused before any provider is asked (margbug01 on #585, v0.1.671: 每个网关密钥都能设置自己的用量上限) — a gateway key named who called and the Usage page grouped its calls, but nothing held a key to an amount, so one client shared on the network could spend a subscription's whole window. A key now carries limit {period day|week|month, tokens, cost, cacheReads} in caller-keys.json, set with the new "limit-key" change. Windows are calendar ones in local time (from midnight, from Monday, from the 1st) and a call counts in the window it started in. Tokens are a call's uncached input, output and cache writes, plus cache reads when the limit says so; cost is an estimate at the Usage page's effective prices (usage.NewCoster), a call with no known price adding none and counted as unpriced. What a key used is summed from usage.jsonl (usage.Visit), so it survives a restart; the new internal/budget keeps the sums in memory per window and appendUsage writes a key's record through budget.Append, which appends and counts under one lock so a window read meanwhile never counts a call twice. Before a keyed POST reaches the mux, keyLimited reserves for it: its body's size in tokens (4 bytes a token) plus the key's mean output per call this window, at the window's cost per token or the named model's input price; it is let in while used + reserved is under the cap, so requests sent at once overshoot by about one call, and the reservation is released when the request ends, a streamed reply being settled then with the usage its vendor reported. A spent key gets a 429 in its API's error shape (Anthropic, OpenAI chat/responses, Gemini) naming the key, the cap and the reset time, with Retry-After, X-Should-Retry: false and X-Magpie-Limit-Reset, recorded as a rejected call (gateway_key_limit); other keys go on. Requests from this computer without a gateway key are not limited; a key used from this computer is. count_tokens and bridge callbacks are never held. GET /v1/magpie/limit tells a key its own status. Gateway keys rows show a Limit badge (what is used of it, per what, Spent in red) and a line under a limited key (used of limit, left, resets / refused until, in flight; unpriced calls in its title); Limit opens an editor under the row (window, tokens with k/m/万/亿, cost in US$, Count cache reads too, what is counted) staged until Save, marked unsaved, Cancel/Escape dropping it, a bad amount disabling Save, No limit removing it; en and zh; no left-border accent; no click scrolls. The CLI gains magpie gateway-key limit <id> [off | day|week|month --tokens N --cost USD --cache-reads] and a LIMIT column in list. TestKeyLimitRefusesBeforeUpstream (two calls within 700 tokens, the third a 429 the fake upstream never sees, message/type/headers, the rejected record, Anthropic's shape on /v1/messages, another key and a keyless loopback request let in, still refused after budget.Forget as after a restart, a raised and a removed limit let in, /v1/magpie/limit), TestKeyLimitHoldsParallelRequests (20 at once against 2000 tokens: some refused, at most one call over, no reservation left; without the reservation all 20 got in and 7350 were used), TestKeyLimitWindowResets, TestLimitWindow, TestLimitValid and TestGatewayKeyLimitCLI are new; gateway-key-limit.test.cjs (WebKit, en and zh) is new and passes, as do gateway-caller-keys, gateway-connect-keys, api-key-theme and click-scroll (whose request-pick case flaked once under a parallel run and passes alone, as on main). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState's known TempDir-cleanup flake aside).
codex: a cached entry keeps the cache's own max when magpie's list has none, and Codex's models_cache.json is read under CODEX_HOME everywhere (follow-up to #674, from reviewing #679) — restoring a cached native entry (backend away) set max_context_window to max(MaxContext, Context) from magpie's live list, so a list saved without the max (before magpie kept it) wrote the window over the cache's own (872000 became 272000); now a list with no max leaves the cache's, and codexcat.Window still raises it to a window past it. codexcat.CacheEntries and catalog.Codex read ~/.codex/models_cache.json even with CODEX_HOME set, while codexVersion and codexInstructions followed it; all four now take catalog.CodexModelsCache() (CODEX_HOME, else ~/.codex), and provider's codexCLIHome and the copy in codexInstructions are gone. TestCodexReadsCacheUnderCodexHome, TestCodexCachedListUnderCodexHomeKeepsItsMax and TestCodexCatalogEntryFromCodexHome fail on the old code (the ~/.codex cache read, none listed from CODEX_HOME); TestCodexNativeModelsTakeSetWindows now wants a cached gpt-5.5 with no live max to keep the cache's 400000 (old code: 272000). 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 alone.
agent: AtomCode support — one provider account, one model table per catalog model, as its own sign-in writes (#736)
fx: a rate fetched behind a page for another home sets nothing in this one, and the gui tests ask no rate of the network (follow-up to #541) — fx.Soon asks for a stale rate behind the page; in the gui tests a state read with no rate cached so reached open.er-api.com, and its answer, landing seconds later, was taken as the rate by whichever test ran then (TestFXStateDoesNotWait saw the live 6.71 where its fake endpoint said 7.31). A fetch whose cache path is no longer the one in force now returns without touching the in-memory rate or the failure time, and internal/gui's TestMain points the rate at a closed local port. go test -tags nogui ./internal/gui ./internal/fx pass three runs of four (the fourth, TestFXInState's TempDir cleanup finding .cache not empty, the flake it had before).
imagemcp: an image that takes longer than the agent gives a tool call is answered "still being made" before the agent gives up, and is saved all the same (#575: 图片生成的mcp经常遇到超时问题 — "Tool execution timed out after 30000ms" in one agent, "Error: Request timed out" in another; the agent then looked for the file, found none, and asked for the image again, and again timed out) — generate_image held its tools/call open until the gateway's image came back, and an image model (gpt-image at quality high, 1024x1536, through a subscription) often takes a minute or more, while agents give an MCP tool call as little as 30 s, the MCP SDK's default is 60 s, and only Codex and Goose have a timeout magpie writes for them (tool_timeout_sec, timeout). So the call was cut short, the generation went on unseen, and the agent's retry started another one that was cut short too. Now each generate_image and generate_video call runs as a job that goes on to its end whatever becomes of the call, and the call waits for it at most answerWithin (20 s, under the shortest of those limits): what is ready by then is answered as before (errors too); what is not is answered, not as an error, that it is still being made, where it will be saved, not to ask for it again, and an id for the new tool generation_result, which waits on the job the same 20 s at a time and answers its paths once it is saved. A call given a progressToken is sent notifications/progress every 5 s while it waits, for clients that wait longer on progress, and
magpie mcp image waits for jobs still running when the agent closes it (up to ToolTimeout) so what is being made is saved. TestSlowImageAnswersBeforeTheAgentGivesUp (a gateway taking 700 ms against a 150 ms answerWithin: the call answers in time with an id and progress notifications, generation_result then answers the saved file, the gateway was asked once, an unknown id is a tool error) fails without the change (the call took 703 ms and answered only when the image was saved). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState's known TempDir cleanup flake aside); go test -race ./internal/imagemcp passes.
library: a repository's skills are turned on or off for an agent at once from its group's heading (#787, mintonight: 我记得昨天还能直接一次性开关一个作者的所有技能包,怎么今天不行了) — the heading of a repository's group (owner/repo, N skills) showed which agents had its skills as plain icons, which looked like the rows' chips but did nothing, so its skills could only be switched one row at a time (or every skill in the library at once). The icons are now chips, as on a row, with All ahead of them: a chip is lit when its agent has every one of the group's skills, half lit (dashed, the icon dimmed) when it has some, and a click gives that agent every skill of the group or, when lit, takes them all; All gives them to every agent shown, or takes them from every one when all have them. Other groups' skills and agents not clicked keep what they have; a click doesn't fold the group. The page sends the group's skill names to a new library/skills/agents-some, library.SomeSkillsAgents, which is EverySkillAgents for the skills named alone (one write; a name not in the library is refused). zh and ja strings. TestSomeSkillsAgents (on and off for named skills only, the agents' folders follow, an unknown or empty name list refused) and group-skill-chips.test.cjs (WebKit, en and zh: lit/half-lit, a click on a half-lit chip, a lit chip, All on and off, the requests sent, the group not folded) — the latter fails on the old code. gui-ja, list-sort, own-skill, shared-skills, every-skill, library-all, library-toggle-status, library-items, remove-all-skills, import-all-skills, library-row-open-scroll and click-scroll pass on WebKit. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
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.
proc: the login-shell probe asks nushell through /bin/sh, and a value the shell printed instead of expanding counts as unset (#738: with nushell as the login shell, shellEnv's printf '…' "$PATH" "$CLAUDE_CONFIG_DIR" … came back as the text "$PATH", "$CLAUDE_CONFIG_DIR", … — a double-quoted string is literal in nushell — and setUnset exported every one of agentenv.Vars with its own name as the value; Claude sign-in then failed with "mkdir $CLAUDE_CONFIG_DIR: read-only file system" (putClaudeLogin's MkdirAll of that relative name under the GUI's cwd /), and the account lapsed minutes later as "Claude Code is no longer signed in", its sign-in looked for under that folder; Codex, Pi, omp and the rest were pointed at "$CODEX_HOME", "$PI_CODING_AGENT_DIR", "$OMP_PROFILE" the same way, in the GUI alone, which is the one caller of UserPath) — shellProbe builds the command: for a shell named nu it is ^/bin/sh -c '<the same printf>' (nushell has no POSIX quoting to ask with and its PATH is a list, while a child of it gets the environment as any child does, PATH joined back into one string, and sh expands the references; the outer string in nushell's single quotes where a backslash is a backslash, the format in sh's double quotes where \0 reaches printf), any other shell keeps the printf it had; and parseShellEnv takes a value that is the variable's own reference ("$CODEX_HOME", or "$PATH" for PATH) for unset, so a login shell with other quoting than POSIX leaves a variable alone rather than setting it to its name. TestShellEnvNushell runs the probe through a nushell on the machine (skipped where there is none) and checks the command's shape anyway; TestParseShellEnvUnexpanded feeds the unexpanded answer. Verified against nushell 0.116.0: PATH comes back, CODEX_HOME from the environment reaches sh, unset variables are "". (#739)
test: one list of the agent folders a sandbox clears, so no sandbox misses an agent (yetone, on #522: A shared list of env vars for the other sandboxes would be a welcome follow-up) — every sandbox kept its own list of the variables that point at an agent's folder, and each had drifted: the gateway's cleared 5 of the 32 magpie reads, the provider package's 2, the tui's 10, the library's 20. A sandbox clearing 2 meets whatever Grok, Kimi, Hermes, MiniMax, Qoder or ZCode the developer has installed, which is how #522 came about — Qoder's two were in the library's list and in none of the four that failed. internal/agentenv is now the one list, and the 21 sandboxes that fake an environment with no agent in it clear it, each keeping beside it the variables that are not an agent's: APPDATA and LOCALAPPDATA, which a sandbox sets to a folder of its own rather than clearing, VISUAL and EDITOR. A name in the list is one magpie reads, and TestVarsAreRead walks the sources to say so, which is how two names a sandbox had been clearing turned out to be read by nothing at all — GEMINI_CLI_HOME and OPENCODE_CONFIG in internal/backup's, while OPENCODE_CONFIG_DIR, the one internal/agent reads (agents.go:545), was not in it. Where a sandbox set an agent's folder to a fixture of its own, the clearing now comes first, since a list of 32 wipes what a list of 5 left standing. Not converted: the omp tests' four variables, which are the ones omp's own lookup consults (a Pi fork) rather than every agent's, and the warm-up's Claude Code traffic flags, which are not folders — CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC stays out of the list for that reason too, its one test setting it on purpose. On Windows 11 the whole suite answers as it did before the change: 31 packages pass, and the same 15 tests in internal/library, internal/gateway and internal/agent fail for what they failed for before (symlinks this machine has no privilege for, and TestSuffixModesReachAgentFiles). go vet ./..., go build -tags nogui (windows, GOOS=linux, GOOS=darwin), and go vet -tags nogui for linux and darwin over every package touched pass. (#543)
providers: each model's credit rate shows by it in the provider's model list and an agent's model picker, Qoder's 0.5× and WorkBuddy's 0.03×, a discount's price before it struck through (01huadalang on Discord: qcoder 或者 workbuddy 能不能实时抓取或者按一段时间抓取能展示列表模型的积分消耗倍率,这样不用打开软件查看,能选择到便宜的模型) — magpie read each model's price only to tell free from not: Qoder's price_factor (
06584e15) and WorkBuddy's "credits" ("x0.03", wbFreeCredits), so the lists said FREE or nothing, and picking a cheap model meant opening Qoder or WorkBuddy. catalog.Model now keeps the price, Rate (the credits a request costs, as a multiple; 0 is not listed, or Free) and RateWas (the price before a discount running now), and carries both through provider.Entry, agent.Option and the providers page's models. Qoder (and Qoder CN): Rate is the price_factor; RateWas an active promotion's before_promotion_price_factor, else an original_price_factor above the price (Qwen3.8-Flash: free, 0.1× struck through), and a promotion's 0 that isn't free is its price before times its discount_factor. WorkBuddy and WorkBuddy AI (the CodeBuddy plan; both builds read the same /v3/config): Rate is the credits (wbCredits; x0.00 stays FREE, an unreadable or infinite rate is none). A plugin's model gives it as it gives free: rate and rateWas on the model its provider.models hook lists, a number (0.5) or as the vendor writes it ("x0.03", "0.5×"), which host.js reads (rateOf) into plugin.Model.Rate/RateWas and pluginCatalog keeps; the moved Qoder and WorkBuddy users get it once their plugins send it. The badge is small and grey (the context badge's colours, no uppercase), beside FREE where that shows, its title saying what a request costs (每次请求消耗 0.5× 积分;折扣前 / 限时免费,原价). Rates are as of the provider's last list: a built-in's is fetched at start-up when it has none, on Refresh and when its key changes, a plugin's when its plugins are asked again (start-up, a sign-in, a plugin change); nothing asks on a timer. Kiro, Cursor, ZCode and Devin list no per-model rate magpie reads, so they show none. TestParseModelsRate (qoder) and TestWBCreditsRate (provider) are new and don't build without the change; TestFakePlugin now checks a plugin's rate given as a number and as "x0.03" (0 for both with host.js not reading them). model-rate.test.cjs (WebKit, en and zh: the chips' badges and titles, struck-through prices, grey not green, no stripe, a click picks and scrolls nothing, the picker's badges) fails without the change and passes with it; click-scroll, models-free-only, model-pick and favorite-star pass in WebKit. No request was made to Qoder, WorkBuddy or any vendor. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME, go test ./internal/gui pass.
gateway: a request archive, off unless turned on, keeps each call's headers and bodies in the user's own S3 bucket and a recent call reads its own back (Jorben on Discord: 请求存档) — the Gateway page's recent calls only ever held the last bodies in memory, the first 256 KB of each, with no headers, gone at a restart. A switch over Recent calls ("Request archive", POST /api/settings/archive, settings' requestArchive) turns it on, refused with a reason while sync isn't set up with an s3:// bucket; with it on, each call the gateway serves gets an id (UTC HHMMSS-<16 hex>, said back in X-Magpie-Archive-Id) and, once it is recorded, its method, path and query, request headers and body and response status, headers and body go as one JSON file to <prefix>/magpie/archive/<YYYY-MM-DD>/<id>.json in the bucket sync keeps its backup in, signed with the same client (davsync.Bucket, no new SDK). Before anything leaves, Authorization, x-api-key, Cookie, Set-Cookie, any header or JSON field or query parameter named for a key, token, secret or password, and anything that looks like a key or token anywhere in the text (the redaction rules' patterns, in a stream's data lines too) become [REDACTED] — gone, no placeholder kept; the archive is plain JSON, readable in the bucket, not sealed with the passphrase. Uploads go one at a time on their own goroutine from a queue of 16 calls, each body capped at the 256 KB the call already keeps and marked truncated; a full queue drops the call with a log line, a failed upload is logged and shown under the switch until the next one goes, and no request waits on any of it. A recent call the archive kept shows its date and id under its bodies, and "Fetch from archive" (GET /api/archive?date=&id=, only a date and an id of that shape) shows the request's and response's headers and bodies as the bucket has them, or says it isn't there yet. The hook is in record() and the Call literal, so PR #239's request log lines are not touched; the archived call carries its usage, and with #239 its vendor request id. Only calls through serve() are archived, not Codex's backend, image or video calls. TestArchiveOn, TestArchiveOff, TestArchiveName, TestRequestArchiveToS3 (a real gateway request through the signature-checking fake S3), TestScrub and request-archive.test.cjs fail without the change; TestSettingsSaveKeepsWhatItDoesNotSend now sets requestArchive. A sandbox magpie web with a fake vendor and a fake S3 server uploaded nothing while off; on, it uploaded a signed archive with none of eight planted secrets in it, read it back through /api/archive and in the page, and with the S3 server down served the call in 0.1 s and said why the upload failed. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (Claude account tests and TestClaudeRunEndsWhenTheClientLeaves pass alone); request-archive, click-scroll, old-webkit, sse-body, gateway-fold pass.
portable: a portable magpie on Windows keeps WebView2's profile in data\webview2 and makes no Start-menu shortcut, and an installed one makes the shortcut once and never puts back one the user deleted (#508, suixinex: 便携模式时还是会创建 C:\Users\<username>\AppData\Roaming\magpie.exe\ 文件夹; 开始菜单的快捷方式删除后再打开magpie会重新添加) — the AppData\Roaming\magpie.exe folder is WebView2's user data folder: Wails v3 passes WindowsOptions.WebviewUserDataPath to go-webview2 as its DataPath, and left empty it is %APPDATA%\<exe name>, which portable mode (appdir) never set. The app's Windows options (windowsOptions) now give it appdir.WebView(): data\webview2 when portable, "" (WebView2's default, so the pages' storage and cookies there are kept) when installed. The shortcut came back because shortcut.Ensure ran at every start of the desktop app and wrote Magpie.lnk whenever it was missing or stale, with nothing recording that it had been made. Ensure now does nothing when portable (no shortcut, no App Paths entry); installed, it makes the shortcut on the first start and records that in start-menu-shortcut in magpie's folder (with the exe), keeps one that is still there pointed at the exe (moved exe, update), and makes none once the file is recorded and the shortcut is gone. A user who deleted it under an older magpie isn't given it back on updating either: a missing shortcut with no record but an App Paths\magpie.exe entry already there (every older magpie wrote it at each start) counts as deleted and is recorded as such. The Start menu, the shortcut writer and App Paths are behind function variables, so the decision is tested with fakes on any OS. Checked for other leaks: autostart only refreshes an entry the user turned on (Refresh never enables), the magpie:// link registration is needed for import links and stays, as appdir's doc says, as do temp folders; the update stages beside the exe, caches already follow appdir. Not changed: the Mac's WKWebView and Linux's WebKitGTK keep their own data in the profile (Wails has no option for it), which #508 didn't report. Nothing ran on Windows (the test box isn't reachable): no real WebView2 was started with the data path and no real shortcut was made or deleted. TestEnsureOnce, TestEnsureAfterOlderMagpie, TestEnsurePortable (fake Start menu in a temp folder), TestWebView and TestWindowsOptionsWebViewData fail without the change. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build and vet ./..., GOOS=windows go test -c for shortcut, gui and appdir, go test -tags nogui -count=1 ./... under a temp HOME pass (TestUsageKeepsTimeToFirstToken and TestUserPath failed once on timing, pass rerun alone; untouched).
…
usage: magpie usage and the TUI's Usage page show the agents' own calls, read from their session files, beside the gateway's, and a Codex-based client's calls aren't counted twice (Kumo31 on Discord: Codex used directly, not through magpie, was on the window's Usage tab but not in magpie usage) — magpie usage and the TUI only ever summed the gateway's log (usage.Summarize); the calls the agents' session files record that the gateway never saw (Codex on the user's own ChatGPT sign-in, Claude Code, Claude Desktop…) were merged only into the ledger the window's Requests tab and the tray panel read (#239). usage.Direct sums those rows of the ledger (Source "log", this computer's, rejected ones left out) by agent, model, account and session, priced as the ledger prices them, and magpie usage prints them after the gateway's tables under "not through magpie · the agents' own requests, read from their session files" (a model whose provider the file doesn't tell reads "local session/<model>"); the TUI's Usage page shows the same block under the gateway's, each with its totals and agents' and models' tables. The two parts are separate, so nothing is counted in both; a period with only the agents' own calls shows them under "no calls through magpie". All of magpie usage's output now goes to the writer it is given. Checking it against the owner's real data showed the ledger counted a Codex-based client's calls twice: miyi-helper's requests through the gateway are logged under its own name while its Codex session file names Codex, and the gateway/session-file matcher keyed on the agent, so they never matched; the matcher now keys on the session, tokens and time alone (a session id names one conversation), which also corrects the Requests tab. With read-only copies of the owner's ~/.config/magpie and four days of ~/.codex/sessions in a sandbox HOME: magpie usage week printed 177 calls not through magpie, in 1.3M, out 52.4K, cache read 3.9M, gpt-6-astra 154 and gpt-6-sol 23 — the same rows, to the token, as usage.LedgerOf (what /api/usage/requests serves) gave for the period; before the matcher fix, today's 7 miyi-helper calls showed in both parts. TestDirectCountsOnlySessionFileCalls (the miyi-helper case included), TestUsageShowsCallsNotThroughMagpie and TestUsagePageShowsCallsNotThroughMagpie fail on the old code. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
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.
usage: magpie usage and the TUI's Usage page show the agents' own calls, read from their session files, beside the gateway's, and a Codex-based client's calls aren't counted twice (Kumo31 on Discord: Codex used directly, not through magpie, was on the window's Usage tab but not in magpie usage) — magpie usage and the TUI only ever summed the gateway's log (usage.Summarize); the calls the agents' session files record that the gateway never saw (Codex on the user's own ChatGPT sign-in, Claude Code, Claude Desktop…) were merged only into the ledger the window's Requests tab and the tray panel read (#239). usage.Direct sums those rows of the ledger (Source "log", this computer's, rejected ones left out) by agent, model, account and session, priced as the ledger prices them, and magpie usage prints them after the gateway's tables under "not through magpie · the agents' own requests, read from their session files" (a model whose provider the file doesn't tell reads "local session/<model>"); the TUI's Usage page shows the same block under the gateway's, each with its totals and agents' and models' tables. The two parts are separate, so nothing is counted in both; a period with only the agents' own calls shows them under "no calls through magpie". All of magpie usage's output now goes to the writer it is given. Checking it against the owner's real data showed the ledger counted a Codex-based client's calls twice: miyi-helper's requests through the gateway are logged under its own name while its Codex session file names Codex, and the gateway/session-file matcher keyed on the agent, so they never matched; the matcher now keys on the session, tokens and time alone (a session id names one conversation), which also corrects the Requests tab. With read-only copies of the owner's ~/.config/magpie and four days of ~/.codex/sessions in a sandbox HOME: magpie usage week printed 177 calls not through magpie, in 1.3M, out 52.4K, cache read 3.9M, gpt-6-astra 154 and gpt-6-sol 23 — the same rows, to the token, as usage.LedgerOf (what /api/usage/requests serves) gave for the period; before the matcher fix, today's 7 miyi-helper calls showed in both parts. TestDirectCountsOnlySessionFileCalls (the miyi-helper case included), TestUsageShowsCallsNotThroughMagpie and TestUsagePageShowsCallsNotThroughMagpie fail on the old code. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.