lc asked in #feedback (message 1558300896142696549) for magpie's masking
to cover more of what identifies a person or a machine. Under Settings >
Privacy > Mask personal data, each kind now has its own Off/On
(settings.redactKinds, redact.Options.Kinds). What each one masks:
- US SSNs: dashed numbers, except ones never issued (000, 666 and 9xx
areas) and well-known examples. Undashed ones only next to "SSN" or
"social security".
- Passport numbers, next to passport, Reisepass, 护照 or パスポート.
- IBANs: compact or in groups of four, with the country's length and the
mod-97 check.
- Birthdays: a date only next to birthday, DOB, born, 生日 and the like.
A date on its own stays.
- MAC addresses, but not documentation examples or all-decimal groups.
- Serial numbers, next to S/N, serial or 序列号.
- The user name in /Users/x, /home/x and C:\Users\x. The name is masked
and the path is kept.
- User and host in a shell prompt or an ssh/scp/rsync target (user@host
goes out as two placeholders, not as an email address), and on
hostname/user lines whose value ends the line. Code such as
`hostname = socket.gethostname()` is left alone.
- Public IPv4 and IPv6 addresses. Private, loopback, link-local, CGNAT,
documentation, multicast and well-known resolver addresses stay, and so
do version numbers.
- Cloud bucket names in s3://, gs://, oss://, cos://, abfss:// and
storage URLs. The object path is kept.
The last four are off by default. Agents send their working folder, the
prompt and log lines in nearly every request, and the model then has to
copy the placeholder exactly inside a path or URL. The first six are
rare and sensitive, so they are on. Each placeholder is restored in
replies, in streamed text and in tool-call JSON. Context rules run only
in a window around their words (rule.matches), so masking with the
defaults on 400KB of app.js takes 136ms (112ms before; 222ms with every
kind on).
Tests, run at this head:
- gofmt -l is clean. go vet passes on darwin, GOOS=linux and
GOOS=windows (-tags nogui) for internal/redact, gateway, gui and
settings.
- go test -tags nogui ./... under a temp HOME with pinned caches passed
everywhere except internal/proc's TestNoCommandBypassesProc. That
failure came from 2da8fbe4, and b934d64a fixed it on main. After the
rebase, the changed packages and internal/proc were run again and
passed. New tests:
- internal/redact TestMoreKinds, TestMoreKindsLeaveAlone (versions,
timestamps, hashes, UUIDs, model ids, code, ordinary dates, URLs in
code), TestCategoryDefaults, TestMoreKindsKnown, TestMoreKindsJSON.
- internal/gateway TestRedactedKinds.
- internal/gui TestRedactKindsAreRedacts.
- I broke the IBAN checksum by hand and TestMoreKindsLeaveAlone went
red.
- GUI tests pass in Chromium and WebKit:
- the new redact-kinds.test.cjs (en, zh, zh-TW, ja and de, at 1100
and 640 px);
- i18n-keys, gui-de, gui-ja, gui-zh-tw, redact-rule-row,
privacy-hides-accounts, privacy-usage-stats, settings-groups,
settings-sections and mobile-web;
- 29 more files that open the Privacy page (252 tests).
privacy-usage-stats now scrolls with the wheel to reach Count me as a
user, which the new rows push below its 760px window. WebKit doesn't
let a script scroll Settings.
- Real gateway check: a `magpie serve` built from this tree, under a
sandbox HOME, on 127.0.0.1:3492, with a fake OpenAI upstream on 3491.
An Anthropic /v1/messages request went to the upstream with each kind
as a placeholder. Turning SSNs off and home folders on changed what
was sent the same way. The streamed reply came back with every value
restored.
What changed: the Sessions page lists sessions seen only through the
gateway (from the usage ledger, read-only, a native session of the same id
wins), and shows in gateway mode. A Sessions switch, off by default and
confirmed before it turns on, records each gateway exchange's text under
the config dir (scrubbed, at most 7 days and 256 MiB, cleaned hourly).
Settings sync and restore can't turn it on; POST /api/sessions/recording
turns it on or off and can clear what was kept.
Why: users of magpie as a gateway (gateway mode, LAN clients) had no way
to see their conversations; the owner chose this design on the PR.
Squashed from PR head 6a343ba1 onto origin/main. Conflicts: gui-shell.md
(kept main's placeMenu text, took the PR's Sessions in gateway mode) and
settings_keep_test.go (main's fields plus GatewayConversations).
Run: go build ./..., go vet on gateway, gui, sessions, settings, usage;
go test -race -timeout 900s on those five packages: all ok.
Codex's lead names each subagent's model itself in spawn_agent, and
Codex's [agents] config can't overrule it. The Agents page's Codex row
now has a "subagent model" square (the app's model picker, Default
first). Its options are only models a ChatGPT account in magpie serves,
because a subagent's task is sealed for ChatGPT accounts.
- settings.CodexSubagentModel holds it. It is an agent Field
(subagent_model), so the TUI/CLI, profiles, davsync's "agents" part
and WSL twins carry it too. The setter refuses another provider's
model. It keeps a ChatGPT account's model that isn't served now, so a
backup restored on another computer still applies (ApplyFields stops
at the first failed Set). POST /api/settings keeps it.
- The gateway (codexSubagentPick) moves a collab_spawn request onto it.
codexBackend does this for Codex's own bare models; serve() does it
for any other model. When no ChatGPT account on in magpie serves it,
or a held gateway key may not use it, the request stays on the model
asked for. Route.Subagent records asked/to/kept. The lead's turns,
titles, guardian and the rest are never moved. Off (the default)
changes nothing.
- Routing page: kindWhy says the subagent was put on the model set, or
stayed on its own and why. "only account on for" names the model it
went on. Strings added in zh, zh-TW, ja and de.
- docs/subsystems: gateway-routing.md and agent-wiring.md describe it.
Run (darwin, rebased on origin/main):
- gofmt -l internal/: clean.
- go vet -tags nogui on gateway, provider, agent, gui and settings:
darwin, GOOS=linux and GOOS=windows.
- go build ./...
- HOME=<temp> go test -tags nogui ./...: all ok.
TestSettingsSaveKeepsWhatItDoesNotSend flagged the new field; it is
now set there.
- GUI tests, Chromium + WebKit:
- codex-subagent-model.test.cjs: 20/20 (en, zh, zh-TW, ja, de at
1100 and 560 px).
- Also green: routing-kind, subagent-effort, codex-memories,
codex-memories-settings, i18n-keys, gui-ja, gui-de, gui-zh-tw and
click-scroll.
Failing without the change (codexSubagentPick returning nil):
- TestCodexSubagentPutOnTheModelSet
- TestCodexSubagentOnAnotherProviderPutOnTheModelSet
- TestCodexSubagentModelNotServedKeepsTheAsked
- TestCodexSubagentModelHeldKeyKeepsTheAsked
TestCodexSubagentModelLeavesOtherRequests guards the other side: the
lead's turns, titles and guardian stay untouched.
Not tried with a real Codex CLI and ChatGPT account. A subagent spawn
needs a signed-in Codex home, and this session may not touch the live
~/.codex. The fake ChatGPT backend tests cover both routes.
holdWriter.Write, which every streamed try goes through (passed through or translated; a key's, a built-in subscription's, or a plugin's fetch, as a moved WorkBuddy's), feeds a loopGuard (loop_guard.go). It reads the reasoning and the text of each protocol's deltas (Anthropic, Chat, Responses, Gemini) and cuts each into units: lines and sentences, compared without case or a closing stop. A list's "1." or a decimal doesn't end a sentence. A unit with no word in it (braces, rules, 0x00, hashes, rows of numbers) isn't counted. When the last 256 counted units of either are among 24 or fewer distinct ones, the try is cut. What came with that write goes out, then the protocol's stream error (Anthropic error event, Chat error chunk, Responses response.failed server_error, Gemini 502 UNAVAILABLE), naming the loop and quoting its units, and the vendor's request is cancelled. The cycle needn't be exact; the reporter's has 9 units in two variants. It falls back to nobody and rests nobody (fail "loop"), and the usage record's err_type is reply_loop. A reply sent whole (no stream, or Gemini without alt=sse) isn't read. Settings › Models › Replies › Stop looping replies (noLoopGuard, on by default) turns it off; new strings in zh, zh-TW, ja and de.
Tested:
- loop_guard_test.go, with the screenshot's 28 lines cut into random 1–7 byte pieces, as one line, and in Chinese. It trips within the window. It reads every protocol's shapes, and tool-call arguments never trip it.
- No trip on magpie's own sources fed as both reasoning and text: gateway.go, fallback.go, anthropic.go, app.js, i18n.js, app.css, routing.js, sessions.js, index.html, docs, LESSONS.md and go.sum. Nor on generated numbered lists, logs, CSV, hex, rules and base64.
- Through the gateway on all 4 protocols with a fake DeepSeek, and on WorkBuddy both built-in and through the community plugin's fetch in the plugin host (TestLoopEndsWorkBuddysReply). In each case the agent gets the reasoning and then the error, the vendor is let go within 2000 lines, nobody rests, and usage keeps reply_loop.
- gateway.go as 4,900 lines of reasoning passes whole on every protocol. With the setting off, a loop runs to its end.
- With the guard removed, the end-to-end and WorkBuddy tests fail on every route.
- The built binary (magpie serve on 127.0.0.1:3592, temp HOME, fake DeepSeek upstream), checked with curl on chat, messages, responses and gemini alt=sse. Each loop ended with that protocol's error, the upstream was let go after 256–271 lines, and the usage log had err_type reply_loop and routing fail "loop". The long gateway.go reasoning came through to [DONE], message_stop, response.completed and STOP. With noLoopGuard, all 100,000 lines passed.
- GUI: loop-guard-setting.test.cjs in Chromium and WebKit, en/zh/ja/de, at 1100 and 420 wide. gui-ja, gui-de, gui-zh-tw, i18n-keys, member-model-setting, click-scroll and settings-sections/groups pass.
- go vet, GOOS=linux/windows vet and builds, go test -race ./internal/gateway ./internal/settings, and the full go test -tags nogui ./... all pass.
Not tried with the real WorkBuddy or DeepSeek model.
nianlee-official asked for an agent whose model list grows only by hand:
today the Agents page's per-agent list hides models one at a time, and
every new model, of a new provider or an old one, shows up in the
agent's picker and config on its own ("New models are shown").
What changed:
- settings: PickedModels, per agent. An agent named in it is shown only
the catalog entries listed for it (after Visible); HiddenModels is not
read for it.
- provider: ModelOff(agent) is the one test CatalogFor, the Agents page
count and Codex's own ChatGPT models (CodexNativeHidden) use, so the
gateway's /v1/models for that agent's key, the lists magpie writes into
its config and Codex's /model all narrow the same way hidden models do.
SetOnlyPicked switches it on or off keeping what is shown as it is
(on: what is shown becomes the picks; off: the listed models not
picked become hidden). SetPickedModels keeps a pick whose provider is
missing for now. renameModelPrefs moves picks with a provider rename.
A model asked for by name still resolves against the full catalog.
- GUI: the list's foot "New models are shown" note is now a switch
"Only models I pick", with a tooltip saying what each side does. On,
a click sends what is ticked ({shown}), not what is hidden, so a model
that arrived after the list was opened isn't picked by accident; the
server still reads {hidden} from an older page. The model in use is
always kept. Hide all / Show all's armed state takes the switch's room.
zh, zh-TW, ja and de strings; the old note string is gone.
- CLI: magpie visible <agent> --only-picked | --show-new; magpie visible
lists such an agent as "only models picked"; magpie models <agent>
says how many are not picked, and what to do when none is.
- /api/settings keeps PickedModels like HiddenModels.
- docs/reference.md: the per-agent list bullet.
Not touched: #1302 (bulk agent visibility) and its files.
Run:
- go test -tags nogui ./... in a temp HOME: 44 packages ok.
- New: provider TestOnlyPickedModels, gui TestAgentModelsOnlyPicked,
gateway TestOnlyPickedModelsList (opencode's key: a new provider's and
an old provider's new model are left out of /v1/models, cursor's key
has them, and a chat request naming fresh/x1 reaches the upstream) and
TestCodexModelListOnlyPicked. With ModelOff made to ignore picks, all
four fail.
- GUI: new agent-models-only-picked.test.cjs, Chromium and WebKit, en/zh/de,
420 and 1100 wide (12/12). With save() sending {hidden} in pick mode it
fails. agent-model*, agent-models*, codex-model-order, cursor-local-models,
desktop-model-menu, click-scroll, gui-ja, gui-de, gui-zh-tw: 85/85.
- gofmt -l clean; go vet -tags nogui ./... on darwin, GOOS=linux and
GOOS=windows; go build, and linux/windows builds.
- The built CLI in a sandbox HOME: visible codex --only-picked, a model
added after it is left out of `magpie models codex` and kept in claude's;
--show-new keeps it out and new ones show again.
Not tried: the real window against a live gateway, or a real agent
reading the narrowed config.
Claude Desktop reads magpie's gateway models and, for every row whose
max_input_tokens is 1,000,000 or more and whose id has no [1m] suffix,
adds a second "1M context window" entry; an id that already ends in [1m]
gets one entry (BIt/PIt/JLt in Desktop's index chunk). So every 1M model
showed twice. A new switch under Agents → Claude Desktop, "Only each
model's largest window" (settings desktopLongest, off by default), lists
such a model by its [1m] id. Models under 1M keep their real window;
fast/non-fast, providers and groups stay separate rows. The base id still
resolves (aliased, Resolve and SwitchedOff trim [1m]), so sessions saved
on it keep working. Desktop needs a restart to re-read the list.
Verified:
- TestClaudeDesktopLongestOnly fails without the change ("on: 6 entries
of 4 rows") and passes with it; the [1m] and old plain ids reach
upstream as the same model.
- Desktop's own discovery and menu code, extracted from a copy of
app.asar, run over magpie's output: off 4 rows → 6 entries, on 4 rows
→ 4 entries.
- desktop-longest.test.cjs (Chromium and WebKit, en/zh, 1100/640px)
times out on origin/main's app.js and passes 8/8; gui-ja, gui-de,
gui-zh-tw, i18n-keys, desktop-model-menu, desktop-tiers,
agent-models-redraw and click-scroll pass 44/44.
- go test (sandbox HOME, nogui) gateway, gui, settings, agent pass;
go vet for darwin, linux and windows.
- Full GUI suite: 3802/3809. agent-expand-steady, panel-arrange,
redact-rule-row, remote-magpie and signin-again pass alone;
context-window fails alone with origin/main's assets too.
Not verified: the real Desktop menu after a restart (its live config was
not touched).
On Windows magpie lists the running distros and probes each one for agents,
their sessions and Claude Code. Etsuya233 asked for a way to turn that off.
settings.NoWSLAgents (per machine, kept by KeepOwn) now gates every
auto-discovery path: agent.wslAgents/wslHomes/WSLRunning, sessions'
wslFiles/wslDirs (via the new sessions.WSLOff hook), and wslrun.Find, which
the Claude subscription uses to run Claude Code in WSL. The toggle is shown on
Windows only and is on by default.
Verified:
- red without the change: TestWSLDetectionOff (1 WSL agent with detection
off; with only sessions reverted, a distro's session is listed),
TestFindWithDetectionOff (Find returned Ubuntu after asking wsl.exe).
- go test -tags nogui ./... in a sandbox HOME: every package ok except
TestPluginLoginTakesTheLapseOff, which timed out at 63.7s under the full run
and passes alone (-count=3) and with its package (-count=1); it doesn't
reach WSL.
- go vet -tags nogui on agent, wslrun, sessions, settings and gui for darwin,
linux and windows.
- GUI: wsl-agents-setting (en/zh/zh-TW/ja/de, 900 and 440px) 30/30;
gui-ja, gui-de, gui-zh-tw, i18n-keys, keep-awake, lightweight,
settings-sections, settings-groups 44/44 in Chromium and 44/44 in WebKit.
- Windows box (10.0.0.132), nogui magpie.exe in a sandbox profile with a
logging wsl.exe first on PATH: on, `magpie agents` listed Claude Code · WSL
magpie-desktop after 3 wsl.exe calls; off, `agents`, `ls` and `sessions`
made no wsl.exe call and wrote no wsl.json.
Not tried: the toggle in the real Windows GUI window, and Claude Code in WSL
through the Claude subscription with the setting off (unit-tested only).
Sinnhu asked for more than one daily warm-up time (e.g. 09:00, 15:05,
19:10). Settings has codexWarmAts / claudeWarmAts beside the old single
keys: the list wins when set (an empty one is off), a file from before
it reads its one time as a list of one, and the first time is written
back to codexWarmAt / claudeWarmAt so an older magpie still reads one.
Times are kept as HH:MM, sorted, once each; a bad one is refused.
The scheduler (dayStart, nextDayStart, dailyDue, heldForDay,
dayStartPassed, warmLook) takes the list: the start due is the latest
of any time that has passed, and the day kept per window is now that
start ("2026-09-28 15:05"), so each time sends once a day. A day kept by
an older magpie ("2026-09-28") counts as every start of that day had,
so nothing is sent twice on the day of the update. An account's own
time (codexWarmAtOf) stays one time.
Settings › Allowances › Daily warm-up: each time is a time field with ×
beside it while another is left, + adds one a window after the last,
Off/On as before. The times wrap in a group of their own beside + and
Off/On, so adding or removing one never moves the button pressed. New
strings in zh, en, zh-TW, ja, de.
Verified:
- go test -tags nogui ./internal/provider ./internal/settings
./internal/gui (temp HOME, pinned caches, pipefail): ok. go build and
go vet -tags nogui ./...; GOOS=linux and GOOS=windows go vet -tags
nogui on the three packages: clean.
- Fails without the change: TestWarmAtSeveralTimes (dayStart looking at
the first time only, or a day-only key: "sent at 06:00"),
TestWarmLookAndHoldAtSeveralTimes ("15:05 not looked at"),
TestWarmAtKeptDayFromBefore and TestDailyDue (an old day-only record
not honoured: "sent again"), TestWarmAtsMigrate (old key not read:
"old file: [] ..."; first time not written back: "old file:
["06:00"] "" ...").
- GUI: warm-at-times.test.cjs, Chromium and WebKit, en/zh/ja/de, 900
and 440px: 16/16; with warm-at-own, settings-groups, gui-ja, gui-de,
gui-zh-tw, i18n-keys: 36/36 in each engine.
Whole GUI suite (node --test, concurrency 4): 3716/3724; the 8 red
(agent-models-redraw, azure-endpoint, context-window,
gateway-key-limit, login-import, member-model-setting, search-api)
each pass run alone with this change, and none reaches the warm-up
row.
Not tried with real accounts or a live day of the scheduler.
Settings → Language offers 繁體中文 beside 简体中文. On System, a
browser or OS locale of zh-TW, zh-HK, zh-MO or zh-Hant (zh_TW.UTF-8 too)
shows zh-TW; zh-CN, zh-SG and zh-Hans-HK keep Simplified. The tray menu
and quota alerts follow the same choice. Release notes fall back to
Simplified.
The zh-TW strings are generated from the zh strings with opencc
(cn→twp), then corrected to Taiwan wording: 設定, 檔案, 帳號, 伺服器,
預設, 閘道, 連接埠, 參數, 選單, 透過, 取得, 用戶端, 存取, 終端機, 「」
quotes. A quota window is 時段 and a context window stays 上下文視窗.
Number units use 萬/億, and #unitsRow is shown for zh-TW.
Verified:
- New gui-zh-tw.test.cjs, in Chromium and WebKit:
- key parity, placeholders and tags against zh;
- no Simplified characters;
- the Taiwan words;
- locale detection for each system tag;
- <html lang>, nav labels, fmtN units, the picker;
- Settings Language at 1000px and 440px.
With the sysLocale zh-TW line removed, it fails. On the base assets,
all 3 fail.
- gui-ja, gui-de and i18n-keys pass.
- go vet, and go test -tags nogui for ./internal/gui and
./internal/settings, pass. That includes the new zh-TW cases in
TestTrayLang / TestTrayLabels / quota_alert_test, which fail without
the zh-TW branches.
A page on http://localhost:3000 asking /v1/models got a 404 to its
preflight and no Access-Control-Allow-Origin, so the browser hid every
answer. Settings › Network › Web pages now takes origins (cleaned to
scheme://host[:port]; "*" and anything that isn't an origin are refused,
said in the page's language). For a listed origin corsGuard answers the
preflight (204, the asked header names, Private Network Access) and adds
Access-Control-Allow-Origin; every other request from it needs an enabled
gateway key, even from loopback, or gets a readable 401. Default: none
listed, and a request with any other Origin or none (every agent's) is
unchanged. A save of the rest of the Settings page keeps the list.
Verified: TestCORSOrigins (gateway, loopback requests: preflight, keyed
calls, keyless/managed-token/turned-off key refused, unlisted origins incl.
"null" and port/scheme variants get nothing, agents untouched) fails
without corsGuard in Handler (preflight 404); TestCleanOrigins;
TestCORSOriginsSetting fails without keeping CORSOrigins in POST
/api/settings; TestSettingsSaveKeepsWhatItDoesNotSend. GUI
cors-origins.test.cjs (en/zh, 440px, add by button and Enter, refusals,
remove, no scroll, no left border) in Chromium and WebKit fails without
renderCORS; gui-ja, gui-de, i18n-keys, gateway-port, lan-docker,
redact-rule-row, settings-* pass in both. go test -tags nogui ./... green;
-race -count=5 on the new tests; darwin, linux nogui, windows builds.
click-scroll's two Requests-page subtests fail the same on origin/main.
* gui: choose installed interface and code fonts and styles
Discover installed styles through DirectWrite, CoreText and Fontconfig. Keep choices per computer, apply them before paint and notify both desktop webviews after saving; defaults retain platform and Omarchy fallbacks.
Verified seven focused Go tests, including real Windows discovery (90 families, 276 styles), 14 font browser cases in Chromium/WebKit, 68 settings/zoom/Omarchy/web cases, go vet, Windows amd64/arm64 and Linux nogui builds. Against the original assets, the new UI test fails with Settings must offer an interface font picker (0 != 1). Full Windows suites have separately reproduced upstream failures; macOS/Linux native discovery is covered by the added CI step.
* gui: retain bundled Mac fonts and variable style precision
Only filter an undownloaded CoreText face when it is explicitly downloadable; bundled fonts are installed without being downloaded. Use resolved font names when descriptors omit them. Preserve fractional weights and nonnegative CSS widths for named variable styles.
* gui: restore the authored font cascade when returning to defaults
Apply family-only trait overrides only while a custom face is active. The browser regression catches code under a bold parent becoming 700 instead of its original 400. Keep the compatibility shim first, and apply boot fonts before visible content.
* gui: keep font picker review separate from CI integration
Leave the native test sources and manual verification command with the feature. Move the added workflow step to a follow-up so the font picker can take the normal review path requested on PR #1007.
On a MacBook the window had last been zoomed. magpie skipped a maximised
window when it saved the size, so it reopened at an older, nearly full
size: 1712×1083 at (8,34) on a 1728×1084 work area, neither full nor the
size it was given nor the default.
settings.WindowMaximised (this computer's own, KeepOwn) now records
whether the window was maximised when it settled; Window stays the size
it restores to (settle). A window not shown yet, let go, full screen or
minimised, or on Linux hidden, records nothing.
Reopened maximised: on the Mac and Windows placeMain maximises the made
window just before its first Show (on Windows once its page has come);
Wails' StartState is undone by the Mac's centring and shows a Windows
window before its page. The least size Maximise drops is put back. On
Linux makeMain sets StartState, since GTK can't be asked about a window
not mapped yet.
A size kept on a larger screen: Windows fits the window to the work area
and centres it, title bar in reach, and keeps the larger size for the
larger screen (fitted); the Mac fits and zooms it itself, and magpie
doesn't zoom it again, so it can still be restored.
TestSettle fails with upstream's rule (5 of 9 cases); TestOpenSize,
TestFitRoom, TestWindowMaximisedKeptAndOwn; the Mac's real build under a
sandbox HOME, 11 cases.
TestWarmAtEachAccountsOwn, TestWarmLookAtAnAccountsOwnTime and TestSetCodexWarmAt pass with -race -count=20, and the first two fail when an account's own time is ignored. TestSettingsSaveKeepsWhatItDoesNotSend covers the new field and fails without the keep in api.go. warm-at-own (two accounts: rows, Own posts 06:00 then 09:00, Off, back to Same, the page doesn't move, nothing cut, no <select>; one account: no rows) passes in Chromium and WebKit, en/zh/ja/de, 900px and 440px, and fails 8 of 9 with main's app.js. i18n-keys, gui-ja, gui-de, settings-groups, settings-sections, click-scroll, reset-reminder, usage-alert and codex-auto-review pass in both engines; go test ./internal/provider ./internal/settings ./internal/gui and vet pass.
* tui: show a ChatGPT account's credits beside its windows
The Usage page printed only the balance for an account that had one, so a
ChatGPT account holding credits lost its 5-hour and weekly windows there.
The balance now follows the windows, as on the app's cards and in
magpie quota.
* accounts: tell a ChatGPT account's credits
magpie accounts left out the credits an account holds, which magpie quota
and the Usage page show. Its rows now carry them, in the line and as
balance in --json.
* codex: a switch for whether an account spends its credits
A ChatGPT account holding credits keeps answering once a usage window is
used up: the vendor spends the credits, and a task goes on. That stays the
default. Each Codex account now has a say in it (Usage card and menu bar
panel: Use credits; magpie quota credits <account> on|off; settings'
codexNoCredits). Turned off, the gateway holds the account at 100% of a
window, as a usage cap holds it at its share, so the other accounts,
groups and fallbacks take the request and no credits are spent. With none
left the request gets a 429 naming what holds each account and how to go
on, unless the account spends its resets by itself and its week is used
up: then a reset is spent first, whether every candidate was held or the
last one tried was refused. A cap set on the account takes precedence.
A reset made now forgets the account's cached allowance, so neither a cap
nor this hold outlives the windows it started again. The switch is
routing's only: which account Codex is signed in to is unchanged.
The Routing page tells why such an account was left out, and magpie quota,
magpie accounts and the TUI mark credits not spent.
* usage: an auto-reset switch keeps by its first line
The auto-reset row centred its switch on its words, so the switch moved
when they rewrapped on a click, and it sat lower than the Use credits
switch beneath it. Both now keep the switch by the first line.
* gui tests: quota-curve expects the Use credits row to close a Codex account's section
The credits switch adds a row after the Auto-use one, so the account after
a Codex account follows the curve, the Auto-use row and then the Use
credits row.
Codex's auto-review (approvals_reviewer = "auto_review") runs on the
auto_review_model_override of the conversation model's entry in its list,
else on codex-auto-review, else on the conversation's model at low effort,
so a conversation on an expensive model reviewed every approval on it.
settings.CodexAutoReview, picked from the app's menu (magpie's models and
groups, or Codex's own pick), is now put as auto_review_model_override in
every entry magpie hands Codex: magpie-models.json and a signed-in Codex's
/models, the ChatGPT backend's entries included. Unset, the list is as it
was (an override magpie left in Codex's cache is taken out, one OpenAI gives
kept). POST /api/settings/codex-auto-review refuses an id magpie doesn't
serve; settings refuse a value that is no model's id; the list's ETag
carries it so Codex asks again; another Settings save keeps it.
Tested end to end in a sandbox HOME with a real DeepSeek account and Codex
0.160.0 (codex exec, approval_policy on-request, approvals_reviewer
auto_review, read-only sandbox, a command asking for escalation): with the
setting deepseek/deepseek-flash the guardian request (kind guardian) went
to deepseek-flash while the conversation stayed on deepseek-v4-pro; unset,
magpie-models.json had no override and the review ran on deepseek-v4-pro.
Picking DeepSeek V4.1 Flash in the real web UI's Settings row wrote the
setting and rewrote magpie-models.json (4 entries with the override), and
the next codex exec reviewed on deepseek-flash. TestCodexAutoReviewEntries,
TestCodexAutoReviewChecked, TestCodexModelsAutoReview,
TestCodexAutoReviewSetting and codex-auto-review.test.cjs (Chromium and
WebKit, en and zh) are new and fail without the change; gateway-mode and
settings_keep cover the new row and field. go vet, go build (GOOS=linux
nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass.
#925: an agent's row said "9.0M cached" but not what share of its prompt
that was. Each row of the Agent (and Model, Account, Key) lists now adds
"hit rate N%", counted as the Cache read tile counts it: cache read over
input + cache read + cache write; a row with no cache read shows none.
usage-hit-rate.test.cjs is new and fails without the change.
#928: with both a provider that searches and a search API set up,
Settings › Web search has "Search first with": Model search (as before,
the default) or Search APIs. Saved as settings.searchFirst ("api", or
empty for the model first); with the APIs first, webSearch asks them
before the Kimi/Google own search and the searcher's model, which search
only when every API fails. The Search APIs row says they are asked first.
The row is shown only when both sides are set up. TestSearchAPIsFirst and
search-first.test.cjs are new and fail without the change.
Test fixes on main: gui-ja/gui-de failed on ja/de strings with a second
{agent} (the "is signed in to {user} now" warning) and on <name> written
<名前>/<Name> in the docker-forward warning; quota-curve.test.cjs opened
/blank, which its harness served as a 404 with no body, and Chromium
refused to open it (ERR_HTTP_RESPONSE_CODE_FAILURE) — a harness bug, now
served as an empty page; search-api.test.cjs waited for #status text that
Brave's refusal had already set, so on Chromium it read the posts before
the copy arrived.
go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test
-tags nogui ./... under a temp HOME pass; the GUI tests above pass on
Chromium and WebKit with Playwright 1.63.
v0.1.1032 counted a 1-hour cache write left out at 2× input for every
model, so gpt-6-astra priced 10,50,1,12.5 showed "$20 (1 hour) at 2×
input", a write OpenAI has no price for. OneHourFor gives a Claude model's
price, listed or given by the user, its 2× input; any other model's 1-hour
writes count at the 5-minute price, magpie model price shows only its cache
write, and the app shows the 1-hour box only for a Claude model or one
priced with one.
OpenAI bills gpt-6-astra, gpt-6.1-sol and gpt-6-luna over 272K input at 2× input and cache and 1.5× output, for the whole request. A price now has tiers: `magpie model price <m> 10,50,1,12.5 --tier 272k 20,75,2,25` (--tier can repeat). A tier applies when input + cache reads + cache writes is over its size, so 272,000 is not over and 272,001 is. The built-in prices take tiers only where models.dev lists one (cost.tiers of type "context"), and it does list one for those three. The ledger, each request and OTLP are costed at the tier the call reached. Session and day totals sum many calls, so they stay at the base price.
Anthropic bills a cache write kept for an hour at 2× input and one kept for 5 minutes at 1.25×. Its usage reports which is which in cache_creation.ephemeral_5m/1h_input_tokens. The gateway (API and Claude subscription) and Claude Code's transcripts now keep that split. A price's fifth part is the 1-hour write. When it isn't given, it is 2× input: that is Anthropic's rule, and only Anthropic reports 1-hour writes. Claude list prices carry the 2× too. cache_write_tokens is still the total, and the usage CSV adds cache_write_5m_tokens and cache_write_1h_tokens at the end. The app shows the split on a request's Cache and on the usage tooltips. Old prices and old records, which have no 1-hour part, read as before. The sessions cache (18) and the call cache (calls-v8) are re-read once.
In the GUI, the price boxes were number inputs and dropped what they didn't parse ("0,25"). They are now decimal text boxes that take both "0.25" and "0,25". A model's price row has a 1-hour box (empty means 2× input) and a quiet Long-context price row. That row shows the list's tier greyed in, or sits behind a link when the list has none. A bad size is refused by name. en/zh/ja/de.
Tests:
- New: TestPriceTiers, TestCostSplit, TestPriceJSON, TestModelPriceTiers, TestModelPriceTierCmd, TestAnthropicCacheWriteSplit and TestClaudeCacheWriteSplit. The ledger CSV test expects the new columns.
- model-price-tiers.test.cjs is new; model-price-edit and provider-price-rate are updated. All of them fail against the old assets and pass in WebKit. Chromium couldn't be launched here.
- go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass.
Until now the only choice was 272K for every model or Full window for every model. The 272K default stays as it is (e950eb08: OpenAI runs its own 1M models at 272K in Codex, and Chen saw 70–90s to a first token at 550K).
What changes:
- A provider's editor has Compact at beside Context window and Max output, in the same form: "500k" for all of its models, "deepseek-v4-flash=272k" for one, and empty to follow Settings. A size at or above a model's window means the whole window. The values are kept in settings.ModelCompacts.
- Settings → Long conversations takes a size of its own beside Compact at / Full window (settings.CompactAt).
- `magpie model compact` takes a size such as 500k.
Order: the model's own value, then its provider's "*", then Settings. Full window turns off only the Settings value. A group uses the smallest value among its members.
Where it applies:
- Codex's model_catalog is told the threshold as context_window, with the whole window as max_context_window.
- Claude Code's CLAUDE_CODE_AUTO_COMPACT_WINDOW follows the main model. A Claude model now takes its provider's value when one is set.
- The Codex list tag changes when the Settings size or Full window changes.
Tests:
- New: TestModelCompactsSetFromTheEditor, TestSetCompactAt, TestCodexCatalogCompactAt and TestClaudeCompactAt.
- TestSettingsSaveKeepsWhatItDoesNotSend now covers the two new fields.
- compact-at.test.cjs (WebKit, en/zh) checks the editor field (shown, refused, chips, saved) and the Settings size (posted, hidden under Full window, no scroll).
- go vet ./..., go test -tags nogui of the root package, provider, codexcat, agent, gui, gateway, settings and catalog under a temp HOME, and GOOS=linux/windows builds all pass.
thedavidweng asked to be told before an allowance resets, or a banked reset runs out, while it is still unused. magpie's notifications only said a window was nearly used up or a balance was low.
Settings › Usage now has a Reset reminder row: Off, 12 h, 24 h or 48 h before (settings.ResetReminder, 0 to 168 hours).
What it tells of, through the same watcher and notifications as the usage alerts (WatchQuotas, dueReminders):
- a window of a day or more (weekly, monthly) renewing within those hours with under 85% of it used. A 5-hour window renews too often to be news.
- an account's resets (Codex's, a GLM team plan's) expiring within those hours unused.
Each is told once per run, kept in usage-alerts.json as the alerts are. A failed or stale reading says nothing.
The notification reads, for example, 7 days: 55% left, renews tomorrow 14:30 — use it before then, and 2 resets unused, expiring tomorrow 14:30. zh, ja, de strings, there and in the row.
Turning it on asks the system's leave to notify and looks at once, as the alerts do.
Tests:
- TestDueReminders: once per run; a 5-hour window, one 90% used and one renewing later are left out; a longer lead takes the later one; resets told; a failed reading says nothing.
- TestReminderText (en, zh, ja, de).
- reset-reminder.test.cjs (WebKit, en and zh): the row, a pick saves it, another save keeps it, no click moves the page. It fails 2/2 on the old assets. usage-alert.test.cjs passes.
- go vet passes. The darwin, GOOS=linux nogui and GOOS=windows builds pass. go test -tags nogui -count=1 ./... passes under a temp HOME.
Before this, only MAGPIE_ADDR could move the gateway, and the app on a Mac or one started from the Start menu has no easy way to set an env var.
The setting:
- Settings has a Gateway port field. Any port from 1024 to 65535 can be typed, then applied with Apply or Enter.
- The port is saved in settings.json and kept on this computer (KeepOwn), so a backup or WebDAV doesn't carry it across.
- MAGPIE_ADDR, where it is set, still comes first. The field is then off and says so.
- A port out of range is refused in the page.
- A port another program listens on is refused: "port N is in use by another program: pick another one". The gateway stays where it was.
- With another magpie serving the gateway, the field says to set the port there.
On a change:
- The listener moves live (Relisten listens on the new port before closing the old one).
- When the old port had been taken by another program and nothing served, the gateway starts on the new one.
- Every agent connected to magpie has its config rewritten with the new URL (agent.OnGateway before the move, agent.Rewire after):
- each one whose wiring is now "unwired" is set again;
- Codex signed in with ChatGPT is joined again and keeps its own model;
- Cursor Private Inference's variables and dsh's routes follow.
- What each agent had before magpie (the stash) is kept through this. Before, setting an agent again on the old URL would have written magpie's old URL and token over it.
- The page says how many agents moved.
What follows the saved port (through gateway.Addr()/URL() and settings.GatewayAddr):
- the CLI and healthcheck;
- Running();
- LAN sharing (Port);
- the drift check for the installed magpie, from beside a MAGPIE_ADDR one;
- importapps' "is this entry magpie's own" check.
zh, ja and de strings.
Tests, all failing on the old code:
- TestPortMovesEveryAgent: every agent with a config file is connected at 3591 and moved to 3592. No file keeps 3591, and none is off afterwards.
- TestPortMovesJoinedCodex.
- TestPortLeavesAnUnwiredAgent.
- TestPortKeepsWhatAnAgentHadBefore: the relay URL and own token come back on disconnect.
- TestDriftInstalledMagpieOnItsPort.
- TestSetPortMovesTheGateway, through the API: taken, out of range, moved, the old port silent, POST /api/settings keeps it, MAGPIE_ADDR refused.
- TestSetPortServesWhenTheOldOneWasTaken.
- TestGatewayURLOnSettingsPort.
- gateway-port.test.cjs on WebKit, en and zh:
- out of range, taken (translated), applied by Enter, the agents counted, the new URL shown;
- the MAGPIE_ADDR state;
- no select, no left border, and the page never scrolls (the row is swapped whole, or WebKit laid the list out empty and the view jumped).
Sandbox run (own HOME, settings port 3591, magpie web on 3593):
- Claude Code (over a relay with its own token) and Codex were connected at 3591.
- POST /api/settings/port:
- 3594, held by another program: refused, in use;
- 80: refused;
- 3592: moved [Claude Code, Codex].
- Both configs then say 3592. The gateway answers on 3592 and not on 3591, and `magpie healthcheck` exits 0.
- `magpie claude default` gave back https://relay.example and own-token.
lan-docker, settings-sections, settings-groups, click-scroll, gateway-caller-keys, gui-ja, gui-de and webdav-settings-refresh pass on WebKit.
go vet, the builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
* Add German translation and harden paths against unset env vars
Add a German locale to the GUI, tray menu, usage alerts and website home page, and refuse to start when the home folder is unset or relative so nothing is written under the working folder; the pi plugin no longer adopts the user's PI_CODING_AGENT_DIR, and an inherited APPIMAGE no longer selects portable mode. Scripts refuse to run from the wrong folder or empty a shared out folder, and a new internal/testenv sandboxes every test package from the user's real files, which also fixes the Claude picker and Termux installer tests on Windows.
* Makefile: know magpie's folder by its go.mod, so paths with spaces work
The check compared $(CURDIR) with $(dir $(abspath …)), which make splits on spaces, so a checkout under 'My Projects' or 'John Doe' refused every target. It now reads go.mod relative to the working folder and wants magpie's module there: make -C and a symlinked Makefile work, make -f from another folder still stops before any recipe runs.
* i18n: German for the strings main added since, with a parity test
75 strings main added (group order, patterns, slow-start wait, usage caps, RTK savings …) get their German, and two that main dropped go. gui-de.test.cjs checks, as gui-ja does, that de has every zh string with its placeholders and tags, and that the window reads in German when picked or on a German system.
* appdir: ignore relative folder variables when reading, keep them for children
CheckEnv no longer unsets anything. magpie reads the XDG folders, APPDATA/LOCALAPPDATA and every agent's folder variable through appdir.Getenv/LookupEnv, which pass over a relative value; the environment stays as it is, so the agents magpie starts get the variable as the user set it. That covers what proc.UserPath lends from the login shell too, which is set after main's check and reached the desktop app unchecked. On Windows a path rooted on the current drive (\Users\x, Git Bash's /c/x) counts as usable, and magpie version and help answer without a home, before run makes any folder.
* Sandbox the tests main added since
internal/wslrun gets testenv's TestMain, and four tests main added that set HOME alone set their home with testenv.SetHome, so on Windows they don't read the runner's profile through USERPROFILE.
* agentenv: PI_CONFIG_DIR and OPENCODE_DB take relative names on purpose
omp puts $PI_CONFIG_DIR under the home and OpenCode a relative $OPENCODE_DB in its data folder, so neither depends on the working folder: appdir no longer passes them over.
* i18n: German for the Library's multi-select and System One's decision API
The 19 strings main added since (#791's skill multi-select, the decision API snippets) get their German; the one main replaced (four APIs → five APIs) goes.
* appdir: tidy the import into magpie's group; say what Go makes of /c/x
The appdir import joins the group of magpie's own imports where a file has one. rooted's comment now says that Go takes a Git Bash path like /c/x as \c\x on the current drive, not C:\x.
Every string the window, the panel and the menus have in Chinese now has a Japanese one too: 2917 entries in I18N.ja, translated to one glossary (プロバイダ, エージェント, ゲートウェイ, ルーティンググループ, 使用量, サインイン…), each keeping its {placeholders}, tags and leading spaces. Language takes "ja" (settings.Langs); System picks ja when navigator.language starts with ja, and <html lang> is ja-JP so CJK text draws in Japanese glyphs. Dates and numbers format through intlLang() (zh-CN or ja-JP) where they said zh-CN only. The tray menu, the quota alerts (7 日枠を 80% 使用、明日 14:30 にリセット) and the restart-to-update item have Japanese of their own (trayLang reads ja from the setting or a ja_JP/ja-JP system). Release notes stay English for ja, as the site has them in en and zh. The 万/亿 units option stays Chinese only.
Looked at in the Omarchy VM (magpie web, tokyo-night, ?locale=ja): Agents, Providers' Add sheet, a link's import, Routing, Usage, Gateway, Settings and the panel read in Japanese with nothing cut or overflowing. gui-ja.test.cjs (every zh key has a ja one with the same placeholders and tags; ja picked, a ja-JP system, en and zh systems, on WebKit and Chromium) and the ja cases in TestTrayMenuLabels and TestAlertText fail on the old code. On WebKit the other GUI tests fail as on the base but mobile-web's desktop comparison with origin/main, whose Settings now has a fourth language. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
A card's logo is its handle. A grip shows by it only while the pointer is on the card, as on Agents and Providers. The card is dragged across the grid: dragCards is dragRows in two directions, the slot nearest its middle is where it goes, and the cards between move a slot over. Alt+arrows move it from the keyboard and keep the focus on it. The order is settings usageOrder, by provider id, saved by POST /api/usage/arrange. It is apart from the Providers order, which is also the order providers are tried in. POST /api/settings keeps it. A card it doesn't name follows in magpie's own order, and an order naming one not on the page now keeps it after them. The tray panel's Usage tab follows the same order.
usage-arrange.test.cjs covers the hover-only grip, a drag across the grid, Alt+arrows, a click that moves nothing, the order after a reload, and the panel. It passes in Chromium and WebKit, en and zh. TestSettingsSaveKeepsWhatItDoesNotSend covers usageOrder. go test ./internal/gui ./internal/settings under a temp HOME, go vet, and builds (darwin, GOOS=linux, GOOS=windows) pass. Also tried on a sandbox magpie web with faked quotas: the drag, the keyboard and a reload kept the order in WebKit and Chromium. The other Usage/panel UI tests pass, except credit-days.test.cjs, which fails about 1 run in 4 on main too.