* 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.
The tests run against fake httptest upstreams. TestDetectProtocols covers a bare host, …/v1/ and a pasted …/v1/chat/completions: chat at /v1 and Anthropic at the root are found with gpt-5.5 and claude-sonnet-5 picked from the list, Responses' 404 is reported, the key is sent, no request asks for more than 16 tokens or streams, and nothing is saved. TestDetectModelAndOwnURLs: a typed model is used everywhere and the list isn't fetched, the provider's own Anthropic URL is asked instead of the typed one, and an empty URL or one with no scheme is refused. TestModelAPI: the refusals, the override via SetModelPrefs, APIs/Native/TestModels following it with one request to /v1/messages, the other model unaffected, and "" clearing it. TestModelAPIRoutes: through the gateway, a chat request for the model set to Anthropic reaches only /v1/messages while chat answers "no available channel"; another model still goes to chat. TestProviderDetect covers /api/provider/detect for a new provider's typed key and a saved provider's key, and a Save's modelPrefs.api kept and reported back. TestModelAPI and TestModelAPIRoutes fail without the APIs() override ("apis []"; the upstream asked chat three times). provider-detect.test.cjs runs in WebKit, en and zh, and fails without the change: Detect sends the typed URL and model, the three results are shown, Use these fills Anthropic's URL and keeps chat's, the per-model API picker appears with two URLs and stages only away from Auto, Ask on one API stages it, and Save carries anthropic plus modelPrefs {"m-mixed":{"api":"anthropic"}}. No click moves the page, there is no left-border accent, and every string has Chinese. TestSettingsSaveKeepsWhatItDoesNotSend now covers ModelAPIs. click-scroll, model-test-one, model-test-why, provider-levels-save, provider-levels-scroll, editor-short-window, azure-endpoint, nokey-editor, provider-search, provider-typed, provider-picks-stay and provider-proxy pass in WebKit. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, and go test -tags nogui -count=1 ./... under a temp HOME pass.
* otel: bodies can go whole to the trace, not cut at 256 KB (#538)
The OTLP export sends a call's request and reply as the trace's Langfuse
observation input and output, but took them from the Recent-calls capture,
which keeps the first 256 KiB of each: a longer prompt or reply reached the
trace cut, ending in "… (cut here by magpie)".
Bodies can now go whole, behind a switch: Settings → Observability →
"Include the whole bodies" (otel.bodiesWhole, MAGPIE_OTEL_BODIES_WHOLE for
magpie serve, which needs Include request and response bodies on too). The
gateway keeps the whole request (already in memory for the call) and spools
the reply to a temporary file as it streams, read back for the export.
Nothing changes for Recent calls (still 256 KiB), the request archive or
usage.jsonl.
The export holds queued bodies within a byte budget and splits an OTLP batch
by body size, so a whole body can neither fill memory nor blow up one
collector request. Secrets are masked as before.
TestOTelExportsWholeBodiesWhenOn (a 300 KiB request and reply reach the span
whole with the switch on, cut and marked with it off), TestOTelQueueBodiesBudget,
and the settings/env and otel.test.cjs switches.
* otel: release recent-call bodies and enforce queue budget