main
2515
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ba0e2a8aa9 |
plugin: Update all and magpie plugin update bring a plugin pinned to an older version to npm's newest, so the update prompt clears (ARNO on Discord)
ARNO: 非官方插件安装后就算更新到最新版,magpie依然提示有更新. A third-party plugin added as name@1.2.3 is pinned. The Plugins page still counts it under "Update all (n)", its row says "vX out" and the Plugins dot is on. Update all (and `magpie plugin update`) reinstalled the pinned version, reported "Plugins updated", and the prompt stayed. The row's own Update already installs the newest version and unpins the spec (name@latest). Update now does the same for a pinned plugin when npm has a newer version than the one installed: it installs that version through add, which keeps the plugin's options and the provider picked for it. A pinned plugin that is already at npm's newest keeps its pin. Git plugins, folders and plugins npm didn't answer for behave as before. The plugin docs (en, zh, ja) now say that updating a pinned plugin yourself unpins it. Reproduced in a sandbox HOME with the real opencode-gemini-auth from npm (magpie web on a spare port, China mirror switch on and off). Pinned at 1.4.15 with 2.0.1 out: before the fix, Update all and `magpie plugin update` left it at 1.4.15 and /api/plugins/updates still listed it. After the fix it is at 2.0.1 as opencode-gemini-auth@latest, the waiting list is empty and Check for updates reports "current". The row's Update (upgrade) path was checked too: unpinned opencode-gemini-auth, and scoped @gitlab/opencode-gitlab-auth 1.3.1 -> 1.3.3 with the mirror on. Both cleared the prompt before the fix as well. TestUpdateAllUpdatesAPinnedPlugin (real bun, fake registry) fails without the change: "Update all left opencode-someones-auth at 1.0.0, want 2.0.0". gofmt -l, go vet (darwin, GOOS=linux, GOOS=windows) on internal/plugin, and go test -tags nogui of internal/plugin, internal/gui and internal/provider under a temp HOME with pinned caches all pass. |
||
|
|
47a313f618 |
library: Pi's MCP servers go to ~/.pi/agent/mcp.json, the file Pi 0.99+ loads, unless pi-mcp-adapter is really loaded; an adapter left installed but not listed no longer sends them to mcp-adapter.json (sydney on Discord)
sydney reported that the Library wrote Pi's MCP servers to ~/.pi/agent/mcp-adapter.json while Pi 1.0 loads ~/.pi/agent/mcp.json. Pi's own source (npm @earendil-works/pi-coding-agent): - 0.99.0 to 1.1.0 have built-in MCP: dist/extensions/mcp/config.js loadMcpConfig reads <agentDir>/mcp.json (and a trusted project's .pi/mcp.json). An extension registering /mcp (pi-mcp-adapter) replaces it; "extensions": ["-builtin:mcp"] turns it off. - 0.87.1 and earlier have no MCP; pi-mcp-adapter 3.x/4.x reads mcp-adapter.json only, adapter 5.0.0+ also reads Pi's mcp.json on a Pi with registerMcpServer. - core/package-manager.js resolve() loads only settings.json "packages", "extensions" entries and <agentDir>/extensions/. npm/node_modules, git/ and the global npm root are install places, not loaded on their own. magpie counted any pi-mcp-adapter folder (npm/node_modules, git, global root) as loaded, so an adapter left behind with "packages": [] made it write mcp-adapter.json, which nothing reads, and move mcp.json away (heliar-k's #1097 is the same case). It also fell back to the adapter file when Pi's version was unknown. Now: - piDetect follows Pi's loader: a package is loaded only when listed (an object entry with "extensions": [] loads none), "-"/"!" entries turn one off, and <agentDir>/extensions/<name> loads; the other folders only give the version. - An unknown Pi version is taken as one with built-in MCP. - With no adapter loaded, servers in mcp-adapter.json are moved into mcp.json (both backed up first); nothing in either file is dropped. - With adapter 5+ loaded on a Pi with built-in MCP, mcp.json is left where it is (the adapter reads it) and new servers go to the adapter's file; with adapter 3/4, or on Pi before 0.99, the old move is kept. Tests: TestPiMCPUnloadedAdapter, TestPiDetectLoaded and TestPiMCPAdapter5KeepsPiFile fail on the old pimcp.go and pass now; older tests that relied on an unlisted adapter now list it. Run: gofmt -l; GOOS=linux and GOOS=windows go vet -tags nogui ./internal/library/; go test -tags nogui ./internal/library/ ./internal/agent/ under a temp HOME. Real Pi in a sandbox HOME: - Pi 1.0.4, reporter's layout (adapter 5.2.0 in npm/node_modules, "packages": []): `magpie library mcp add` wrote mcp.json and `pi mcp list` shows both the user's server and the new one connected. The old binary wrote mcp-adapter.json and Pi listed no servers. `magpie library sync` with pi off PATH (version unknown) moved that home's mcp-adapter.json back into mcp.json, with backups, and Pi connected both. - Pi 1.0.4 with `pi install npm:pi-mcp-adapter@5.2.0`: mcp.json is kept, the new server goes to mcp-adapter.json, and `pi-mcp-adapter doctor` connects both. - Pi 0.87.1: still mcp-adapter.json, as before. Not run: Windows or WSL Pi, project-level .pi/settings.json, a real interactive Pi session. Reported by sydney on Discord; same cause as #1097 (heliar-k). |
||
|
|
666c0fa4fd |
routing: the context window holds still while requests stream in, and the prompt-cache card shows the last request's hit, dimmed, or a dash with the reason instead of "——命中" and an empty bar (dumplings on Discord)
dumplings reported that on Routing, every new request made the context
window jitter and flash, and that the prompt-cache card read "——命中",
glued together, with an empty bar under it.
Causes, measured with a frame sampler while requests stream through
start/read/done:
- A live request's story is shorter than a finished one's. "Is
answering…" becomes "answered in 4.0 s…", which wraps onto two lines
at phone width. The log head also gains View usage and Replay once the
request is done. So the card under it moved up and back down on every
request: 45px at 420px wide, 6px at 900px.
- The state pill ("Live" or "Counted") changed width, which moved the
model name beside it by about 11px.
- While a request was under way the cache card dropped to "—" with a
0% bar, then came back. That was the flash.
- A flex item's leading space is dropped, so " hit" and " / 272K" were
glued to the figure before them.
Fix:
- routing.js holdStory keeps the log head and steps at the tallest
height seen while following live, at this width and language. A
picked request, a replay or a past day is drawn at its own height. A
ResizeObserver on the steps measures again as soon as the width
changes. Without it, a scrollbar appearing before the first request
left a stale height and a 6px shift (seen in WebKit under load).
- context.js stacks all three state words in one grid cell and shows
only the current one, so the pill keeps one width.
- While the next request is under way, the cache card keeps the last
request's figure, dimmed (.was), with "The last request's · this one
is on its way". When there is nothing to show, it gives a faint "—"
and the reason ("Read once the vendor answers" or "The vendor didn't
say"). The bar stays hidden but keeps its space. The remembered
figure resets when the agent or session changes.
- .ctx-big gets a gap. Width and opacity transitions are turned off
under reduced motion.
- The three new strings are added in zh, zh-TW, ja and de.
Verified:
- The new tests/routing-ctx-still.test.cjs streams two requests and
checks that the card, model, state, used, cache, figure, bar, grid,
legend and foot rects never move by more than 0.5px. It also checks
the figure-to-word gap and both empty states.
- It runs in Chromium and WebKit, at en 420/1100, zh 420/1100,
zh-TW 900, ja 900 and de 420. It narrows the viewport by 10px after
load.
- On the old assets it fails 16 of 16 (the card moves 6–52px). It also
fails with only the ResizeObserver removed.
- routing-live-patch now expects the held figure ("97%") on live steps
instead of "—".
- Under --test-concurrency=4 in both engines: 578/578 pass across
routing-*, context-*, click-scroll, group-context, panel-routing and
gui-de/ja/zh-tw.
- go test -tags nogui ./internal/gui passes.
- GOOS=linux and GOOS=windows go vet -tags nogui ./internal/gui are
clean.
|
||
|
|
e3bd766fae |
providers: a saved provider's editor keeps the add form's Fetch models button, "Fetch models again" once a list was fetched, to re-fetch after a key moves to another group (耍赖天都爱 on Discord)
耍赖天都爱 on Discord: 「provider中模型拉取一次就没有这个按钮了 能不能加上 有
的provider换一个分组要从新拉取模型」. The add form offers "Fetch models"
(获取模型); once saved, the editor's same action was a generic "Refresh"
(刷新) in the models footer, so after the first fetch the button looked
gone, while a relay key moved to another group serves another list.
The editor's button now says "Fetch models", and "Fetch models again"
(重新获取模型) when a vendor list was fetched before. Its title says it asks
with the key typed here and what happens to the picks. The fetch itself is
unchanged: it sends the typed form (asTyped), and Refetch replaces the list,
drops picks the new list no longer has and keeps ids added by hand. The
hints that named Refresh ("from models.dev · …", "magpie's list · …",
"Jev's names · …", Forget's title, the empty-list hint) name the button now,
in zh, zh-TW, ja and de. The TUI's "m fetch models" and `magpie provider
refresh` already re-fetch at any time; the CLI help names the app button.
Verified:
- new internal/gui/tests/provider-fetch-again.test.cjs (Chromium + WebKit;
en and zh at 900 and 420px, zh-TW and ja at 900, de at 420): a never
fetched relay shows Fetch models and its hint names it; a fetched one
shows Fetch models again; with group B's key typed it asks with that key,
shows group B's list, drops the pick B lacks, keeps an id added by hand,
the button is still there and asks again; nothing scrolls. On the old
app.js/i18n.js it fails: "waiting for ... getByRole('button', { name:
'Fetch models', exact: true }) to be visible" (en), "again has its zh".
- provider-typed, provider-picks-stay (labels updated), gui-zh-tw, gui-ja,
gui-de, click-scroll: 51 pass, 0 fail, both engines.
- go test -tags nogui . ./internal/tui ./internal/gui under a temp HOME: ok.
go vet -tags nogui . ./internal/tui, GOOS=linux and GOOS=windows: ok.
|
||
|
|
65dcc57369 |
tests: the root package's plugin tests run the bun on PATH once before their one-minute context, so a bun that has never run here doesn't time them out (ggbdpq, #1306)
TestAccountsOfAPlugin and the plugin login tests (fakeCoHome) point
MAGPIE_BUN at the bun found on PATH and do all their plugin work inside
a one-minute context.
|
||
|
|
e611fc65d6 |
codex: magpie restarts the codex app-server a change left on the old model list when no codex session is on it, and the Agents page offers Restart when one is (luci on Discord)
luci (Codex CLI 0.162, magpie 0.1.1150): after Codex was wired back to
magpie, codex in a new terminal listed Codex's own models although
config.toml was magpie's.
Cause, reproduced with real codex 0.162.0 in a sandbox CODEX_HOME:
codex sessions attach to a background app-server (the managed daemon)
that is per CODEX_HOME, not per terminal (socket under
$CODEX_HOME/app-server-control). It is detached, has no idle exit and
builds its model list once, at start. A daemon started while Codex was
off magpie keeps the official list for every later session, in this
terminal or a new one, until it restarts. magpie writes only PATH to
shell rc files and doesn't unwire Codex on quit, so nothing
per-terminal is involved. luci's "a new terminal fixed it" isn't
explained by the daemon. Codex's hourly updater restarting it is a
guess, not proven.
What changed:
- provider.KeepCodexDaemonCurrent(ctx, home, changed) compares the
daemon's start time with when magpie last changed what Codex reads at
start. The start time is the mtime of a pid file in that home that
names the running daemon's pid, so another home's daemon (ChatGPT.app,
other sandboxes) is never acted on.
- With no codex session attached, it runs
`codex app-server daemon restart` with CODEX_HOME set to that home.
- Otherwise it reports Behind and how many sessions are attached.
- isCodexClient leaves out apps' and editors' own codex, exec-server,
mcp-server and the daemon's own processes. It reads the whole ps line,
since ps leaves a path's spaces unquoted ("Codex Computer
Use.app/...").
- agent: Codex's Notice after a change runs this check first. It says
either that magpie restarted the daemon, or that the daemon is behind
and how to restart it (Agents page or the command).
- The GUI backend and `magpie serve` re-check every 15s. A session that
was open at the change is usually closed later, and that is when the
daemon can be restarted.
- Agents page: the stale "daemon" copy line has a Restart button that
POSTs /api/codex/daemon/restart (timeout now 2 min, as a running turn
is let finish). It reuses translated strings. On Windows, where
processes aren't listed on each look, the row shows the last check.
- Restarts are serialized by one mutex.
- WSL's Codex is not handled automatically. Its row keeps the advice
text, which names the restart command.
What was run:
- Real codex 0.162.0 in a sandbox CODEX_HOME, with a fake gateway, on
luci's sequence: the daemon started on the official config, then
magpie's config and catalog were written.
- With a TUI open, /model listed GPT-6.x. The check said Behind with
a session attached and did not restart.
- With the TUI quit, the check restarted the daemon. A new codex in
the same terminal listed Magpie Alpha and Beta.
- Earlier in the sandbox: a daemon restart under an idle attached 0.162
TUI reconnects, and /model then shows magpie's models. A running turn
is waited for up to 60s.
- New tests:
- TestKeepCodexDaemonCurrent(Fails), TestIsCodexClient (with the
space-in-path case from the real process list) and
TestCodexNoticeKeepsTheDaemonCurrent. They don't build or fail on
the old code. The space case failed before the ps-line fix.
- The GUI test "the daemon's line restarts it" times out on the old
app.js.
- Results:
- agent-stale-copies.test.cjs: 60/60 in Chromium and WebKit, in en,
zh, zh-TW, ja and de, at widths 980 and 440.
- go test (temp HOME) for ./internal/agent, ./internal/provider,
./internal/gui and the root package: ok.
- gofmt -l: clean.
- GOOS=linux and GOOS=windows go vet -tags nogui: ok.
- The Windows path is checked only by vet and not tried on a Windows
box.
|
||
|
|
bb3b3f1486 |
gateway: with "only each model's largest window" on, Claude Desktop shows a model of 1M or more as its 1M entry, "1M context window", not as what looked like the plain model (杰多夫 on Discord)
The cause: under settings.DesktopLongest, /v1/models lists such a model by its "[1m]" id alone (#1272). Claude Desktop 2.7032 (app.asar, read only) marks an entry as the 1M one only on the entry it adds itself: JLt emits `${id}[1m]` with description WLt = "1M context window" and supports_1m_context, and only for a model with supports1m. PIt gives supports1m to a bare id alone (LC(id) === id), and OIt folds a "[1m]" row only into a bare one listed beside it. So the lone "[1m]" row was shown by its name with no description, exactly like the plain entry it replaced: with the switch off the menu had "DeepSeek V4 Pro" plus "DeepSeek V4 Pro · 1M context window"; on, only "DeepSeek V4 Pro". The reporter read that as the switch keeping the plain model and dropping the 1M one. What changed: such a row now carries description "1M context window", the words Desktop gives its own 1M entry, and Desktop shows a row's description (PIt keeps it; JLt prefers it to its fallback). The id is unchanged: the "[1m]" is what gives a Claude Code session the 1M window, and magpie serves it as the plain model. Rows under 1M, and every row with the switch off, are unchanged (no description). What was run: - TestClaudeDesktopLongestOnly now checks each row's description, on and off. With the description line removed it fails: "on: vend/deepseek-v4-pro (...[1m]) described "", want "1M context window"". - Sandbox HOME gateway on 127.0.0.1:3561 with real providers, /v1/models as Claude Desktop asks for it, fed through a script that follows PIt/OIt/JLt: on, each 1M model is one entry "<name> · 1M context window"; off, the plain entry plus Desktop's own 1M one, as before. - gofmt -l; GOOS=linux and GOOS=windows go vet -tags nogui ./internal/gateway; go test -tags nogui ./internal/gateway under a temp HOME: ok. No GUI or string change. |
||
|
|
d9bf7c1ccf |
zcode: the API format the user picks for magpie's provider in ZCode's settings stays theirs; a sync no longer puts Anthropic Messages back on every start (杰多夫 on Discord)
What changed: zcodeRules wrote api.type "anthropic-messages" into provider_config.json, and zcodeProviderJSON wrote kind "anthropic" into config.json, on every sync (magpie's start, any catalog change). A user who switched the provider to OpenAI Chat Completions or Responses in ZCode had it switched back the next time magpie started. zcodeFormat now reads the format already there and keeps it, in both files: provider_config.json's api.type (ZCode 3.14 on) or config.json's kind (older ZCode), the one written last where they differ, mapped as ZCode maps them (anthropic-messages/anthropic, openai-chat-completions/openai-compatible, openai-responses/openai). A new provider, or a format ZCode doesn't have, is Anthropic Messages as before. Why it's safe: magpie's gateway answers all three at the same base address. Checked with ZCode 0.16.9's own agent CLI (glm/zcode.cjs -p, run from a copy under a sandbox HOME, never ~/.zcode) against a sandbox gateway with the DeepSeek provider: anthropic-messages asks /v1/messages, openai-chat-completions /chat/completions, openai-responses /responses, and a tool-using turn (Read a.txt) answered 200 on each, to deepseek-flash. What was run: - TestZCodeKeepsUserFormat (new): a format picked in provider_config.json, for each of the three, survives two syncs and is carried to config.json's kind; one picked in config.json is carried to provider_config.json; an unknown one isn't kept. On the old zcode.go it fails: "picked openai-chat-completions: kind "anthropic", api "anthropic-messages"". - Sandbox, real binaries: provider_config.json set to openai-responses, then `magpie zcode magpie`: the old build wrote anthropic-messages back, this one kept openai-responses (config.json kind openai). Then openai-chat-completions, resynced, and the real ZCode CLI ran a turn on the file magpie wrote, served over /chat/completions. - gofmt -l; GOOS=linux and GOOS=windows go vet -tags nogui ./internal/agent; go test -tags nogui ./internal/agent under a temp HOME: ok. The "bcww: Field required" 400 on Anthropic Messages is not part of this commit: ZCode's real requests (captured from its CLI, plain and with web search) to deepseek/deepseek-flash through magpie returned 200, and DeepSeek answers schema errors in serde's words, not "Field required". |
||
|
|
eb035e29e1 |
sessions: a session seen only through the gateway can be deleted: its row and recorded conversation go to magpie's trash, and Restore brings them back (lc on Discord)
lc ("只可列出?不可删除?agent claude code") saw Claude Code sessions named by
uuid under "No folder" at "0 B", with "These Claude Code sessions are read
only; magpie can list them, but cannot resume or delete them." Those rows are
gateway projections from the usage ledger: Claude Code ran with its own files
somewhere magpie can't see (another computer, a container, another config
folder), so nothing native won on the same agent+id. An agent with only such
rows was sent as not deletable, and every row was read_only, so the page had
no Delete at all.
What changed:
- sessions.DeleteGateway moves the session's recorded text folders
(gateway-conversations/<date>/<identity hash>) into
trash/sessions/gateway/<ts>-<hash>/files/<date> with a note marked gateway,
and records {agent, id, last} in gateway-sessions-hidden.json.
/api/sessions and /api/sessions/manage leave it out up to that last
request; a newer request lists it again. The usage ledger is untouched, so
Usage, /api/sessions/one and stats keep what it spent. A session with a
request in the last minute is refused as active.
- Restore moves the text back (skipping what expired or was cleared, refusing
rather than overwriting a same-day re-recording), checks each item is this
session's identity folder inside the store, and unhides it. Delete forever
keeps it hidden.
- The recording's promises hold in the trash: the hourly prune erases
trashed gateway text past 7 days, "Delete saved conversations…" erases it
too, and HasGatewayConversations counts it.
- The manage route marks an agent with gateway rows deletable and sends
those rows deletable, with gateway: true and the size of their recorded
text; the delete route sends gateway ids to DeleteGateway and native ones
to Delete as before.
- Sessions page: gateway rows group under "Seen through the gateway"
instead of "No folder", show "{size} recorded" only when something was
recorded (no "0 B"), and the note says why they can't be resumed and what
Delete takes and keeps. The delete dialog says the same for gateway rows
instead of "Their files are moved…"; the Trash marks them and notes the
7-day expiry. New strings in zh, zh-TW, ja and de.
- docs/subsystems/gateway-sessions.md describes the delete, hidden list,
restore, expiry and clear.
Verified:
- go test -tags nogui ./... (sandbox HOME) green; gofmt -l clean; go vet
./...; GOOS=linux and GOOS=windows go vet -tags nogui ./internal/sessions
./internal/gui; go build, GOOS=linux -tags nogui and GOOS=windows builds.
- TestSessionsDeleteGatewaySession fails on the old code with "Claude Code
with only gateway sessions isn't deletable: [{Agent:claude
Deletable:false}]"; sessions-gateway-delete.test.cjs fails there too.
TestGatewayDeleteTrashExpires, TestGatewayPurgeKeepsItHidden,
TestGatewayDeleteNothingRecorded and TestGatewayRestoreRefusesForeignItems
cover expiry, purge, an unrecorded session and a forged note.
- GUI: sessions-gateway-delete.test.cjs in Chromium and WebKit, en, zh,
zh-TW, ja, de, at 1000 and 390px (in-app dialog, Cancel sends nothing,
click doesn't scroll, no left-border accents, no page overflow); all
sessions-*, gateway-conversations, checkbox-look, old-webkit,
sess-agent-strip, i18n-keys, gui-ja, gui-de and gui-zh-tw tests pass.
- Real try: magpie web from this tree on a sandbox HOME seeded with three
gateway-only Claude Code sessions (two recorded): deleted a recorded and an
unrecorded one, checked the store, trash and hidden list on disk, restored
the recorded one (its transcript reads again), purged the other (stays
hidden). That try found the Trash page breaking on an unrecorded session's
null items; DeleteGateway now writes [] (TestGatewayDeleteNothingRecorded
fails without it) and the page reads a missing list as empty.
|
||
|
|
0d5fdbb2de |
provider: moved Cursor users get plugin 0.2.3: where a proxy or network blocks HTTP/2 ("h2 is not supported"), a Cursor chat goes over HTTP/1.1 as RunSSE + BidiAppend on api2 instead of failing (magpie-community/plugins#59, kazecreator)
Behind TLS inspection or a network that drops HTTP/2, every moved Cursor chat failed with 502 "Cursor: h2 is not supported" or never answered, while sign-in, models and usage worked. 0.2.3 falls back to Cursor's own HTTP/1.1 transport; a failure that says HTTP/2 itself can't be had keeps later Runs on HTTP/1.1 for 10 minutes. The cursor mover's min goes from 0.2.2 to 0.2.3, so keepMovedCurrent updates every moved Cursor user. 0.2.3 on npm (registry.npmjs.org) has an index.mjs byte-identical to plugins main 5b67ff4. The plugin PR was reviewed at f5c11b7 and merged: - bun test in packages/cursor: 66 pass, 0 fail, three runs - whole repo: 1026 pass, 0 fail; bun scripts/check.mjs all 15 pass - each guarded piece broken by hand turns http1.test.mjs red - not tried with a real Cursor account on the review side go test -tags nogui ./internal/provider passes (temp HOME, pinned caches). go vet -tags nogui ./internal/provider passes for darwin, linux and windows. gofmt -l is clean. |
||
|
|
83c963c466 |
gateway: a gateway MAGPIE_ADDR puts on the network asks callers from other machines, a container's host among them, for an enabled gateway key, as sharing does
What changed - lanGuard: a request whose peer isn't loopback (or that carries a forwarding header) is remote. While the gateway is shared, or MAGPIE_ADDR names a host other than loopback/localhost (OnNetwork: 0.0.0.0 in the Docker image, a server's address), a remote request is answered only with an enabled gateway key, on every route behind the guard; otherwise it is refused (403) as before. Before, a MAGPIE_ADDR on the network with sharing off took remote requests with any key. A remote request let in with a key is marked as shared-mode ones are, so quotas, quota cards, refresh and prices answer it. callerGuard follows the same condition. OpenToAnyone is replaced by OnNetwork. - Loopback is unchanged: any key still works on the gateway's own machine and inside the container (HEALTHCHECK included). - CLI keyNote and the console's Connect note no longer describe an open gateway; on the network they say other machines need a gateway key (`magpie gateway-key add <name>`). The console shows the Gateway keys block and network addresses when OnNetwork (gatewayJSON.onNetwork replaces open); the "Open to the network" strings are removed. - Docs: docker.html (en/zh/ja) and remote.html (en/zh/ja) say callers outside the container need a gateway key, how to make one, that sharing isn't needed for it, and what an upgrade means for agents that sent `magpie`; docs/reference.md and providers-accounts.md likewise. A MAGPIE_ADDR pinned to one interface's address now asks a key of this computer's agents calling that address too (documented). Tests - New TestNetworkGatewayGuardsEveryRoute: from 172.17.0.1 and 192.168.1.9 every registered route (model APIs, count_tokens, systemone, the Responses websocket GET, MCP, Codex backend and catalog, /v1/magpie/*, images, embeddings, rerank, videos, Gemini) gets 401 without a valid key; with one, models and quotas answer 200. - TestNetworkGatewayNeedsAKey (was TestExplicitOpenGatewayAccepts...), TestLANGuard, TestProxiedLoopbackNeedsTheKey, TestQuotasOverLAN, TestMCPProxyStaysHere, session route and remote prices tests, TestOnNetwork, serve_cli and gui public_url tests updated. With the old lanGuard restored, TestNetworkGatewayNeedsAKey, TestNetworkGatewayGuardsEveryRoute, TestLANGuard and TestProxiedLoopbackNeedsTheKey fail. Run - go test -tags nogui -count=1 ./internal/gateway ./internal/gui ./internal/provider . under a temp HOME: ok. - gofmt -l clean; GOOS=linux and GOOS=windows go vet -tags nogui on those packages: clean. - GUI: public-url and gateway-caller-keys tests, Chromium and WebKit: 22/22. - Locally built image (OrbStack), -p 127.0.0.1:3481:3425: from the host, /v1/models with no key, `magpie` and another made-up key -> 401; count_tokens and /mcp/x -> 401; a key from `docker exec ... magpie gateway-key add Host` -> models 200, quotas 200; inside the container wget /v1/models -> 200; healthcheck exits 0. `web --addr 0.0.0.0:3430`: /api/state 401 without the web key, 200 with its cookie. |
||
|
|
c4476d5658 |
library: skills from GitHub are checked as the skills page opens, and one changed here is marked and asked about before an update replaces it (#1449)
YangZhiqiang98 asked (#1449) that the Library find which skills have updates and update them. magpie already checked skills from GitHub (a market install among them) by hand and could update them, but an update deleted the library's folder, so a skill the user had changed (by hand, through an agent's link, or in an agent's copy not yet taken in) was lost without a word, and nothing checked unless the user pressed Check. - A skill is "changed here" when its library folder no longer hashes to what was fetched (Finder's .DS_Store aside), or an agent's copy was edited since its mark and not yet taken in (editState, skill_changed.go). The view says so (SkillView.edited) and the row shows "Changed here". - UpdateSkill refuses such a skill with *EditedError unless told to replace it. The row's Update asks in the page first (Keep mine / Update; Update sends replace). Update N, the group's Update N and the CLI's update-all leave it as it is and say so. The CLI takes `magpie library skill update <name> --replace-edits`. - The version an update replaces is moved to the backups when it was changed, or when it can't be told (a skill with no hash from before). - View.checkDue: no check since magpie started, or none for 12 hours. The page then runs the check once by itself as the skills open, and says only what there is to update or add. Not checked, by design: skills the skills CLI manages (`npx skills update` updates them; magpie only groups them), linked local folders, skills imported from an agent with no source, and From-only skills have no remote; CC Switch-installed skills can still be updated by hand. Verified: - go test -tags nogui ./... (sandbox HOME); new TestUpdateKeepsASkillChangedHere, TestUpdateKeepsACopyEditedInAnAgent, TestUpdateKeepsWhatItCantTell, TestCheckDue, TestCheckSkillsMarksItDone, all from httptest. With editState forced to "not changed" the first three fail. - gofmt -l, go vet, GOOS=linux and GOOS=windows go vet -tags nogui on ./internal/library ./internal/gui ./internal/tui . - GUI: new skill-update-edited.test.cjs (Chromium and WebKit, 860 and 400px, en and zh; no scroll on click; fails with the row's ask removed), plus the 22 other library skill tests, gui-ja, gui-de, gui-zh-tw, i18n-keys and click-scroll: 262 pass. - Real use in a sandbox HOME with github.com/anthropics/skills: installed pdf and docx, the real check said both current; an edit through Claude Code's link was refused by `skill update pdf`, skipped by `skill update` (docx updated), replaced by `--replace-edits` with the edit in backups/<time>/library/skills/pdf; docx edited later shows edited, a .DS_Store doesn't. |
||
|
|
d86a4e0568 |
site: the docs have Docker and Remote magpie pages in English, Chinese and Japanese, listed in every docs page's contents and footer and linked from the READMEs (HuanCheng Bai on Discord)
Until now, both were only in docs/reference.md. The new pages are at
/docs/docker, /docs/remote and their zh/ and ja/ versions.
- docker: the image, its tags, ports, the /config volume and the nonroot
user. How to run it with the browser UI (`web --addr 0.0.0.0:3430
--no-open`, MAGPIE_WEB_KEY, the ?k= cookie). Signing in through the
Callback URL paste or `docker exec ... magpie accounts add`. What
/config keeps, the env vars, sharing with gateway keys, and a Compose
file for a NAS. Healthcheck and updates.
- remote: sharing the main magpie, then adding it as a Remote magpie
from the app, the TUI (a/w/m) or `magpie provider add remote-magpie
<key> url=... id=office`. What comes over (models, groups, APIs,
images, System One, embeddings, prices, quotas) and the errors magpie
shows, quoted from remote_quotas.go and lan.go.
The facts were checked against the code, not reference.md:
- the Dockerfile ENV, VOLUME, HEALTHCHECK and EXPOSE;
- docker.yml's tags;
- web_cli.go;
- the lanGuard in internal/gateway/lan.go;
- the remote-magpie preset.
The Docker page's warning says a Docker gateway answers anyone until
sharing is on, because MAGPIE_ADDR is 0.0.0.0 there.
The new links in the top nav bar carry a toc-only class:
- docs.js still lists them in the contents;
- the bar keeps today's width, since nine links overflowed it at
1024-1280px.
`.steps li` gets min-width: 0, so a long inline code no longer widens
the grid past a phone's width, where `overflow: hidden` clipped the
text.
Run:
- Real containers from ghcr.io/yetone/magpie:latest (v0.1.1150):
- the documented `docker run ... web` came up healthy, and its log
output is what the page shows;
- with "lan": true, `Bearer magpie` got 401 and a gateway key got 200;
- a second container ran `provider add remote-magpie <key>
url=http://<a>:3425 id=office` and listed office/test-relay/... models.
- `node --test site/*.test.mjs`: 23 pass.
- Playwright, in Chromium and WebKit, at 1280, 1024 and 390, light and
dark:
- no horizontal scroll and no nav overflow on all 20 docs pages;
- every internal link and #anchor resolves. /zh/ and /ja/ are routed
by the worker and return 200 live.
|
||
|
|
c65d9fb473 |
codex: magpie-models.json keeps the keys the user added by hand to a model, and says supports_reasoning_summaries itself, so Codex before 0.145 sends the effort and shows thinking (heng91, #1450)
What was wrong: every write of ~/.codex/magpie-models.json rendered the catalog from magpie's models alone, so a key the user had put into an entry was gone after the next model switch, catalog sync (Refresh, a provider added, a gateway start) or a switch to Codex's own default, which also deleted the file outright. The key heng91 added, supports_reasoning_summaries, is one Codex before 0.145 needs: its client.rs build_reasoning sends no `reasoning` (no effort, no summary, so no thinking shown) for a model whose entry doesn't say true, until openai/codex#32206 (first in 0.145). 0.144 also refuses to load a catalog an entry of which lacks the key at all. What changed: - codexcat: every entry says supports_reasoning_summaries, true for a model with effort levels, false otherwise. Later Codex ignore it. - codexcat.Keep: before a write, the keys of an entry on disk magpie doesn't write are carried over to the entry of the same slug, and a true of supports_reasoning_summaries stays over magpie's false. A key magpie writes keeps magpie's value; a model magpie no longer serves drops out; an unreadable file keeps nothing. - agent/codex.go: the write when magpie becomes the provider and the catalog sync go through Keep; the three places that removed the file remove it only when nothing of the user's is in it. Run: - New tests TestCodexCatalogKeepsWhatTheUserAdded, TestCodexCatalogRemovedWhenNothingAdded (agent, from the file magpie writes, edited by hand) and TestCodexCatalogReasoningSummaries, TestKeepUserKeys (codexcat). Each fails on origin/main's code. - Real Codex in a sandbox CODEX_HOME against a capture server: 0.144.0 fails to load the old catalog ("missing field supports_reasoning_summaries"), loads the new one and sends reasoning {effort: high, summary: auto}; with the user's keys kept it sends summary "detailed". 0.162.0 loads both and sends the same. - gofmt -l clean; go vet, GOOS=linux and GOOS=windows go vet of ./internal/agent ./internal/codexcat; linux and windows builds; go test -tags nogui ./... under a temp HOME, all ok. |
||
|
|
229ce19acc |
gateway: the loop guard's end-to-end tests bound how soon the loop was cut by what reached the agent, not by how far the fake vendor wrote ahead of a slowed gateway (#1443)
Cause: loopCut failed when the fake vendor had sent more than 2000 lines
by the time it saw its request let go. That count isn't the guard's: the
cut is made in holdWriter.Write, on the write that trips it, and cancels
the vendor's request there. But the vendor's writes succeed as long as the
sockets' and transports' buffers take them, so it runs ahead of a gateway
that reads slowly. Under load (CI macOS -race, a full run at load ~35) it
got ~2,800-3,200 lines ahead of a cut the agent saw at the 259th line.
Fix (test only, the guard was already deterministic): loopingVendor sends
at most 2000 lines of its loop, then holds its stream open until its
request is let go (20s, else it says it wasn't). Being let go therefore
means the guard found the loop within those 2000 lines, however the test
is scheduled. How soon it cut is now checked on what reached the agent:
the loop lines in the stream are at most the guard's window (256) plus one
32K read of the vendor's stream, which the cut's write sends whole.
Run:
- Reproduced by injecting a 300us sleep into holdWriter.Write (not
committed): the old test failed 140 of 180 subtests (-race -count=5
-cpu 1,2,4, 16 busy loops beside it), every route and protocol but chat,
"let go only after 2754..3082 lines", the vendor up to 8322. The agent
got ~259 loop lines in each.
- The new test with the same sleep and load: -race -count=50 -cpu 1,2,4,
1800/1800 subtests pass (built-in and plugin, chat/anthropic/responses/
gemini, and TestLoopEndsTheReplyOnEveryProtocol). At most 470 loop lines
reached the agent (the bound is 583; 256 + 32768/141 is ~490).
- Without the sleep, under load: -race -count=20 -cpu 1,2,4 of these plus
TestLoopGuardOff and TestLongReplyThatDoesNotLoopGoesThrough, all pass.
- The test goes red when the guarantee breaks: with cutLoop's h.stop()
disabled ("the vendor's request was not let go", built-in anthropic,
responses, gemini; chat is still let go when the failed write ends its
copy), and with the guard's trips dropped for its first 800 writes ("the
loop was cut only after 1038 of its lines reached the agent").
- gofmt -l, go vet (darwin, GOOS=linux, GOOS=windows) ./internal/gateway,
go test -race ./internal/gateway under a temp HOME.
|
||
|
|
62d7cd7be3 |
sessions: recording gateway conversations is a setting card over the list, with what it keeps and where, and a labelled delete that shows only when text is kept (lee on Discord)
What changed:
|
||
|
|
9b51428e16 |
provider: a Claude model models.dev lists after the Claude account's last fetch, Haiku 5.5 among them, is offered at once, not after the next Refresh (wakaka on Discord)
The Claude subscription's models are magpie's own list, the models.dev catalog's anthropic rows. Its fetch kept a copy of that list in models/claude.json, and available() served that copy over the catalog from then on. FetchNew fetches an account only while it has no list, so the copy was never taken again unless the user pressed Refresh: the hourly models.dev sync brought Claude Haiku 5.5 (listed from 2026-10-07) into the catalog, while the Claude account kept offering the list as it was at its first fetch. The owner's own copy is from 2026-10-03 and has no claude-haiku-5-5, though the synced catalog beside it has. An account whose fetch asks no vendor (magpieList: Claude's, and Factory's compiled-in droid list, which had the same hold-out after an update) is now always served its list as it is now. A vendor's own list (Codex, Copilot, Kiro, ...) is served as fetched, as before. Haiku 5.5's facts come from the catalog: models.dev's row (1M context, 128K output, low..max efforts, $0.10/$0.50 with $0.50/$2.50 over 100K) matches Claude Code 2.1.296's own model catalog (window 1e6, native 1M, max_output 128000, effort/xhigh_effort/max_effort, default medium). So no static table needed a row: claudeEffortsFor gives an unknown newer Claude all five levels, and claudeTopEffort rightly leaves it out (2.1.296 reads the top effortLevel only for the models up to Fable 5.1). With the model listed, the haiku tier follows the newest Haiku (claudeLight), as Claude Code's own default haiku on the Anthropic API is claude-haiku-5-5 since 2.1.293. Verified: - TestClaudeAccountServesModelsListedSinceItsFetch (models.dev's real rows) fails without the models.go change: "Haiku 5.5 not served on the Claude account: [claude-opus-5-5]". TestFactoryServesModelsAddedSince ItsFetch fails without Factory's flag. Both pass with the change. - gofmt -l clean; go vet, GOOS=linux and GOOS=windows go vet -tags nogui ./internal/provider; go build, GOOS=linux and windows build -tags nogui. - go test -tags nogui ./internal/provider ./internal/agent ./internal/catalog ./internal/gateway ./internal/gui under a temp HOME: all ok. - Not tried against a running magpie with a real Claude sign-in: the owner's gateway on 3425 wasn't running. The stale models/claude.json and synced models.json were read from the owner's cache (read only). |
||
|
|
1134c7a8af |
codex: the restart advice after a model or catalog change names the app-server codex sessions share ("codex app-server daemon restart"), on Windows and WSL too (TJHHHH, luci on Discord)
TJHHHH and luci (magpie 0.1.1137, codex-cli 0.162.0): the Codex app
listed magpie's models while the codex CLI showed only the official
list, on and off. Codex 0.162's TUI attaches to a background
`codex app-server --managed-daemon` (feature daemon_auto_start, stable
and on by default on every OS; skipped on Windows only when elevated or
detached launch is restricted, on WSL only when CODEX_HOME is on drvfs).
That daemon builds its model list once, when it starts, and restarts
itself only when its binary or version changes. So closing every codex
session and opening a new one keeps the list from before magpie wrote
model_catalog_json, while the desktop app's own app-server, restarted
with the app, has magpie's.
Reproduced with the real codex-cli 0.162.0 in a sandbox CODEX_HOME: a
daemon started with no catalog kept listing Codex's 8 models in model/list
after config.toml switched to a magpie catalog and every client closed;
`codex app-server daemon restart` made it list the catalog's models.
Nothing since v0.1.1137 changed this.
magpie's advice only said "restart the Codex app (and open codex
sessions)", which doesn't reach the daemon. On macOS and Linux the
Agents row already flags a stale daemon copy (StaleCopies), but that
returns nil on Windows and WSL, where the reporters may be. The Notice,
and the WSL codex kind's restart text, now name the shared app-server
and provider.CodexDaemonRestart. magpie still doesn't restart it itself:
that ends every session attached to it (as
|
||
|
|
5aca7d0924 |
codex: effort left at Default stays Default and says the level Codex takes then ("default (medium)"), and isn't written over with medium when a model is picked or the catalog synced (lgtm on Discord)
lgtm set Codex · WSL Ubuntu's 推理强度 to 默认 and it came back as 中.
Two things did that, on every platform (the WSL row is the same codexIn
code as the local one):
- The effort field's Get answered an unset model_reasoning_effort on a
magpie model with codexcat.DefaultEffort (medium), so right after the
pick the slider and the row read 中, not 默认.
- settle() counted an unset effort as one the model doesn't take and
wrote DefaultEffort into config.toml: on every Sync for a magpie
model, on Join, and on a model pick, Codex's own models included. So
Default was pinned as medium, and on Codex's own models whose default
is low (gpt-6.1-sol, gpt-6-astra, gpt-5.6-sol in 0.162's models.json)
magpie overrode Codex's own default.
With model_reasoning_effort unset, Codex takes the catalog entry's
default_reasoning_level (core/src/client.rs build_reasoning). Checked
for real with codex-cli 0.162.0 in a sandbox CODEX_HOME: a
model_catalog_json entry with default_reasoning_level "low" and no
model_reasoning_effort sent {"reasoning":{"effort":"low"}} to a stand-in
Responses endpoint.
Now settle replaces only a set effort the model doesn't take, Get
returns the unset value, and the effort control's options start with
Default carrying Takes: the level Codex then takes, from the entry it
reads (codexcat.TakesEffort: Codex's own models_cache entry for its
models and a ChatGPT account's codex/ ones, else the one Entries writes).
The GUI shows it as "default (medium)" / 默认(中) / 預設(中) /
デフォルト(中) / Standard (mittel). A stopped WSL distro's options get the
same Default (codexEfforts).
Tests: TestCodexDefaultEffortStaysDefault fails on the old settle
("Default written over: model_reasoning_effort = \"high\"", "Default
shows as \"high\"") and passes now. TestCodexCustomProviderModelHasEfforts
now expects Default first with Takes "high" and an unset value read as
"". codex-default-effort.test.cjs (window at 560 and 360px, tray panel;
en, zh, ja, de; Chromium and WebKit) fails on the old effortName and
passes now.
Run: gofmt -l (clean); go vet -tags nogui on darwin, GOOS=linux and
GOOS=windows for ./internal/agent ./internal/codexcat ./internal/gui
./internal/tui and the main package; go test -tags nogui of those and
./internal/catalog under a scratch HOME (all ok); the GUI effort, gui-ja,
gui-de, gui-zh-tw and i18n-keys tests (all pass), and the whole GUI suite
in both engines: 4711 pass, 8 fail, all in library-servers-bulk
(chromium, ".lib-everyon moved from 290 to 288") and purpose-filter
(webkit), which fail the same way run alone on origin/main
|
||
|
|
c1c887798b |
provider, docs: a bare model id several providers serve goes by the Providers order, said and tested (MOMO on Discord)
MOMO's {"model":"glm-5.3"} was answered by opencode-go though a6api
serves glm-5.3 too, and they asked for a 409 or for the response to say
which provider was used. The pick was already deterministic, but
Resolve's comment said "exactly one provider" and no doc said what
happens.
Decision: keep serving bare ids, no 409. magpie writes only
provider/model ids into agents' configs (agent/route.go magpieModels),
so a bare id comes from a user's own config, and a 409 would break every
such client the day a second provider adds the model. The rule is now
written down and pinned:
- found groups on (the default): GroupFor takes the bare id to its found
group auto-<slug>, members in the Providers order, smart routing and
fail-over to the next provider; a conversation stays with the member
that answered it while its cache is worth it (affinity).
- found groups off: Resolve takes the first provider in the Providers
order that exposes it, alone.
- every reply already carries X-Magpie-Provider / X-Magpie-Model
(member.go), and the usage log has the provider.
What changed: Resolve's comment; gateway-routing.md model resolution
row; reference.md, the ways a model is named and the headers;
bare_slug_test.go: two providers serving glm-5.3, both modes: the first
in the Providers order answers every new conversation, reordering moves
new ones, an answered one stays in the found group, and a 503 from the
first falls over only in the found group. It passes on the old code
too: it pins behaviour that was already there, now documented.
Run: gofmt -l; go vet -tags nogui on darwin, linux, windows for provider
and gateway; go test -tags nogui ./internal/gateway ./internal/provider
in a sandbox HOME.
|
||
|
|
e50a958014 |
gateway: an unknown model's 404 names the nearest ids, display names first, and how a model is named (MOMO on Discord)
MOMO asked for "Zen-Space-Bunny", the model's name as a picker shows it, and the 404 said only "magpie knows no model" and then every id. It named neither the id meant (opencode-zen-free/space-bunny-free, "Space Bunny Free") nor the forms a model id can take. What changed: - provider/closest.go: Closest(asked, n) splits asked into lowercase letter and digit words and scores every catalog entry: a word in the display name (the user's or the vendor's) counts 4, in the model id 3, the provider's id or name 1 and only alongside one of the others; the whole of asked found in the name or id adds 4. - gateway/unknown_model.go: unknownModel puts "did you mean …?" (up to 3), then the forms (provider/model, a routing group's id or name, a bare model id and where an ambiguous one goes), then the ids as before. Used by the chat 404 and the embeddings 404. Switched-off, disabled-group and empty-group 404s are unchanged. Draw and video keep their own 404: their models are a different list, and the chat catalog's nearest ids would point at models that can't draw. - docs: gateway-routing.md model resolution row. Tests: unknown_model_test.go: "Zen-Space-Bunny" against opencode-zen-free serving space-bunny-free named "Space Bunny Free" is told that id first and the forms; a provider's name alone suggests nothing; a display name ranks before an id with the same words. The 404 test fails with the gateway hooks reverted. Run: gofmt -l; go vet -tags nogui on darwin, linux, windows for provider and gateway; go test -tags nogui ./internal/provider ./internal/gateway and the root package in a sandbox HOME. |
||
|
|
0311c720c1 |
gateway, provider: a model its vendor answers is deprecated leaves that provider's lists until it is answered again, and the next member is asked (MOMO on Discord)
MOMO's /v1/models listed opencode-zen-free/exo-free, and a request for it got 410 "Model exo-free has been deprecated.". OpenCode Zen's own /zen/v1/models still lists exo-free (checked 2026-10-10), and models.dev gives it no deprecated status, so the zen-free plugin's discovery keeps it and a refresh of the list would bring it straight back. No preset hardcodes it. What changed: - fallback.go: retiredWords / modelRetired: a 400, 404, 410 or 422 saying the model is deprecated/retired (Zen's "ModelDeprecated", OpenAI's "The model ... has been deprecated"), without quota words. It is retryable (410 wasn't), and modelRefused, so only that model rests. - gateway.go: such an answer calls provider.Retire(provider, model); routing.go: servedCandidate calls Unretire. - provider/retired.go: retired.json beside providers.json, re-read by mtime; Retire/Unretire touch the catalog so agents' lists are written again. buildEntries leaves a retired model out, for that provider only, for at most a week. provider/model requests are still sent. - docs: gateway-routing.md failure handling, reference.md. Tests: retired_test.go with Zen's literal 410 body: the model leaves /v1/models while the provider's other model stays; a group falls over to the next member and only the 410's provider is marked; modelRetired cases. All three fail with the hooks reverted. Run: gofmt -l; go vet -tags nogui on darwin, linux, windows for provider and gateway; go test -tags nogui ./internal/provider ./internal/gateway ./internal/agent ./internal/catalog in a sandbox HOME. |
||
|
|
52f6b3a3ab |
plugins: the GitHub list waits up to 3s for a repository's https icon fetched the first time (yetone: 非官方插件显示不出来 logo)
RepoIcons starts every fetch, then waits for those not in yet, so the list's first answer has them; one slower than the wait shows the next time. The fetch takes fetchPluginIcon before its goroutine starts. Ran: go test ./internal/provider ./internal/gui; the new test -race -count=10, and red with a 1ns wait. A sandbox build (fresh HOME, real GitHub): the first /api/plugins/github answer has all 8 icons the 13 tagged repositories give, screenshot in light and dark. |
||
|
|
3e5bc3d8fe |
gateway, usage: conversation text is kept only under a session the client named, and a client's "request-…" session counts like any other (#1355)
What changed: with recording on, a request that names no session used to get a made-up "request-…" id and its text saved under it. Nothing listed that id and GatewaySessionByID turned it away, so the text could never be opened. magpie now makes up no id: such a request isn't recorded, and the X-Magpie-Session reply header only echoes the client's own session. With no made-up ids left, the "request-" prefix checks in appendUsage, OTel, Usage's sessions, Routing's vias and the gateway Sessions projection only dropped sessions clients named themselves (appendUsage wiped them from the ledger). They are gone. The tests that held the old rule now say the new one, and gateway-sessions.md says it. Run: TestGatewayConversationNeedsTheClientsSession and TestGatewayConversationClientRequestPrefixedSession fail on the PR's code (a made-up id is returned; the ledger keeps 0 of 2 calls) and pass here; putting appendUsage's prefix check back turns the second red again. gofmt -l clean; go vet; GOOS=linux and GOOS=windows go vet -tags nogui on gateway, gui, sessions, settings, usage; go test -race -timeout 900s on those five: ok (gateway's TestCORSKeyThroughTheServer rerun after its fix). GUI: gateway-conversations, gateway-mode and all sessions-* tests, 216 cases in Chromium and WebKit: pass. |
||
|
|
c9b8263378 |
sessions, gateway: Sessions lists gateway sessions, and can keep their text on this computer when turned on (#1355, mu-zi-lee)
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. |
||
|
|
12c0550ae7 |
gateway: a signed thinking block with no text goes to Messages with its empty thinking, not without the field (#1447, Edward4226)
The second refusal in #1447: OpenCode Go's Claude models answer a
thinking block with a signature and no text,
{"type":"thinking","signature":"abc123"}, with 400 "messages.1.content.0.
thinking.thinking: Field required". Messages requires the field, but
aBlock's Thinking was omitempty, so buildAnthropic dropped it whenever a
signed part had no text: Claude Code's own thinking when its display is
omitted ("thinking": "" with a real signature), and Gemini's signed
parts, translated to an Anthropic upstream.
aBlock now always writes "thinking" on a thinking block (MarshalJSON);
every other block keeps leaving its empty fields out. Only aBlock had
this: the Anthropic reply encoders already write the field as a map.
A separate change from the prefill fix (
|
||
|
|
916c1cc7ea |
gateway: a custom provider's video models, marked "kind": "video" by its own model list, are started, polled and fetched at its OpenAI Videos API (#1446, Edward4226)
What changed
- Videomakers: a custom provider (no account, not a remote magpie, no
preset plan's videos, a Chat or Responses base) makes videos with the
models its fetched /v1/models marks "kind": "video"
(catalog.LiveVideomakers). Nothing is guessed from a name: sora-like-2
without the kind, a chat model and grok-imagine-video named on it all
still get the 400 and reach no upstream video route.
- /v1/videos, /v1/videos/{id} and /v1/videos/{id}/content forward to
{base}/videos the way Remote magpie's already do (filmsOpenAI), where
base is the Chat base, else the Responses base (videoBase). The vendor
gets its own User-Agent and no caller headers; passOnCaller stays
remote-magpie only.
- Custom video ids base64 the vendor's id (its own may hold dots) and end
with the key's fingerprint (customVideoID, markedKey), so a poll and its
download use the key that started the video, and a key turned off since
answers 404 "no longer on". Remote magpie's and Volcengine's id formats
are unchanged.
- Gateway key holds on all three routes: account/key holds before any
upstream call (videoAsker), model holds once the model is known
(videoModelHeld). Grok's and Volcengine's polls and downloads ignored a
key's model holds before; they now apply them too.
- videoObject's done/succeeded mapping is untouched: the custom path passes
the vendor's OpenAI object through (asOurs), so its "completed" reaches
the client as sent.
- docs/subsystems/gateway-routing.md says so under Remote magpie APIs.
Why
- Edward4226's provider serves OpenAI's Videos API, and magpie answered
400 "can't make videos" because only Grok, Volcengine and Remote magpie
were allowed.
What was run
- New tests in video_custom_test.go (httptest OpenAI Videos fake whose id
holds a dot; start, two polls, content bytes; exact upstream request log;
Responses-only base; two keys with holds; Grok poll/content held by key
models). On the old video.go 4 of 5 fail (videomakers [], 400 "can't
make videos", held Grok poll 200); the name-guard test passes on both,
as it should. Breaking markedKey by hand, or the content model check,
turns TestCustomProviderVideoKeys red.
- go test -tags nogui -count=1 ./internal/gateway/ ./internal/catalog/
./internal/provider/: ok. gofmt -l clean; go vet -tags nogui
./internal/gateway/ on darwin, GOOS=linux and GOOS=windows: clean.
- End to end: the built magpie (nogui) under a scratchpad HOME, `magpie
provider add Studio url=http://127.0.0.1:3561/v1 key=…`, `magpie serve`
on 127.0.0.1:3562, against a Go fake of the Videos API on 3561. curl
start -> queued, poll -> in_progress, poll -> completed, content ->
video/mp4 whose 200000 bytes cmp equal to what the fake served. The fake
saw POST /v1/videos with model vid-1, the polls and /content, each with
the provider's key and no X-Magpie-Agent.
- No real video vendor was used. Multipart uploads to a real OpenAI
upstream weren't tried. The GUI lists no video models per provider, so
it has nothing to check.
|
||
|
|
af1e477d83 |
gateway: a resumed reply to a model that takes no prefill is answered after it, not refused (#1447, Edward4226)
OpenCode Go's Claude models (over Anthropic Messages) refuse a conversation ending with the assistant's message, "This model does not support assistant message prefill. The conversation must end with a user message.", and Volcengine's Agent Plan does too, "The last message cannot be from the assistant for a model that does not support prefill". magpie took every Anthropic upstream to go on from a prefill, so both a client's own resume (Codex sends what it has of a cut reply as the last assistant message) and magpie's own continuation of a cut reply (#853) ended with that refusal. The refusal is about the model, so it is remembered for that provider, model and API in unfit (prefillRefused); the provider's other models, and every upstream that takes a prefill, are unchanged. - A Chat or Responses client's conversation ending with the assistant's message, which those APIs read as a turn already said, is asked again with a user turn after it ("Continue."), the conversation the refusal asks for, and so from then on: translated (forwardTranslated, whole and streamed) and relayed as is (passthrough, Chat and Responses: the Volcengine case). - An Anthropic client's prefill is its own ask and still gets the refusal as the vendor sent it. - magpie's own continuation is not asked of that model again, and the try it refused ends the reply with what cut it, as a reply that can't go on did before #853, not with the refusal of an ask the client never made. No user-turn continuation there: a re-answer spliced onto the part the client has is the garble continuation.go avoids. The thinking-block refusal in the same report (a signed thinking block with no text) is a separate fix, in its own commit. Tests (prefill_test.go), from the report's literal messages: the issue's Responses repro and a streamed Chat request to an Anthropic upstream (TestTrailingAssistantToNoPrefillModel, also checks another model keeps its prefill), Volcengine's refusal relayed on Responses and Chat (TestTrailingAssistantRelayedToNoPrefillModel), a cut reply (TestStreamCutOnNoPrefillModel), an Anthropic client's prefill (TestAnthropicPrefillToNoPrefillModel), userLastBody. With the refusal unrecognised (the old code's behaviour) the first three fail with the report's "UP: Upstream request failed: [invalid_request_error] This model does not support assistant message prefill…" / "…the reply didn't end with what cut it"; with the remembering removed they fail with "calls 4 … asked one again" and "calls = 2 … asked to go on again". Run: gofmt -l (clean), go vet -tags nogui ./internal/gateway on darwin, GOOS=linux and GOOS=windows, go test -tags nogui ./... under a scratch HOME with pinned caches (all ok). Not run for real: the owner's OpenCode Go key answers 403 "An active OpenCode Go subscription is required", and there is no Volcengine Agent Plan account. |
||
|
|
8c21a41846 |
gateway: auto mode's server review reaches Claude alone, and its verdict comes back on every route (lc on Discord)
lc saw Claude Code's notice that the session isn't eligible for the free server-side auto mode classifier "because your requests go through 127.0.0.1:3425". Claude Code 2.1.295 sends the dangerous-tool-use-2026-09-03 beta and a `safeguards` field, and reads `safeguard_results` from every reply (a stream's message_delta, a whole reply's top level). A reply without them latches the session to the local classifier and shows the notice. What changed: - serverReview (automode.go), called from forwardOnce, so passthrough, count_tokens and the translated path alike: a body naming a model that isn't Claude (DeepSeek's, Kimi's Anthropic APIs) goes without the beta and without safeguards. AiHubMix answers them with 400 "safeguards: Extra inputs are not permitted"; DeepSeek ignores them. A provider that turned the beta away (remembered by s.betas, e.g. Bedrock's "Unexpected value(s)") gets no safeguards either. Claude, a body with no model (Bedrock's path) and api.anthropic.com keep both as sent. - anthropicWholeEvents: a whole reply streamed after keepalives put safeguard_results in message_start, where Claude Code doesn't read it. It now goes in message_delta.delta. - buildAnthropic sends safeguards to a claude-* model; decodeAnthropic carries delta.safeguard_results to the encoder, which already wrote them. Tool ids are left as they were on every path. - magpie does not write CLAUDE_CODE_AUTO_MODE_SERVER=0 for other vendors' models: /model can move the same session to a Claude model behind the gateway, and the setting would hide its eligibility there. Said in automode.go's header comment. How magpie presents itself is unchanged. Tests use Claude Code 2.1.295's captured beta header, an object classifier_context and the status.available.tool_uses verdict shape. TestAutoModeReviewOnlyForClaude, TestAutoModeReviewGoesWithItsBeta, TestAutoModeReviewOfAWholeReplyStreamed and TestAutoModeReviewTranslated each fail with the source changes reverted (400 from the vendor, verdict in message_start, safeguards dropped to Claude / verdict not given back). Run: gofmt -l; go vet, GOOS=linux and GOOS=windows go vet -tags nogui ./internal/gateway; go test -tags nogui ./internal/gateway (ok, 191s) under a scratchpad HOME. End to end: real Claude Code 2.1.295 -p --permission-mode auto under a sandbox CLAUDE_CONFIG_DIR, through this build's gateway on 127.0.0.1:3563, to a fake Anthropic API answering with safeguard_results: on ant/claude-sonnet-5 the beta and safeguards reached the fake, Bash ran on the server verdict and no notice was shown; on ds/deepseek-flash neither reached the fake and Claude Code classified locally with the notice, as designed. Not run against Anthropic's real API: no first-party Anthropic key was available. |
||
|
|
a998a36367 |
provider, gateway: remote magpie serves decision and retrieval APIs (#1282)
* provider, gateway: remote magpie serves decision and retrieval APIs Configure System One from the remote address and discover explicitly marked decision models, including Clef and custom names. Keep exposed retrieval models when importing the remote catalog, without treating decisions as chat or image generators. Refresh removes stale decisions and the editor receives an explicit empty decision list when none remain. Forward caller/session attribution for decision, retrieval and media APIs. Preserve TUI model-list errors for a remote that cannot be reached. Document the HTTP API surface and old-peer compatibility. Verified with the full nogui Go suite, focused race checks, go vet, native macOS and Linux/Windows cross-builds, and the Remote magpie browser tests in Chromium and WebKit in English and Chinese. The new System One integration test fails on the old code with an empty model list, and the retrieval test fails because the remote embedding model is filtered out. Integration tests exercise two gateways and simulated upstreams; live vendor accounts were not used. * gateway, provider: remote forwards keep official client headers Claude Code and Codex keep their User-Agent through both gateways, so a relay behind the remote that admits only official clients accepts their requests. Only decision, retrieval and media calls set magpie's User-Agent at their own call sites. The two-hop test covers Messages, Responses, Chat Completions and a Codex originator with another User-Agent. Keep entryFor limited to remote decision providers. Test rejection of unlisted remote decisions and remove the redundant resolveDecideModel early return: the explicit model marker and list-only Jev already exclude vendor aliases. Document retrieval entries remaining in the shared agent catalog and TUI list errors being shown for other mixed providers too. Verified with the Remote magpie provider/gateway tests. Before the fix, TestRemoteMagpieClientHeaders fails all four cases with the relay receiving magpie/dev and returning 403 official client required; all pass after it. Follow-up to yetone's review of #1282. * provider, gateway, gui: remote discovery reports only the decisions it lists (#1282) Show CLI list failures and catalog counts for mixed providers. Derive the remote decision endpoint from its one editable address, name custom decisions by their ids, and reuse successful discovery in endpoint tests without an empty decide OK row. Bare-provider decision selection is documented. Simplify decision classification around the explicit remote list and reuse model markers while building Available, Catalog and Deciders, avoiding a whole-list copy and scan for every model. Other providers keep their rules. The regression tests failed before the fixes on swallowed CLI errors, blank labels, extra endpoint fields, empty decide results and repeated discovery. Deliberately removing retrieval/video User-Agent or System One caller attribution makes all three receiving-side tests fail with lost agent/via. Verified with sandboxed Go tests, targeted race checks, Linux GUI/nogui and Windows vet/build, Chromium/WebKit en/zh browser tests and zh-TW/ja/de checks. The two-hop tests use real gateway handlers with simulated vendor replies; real vendor accounts and native Windows/macOS runs were not used. Co-authored-by: XYenon <register@xyenon.bid> Amp-Thread-ID: https://ampcode.com/threads/T-01a11cd6-93aa-767c-9d6e-f0db2a9c5bab * gui, gateway tests: remote decision refresh leaves only its current model choices (#1282) The empty-decision browser fixture cleared deciders but kept decision entries in models, so its screenshot still showed the same chips. Remove those entries and assert the visible choices in both states. Check the real providerInfo model list after refresh too, with Jev Router retained as chat, and verify an unnamed remote decision reaches the local picker with its id and provider label. No production code changes. Verified all Remote magpie tests in root, provider, gateway and gui under a sandbox HOME with -race; the new assertions in gateway/gui pass. Chromium and WebKit pass all six browser cases in English and Chinese; the corrected screenshot run passes 3/3. Targeted Linux and Windows nogui vet and gofmt are clean. Amp-Thread-ID: https://ampcode.com/threads/T-01a11cd6-93aa-767c-9d6e-f0db2a9c5bab Co-authored-by: XYenon <register@xyenon.bid> --------- Co-authored-by: Amp <amp@ampcode.com> |
||
|
|
566760147a |
provider: moved Factory users get plugin 0.1.22, whose Claude Code restored-context turn after /compact no longer reaches Factory unadapted (plugins#68, Vigilans)
After /compact in Claude Code 2.1.280, every turn on factory/claude-opus-5-5 was 403 "Factory: Forbidden". The reporter's bisect found that Factory refuses each fixed fragment of the turn Claude Code sends with the files it restores: the "Note: <path> was read before the last conversation was summarized…" notes, "Called the Read tool with the following input:", the "# Environment\nYou have been invoked in the following environment:" paragraph and the model line. Plugin 0.1.21 recognized none of them in that combined, unwrapped block. Plugin 0.1.22 (magpie-community/plugins 0437abb, on npm) recognizes the complete block and applies the existing compat spellings and the lossless Read-result encoding to each fixed paragraph, the rest byte-for-byte. The mover's min is raised so keepMovedCurrent updates moved users. The built-in, which has none of the plugin's Claude Code adaptations, is unchanged. Plugin: its two new tests use the issue's literal fragments; the adapting one fails on 0.1.21; bun test (1010 pass) and check.mjs pass. Here: go test -tags nogui -count=1 ./internal/provider/, go vet -tags nogui for darwin, linux and windows, gofmt -l pass. Stubbed fetch only; not tried against Factory itself (no Factory account here). |
||
|
|
599d784f59 |
library: magpie-image runs this computer's magpie, whatever path another computer or an old install saved (#1439, dawn1095)
magpie-image was kept in library.json with one computer's magpie by full path (os.Executable at install). A WebDAV sync carried C:\Users\...\Downloads\magpie-windows-amd64.exe to a Mac (and a Mac's app to Windows), and a magpie moved out of Downloads left its old path behind: the Library page said it couldn't start, and agents were given a program that isn't there. - heal (self_here.go): a server that is magpie's own -- `mcp ...` run by a program named magpie (magpie, magpie.exe, magpie-<os>-<arch>[.exe]) -- whose program isn't an existing absolute path here is pointed at this magpie, as the library is read (load) and after every change (so a bundle Put from another computer is healed before it is written into the agents). Every agent writer, project MCP files, the WSL side and CheckServers read through load, so all of them get it. A magpie program that does exist here (another build) and any server run by another program are left as they are. - Collect carries magpie's own server as plain "magpie": two computers with the same library carry the same bundle, so davsync's library hash matches and a sync doesn't write one's path over the other's. - The path given is update.Program (exported cliProgram): the AppImage rather than its mount, Homebrew's opt link rather than the Cellar. - TestWSLMagpieImage now says which magpie this (simulated Windows) computer runs. Verified: - New tests: TestOwnServerFromElsewhereRunsThisMagpie (Windows path read on macOS, a Mac path, an exe moved out of Downloads, a bare "magpie"), TestUsersOwnServerStaysAsItIs, TestCarriedOwnServerIsEachComputersMagpie. Broken by hand five ways (heal off; no heal in change; no marker in Collect; ownServer matching every server; existing magpie builds not kept) -- each went red with a FAIL naming the behaviour, green restored. - gofmt -l clean; go vet -tags nogui ./...; GOOS=linux and GOOS=windows go vet (library, update, davsync); go build, GOOS=linux -tags nogui and GOOS=windows builds; go test -tags nogui ./... green in a sandbox HOME. - Sandbox app (own HOME, ports 3561/3562) seeded with a library.json holding the reporter's Windows path for magpie-image plus a user's own missing Windows tool: on start library.json, Codex's config.toml and Claude Code's .claude.json got the running binary; /api/library/mcp/check answered magpie-image ok with 3 tools, the user's server unchanged and notfound. Not verified: on the Windows box, or a real two-computer WebDAV sync. |
||
|
|
7f62dbf3ed |
gateway, agents, gui: a model's reply limit is published within the window beside it, on /v1/models, Cursor's and Muse's lists, the Gateway page and magpie model output (#1438, yhong91)
#1438 (yhong91): GET /v1/models listed grok/grok-4.7 with context_window 256000 (the Grok backend's own) and max_output_tokens 500000 (models.dev's x-ai entry, which catalog.Decorate fills in when the backend gives no output). A client that reads both sizes its replies past what the window holds. The agents' files already kept output within the window (maxTokens, #338/#347), but the gateway's lists, the GUI and the CLI did not. What changed: - provider.OutputWithin(window, output) and Entry.PublishedOutput(): the output is kept within the window; an unknown window or output (0) is left as it is. - Every place that publishes the pair uses it: modelObject (/v1/models), cursorLocalModel (Cursor Private Inference), Muse Code's list, the Gateway page's models and groups (providerInfo, providersState), the agents' maxTokens, and `magpie model output` (which also says "500k, kept within its 200k window" when it lowers one). - Entry.Output stays the model's own limit. The request-path cap (outputLimit/withMaxOutput) and the user's Max output setting are unchanged, so no request is lowered further than before. This is why the clamp is not in replyLimit/entryFor as the issue suggests, and why the commenter's patch (modelObject only) was not taken as is: it was right but missed Cursor, Muse, the GUI and the CLI. - docs/subsystems/gateway-routing.md: the rule and its routes. Tests (each fails without the change): - gateway TestModelListsKeepOutputWithinTheWindow: models.dev 500000 over a backend 256000 window, plus output within the window, unknown window and unknown output, on /v1/models, Cursor's capabilities and Muse's limit; a group request with max_tokens 300000 still reaches upstream as 300000. - provider TestOutputWithin, gui TestProviderModelOutputWithinTheWindow, TestModelOutputCmdWithinTheWindow. Breaking it by hand: OutputWithin returning output unclamped turns all of them red, plus agent TestMaxTokensWithinContextWindow; gateway sites back on e.Output turn the gateway test red on all three lists; moving the clamp into Entry.Output turns the fixture and the group-request guard red. Real data: an offline dump of the owner's catalog (copies of ~/.config/magpie and its caches, 203 entries) has 3 models with output over their window, grok/grok-4.5, 4.6 and 4.7 (256000 vs 500000), now published as 256000; 0 are not Grok. The live gateway on 3425 was not running, so /v1/models itself was not read, and no real Grok request was made. Run: gofmt -l; go vet -tags nogui ./... on darwin, linux and windows; go test -tags nogui ./... -count=1: all green except TestLoopEndsWorkBuddysReply/plugin/anthropic ("let go only after 3207 lines", bound 2000) at load average ~35 with other suites running; it passes 5 of 5 alone and does not touch the output limit or request path. |
||
|
|
b4bdd59f72 |
gui: a long app menu stays in the window and scrolls inside itself, so the last removed group can be brought back (#1437, Hipple)
With 28 found groups removed, Routing's "28 found groups removed" menu ran
off the bottom of a ~717px WebView2 window and couldn't be scrolled.
openRowMenu had no height cap (max-height: none, and .pop clips with
overflow: hidden), it only flipped up when the whole menu fit above, and any
scroll, even one inside the menu, closed it.
What changed:
- placeMenu (app.js) places every app menu: under its anchor, over it when
the window runs out below, and one that fits neither way takes the larger
room with its height held to it. openProtoMenu's placement moved into it
unchanged; openRowMenu (agent rows, model chips, Routing's removed groups
and its other row menus) and openSessCombo (the Sessions/Usage filters,
which could run off the bottom the same way) now use it too.
- .row-menu scrolls inside itself (overflow-y: auto, overscroll-behavior:
contain), and openRowMenu closes only on a scroll outside the menu, as
openProtoMenu and openSessCombo already did.
- docs/subsystems/gui-shell.md names placeMenu.
Verified:
- New removed-groups-menu-scroll.test.cjs: 30 removed groups, 1100x700 and
560x700, en and zh, Chromium and WebKit, 8/8 pass. It wheels the page to
the line, clicks it where it is, checks the menu is inside the window and
scrolls, wheels the menu until the last entry shows inside it, clicks it
(posts groups/show auto-gone-30), and checks the page never scrolled and
has no sideways scroll.
- Broken by hand three ways, each red in every case: the old app.js/app.css
("the menu stays in the window": bottom 1494 of 700), the old
scroll-closes-menu handler ("scrolling the menu doesn't close it"), and no
overflow-y on .row-menu ("the last entry is scrolled into view inside the
menu").
- The 63 GUI test files that open app menus or guard click-scroll, both
engines: 595 pass, 1 fail: search-api.test.cjs webkit zh, at page load
(the row is redrawn between waitFor and scrollIntoViewIfNeeded) before any
menu opens; it passes 5/5 alone. Not run: the rest of the GUI suite, and
gui-zh-tw/ja/de (no new strings).
- gofmt (no Go changed), go vet ./internal/gui (darwin, GOOS=windows),
GOOS=linux and GOOS=windows go vet -tags nogui ./internal/gui, and
go test -tags nogui ./... under a sandbox HOME: all pass.
- Not tried in a real Windows WebView2 window.
|
||
|
|
5b46b6142f |
gui: Names in agents' lists shows the saved choice on Routing and in the provider editor before Settings is opened (#1433, ObsidianArch02)
With plainNames: true, plainOwnNames: false saved, a restart that opened Routing before Settings showed "Names in agents' lists" as On. suffixMode() read only `prefs`, Settings' own read of the settings, which stays null until the Settings page loads, though startup had already read the same settings into state.settings. The provider editor's Names & levels (Provider in model names, and its "Agents' lists show ..." line) and a group editor's label read the same thing, and were wrong the same way. - suffixMode() reads state.settings, the copy every page draws from. A plain-names pick and Settings' own read (loadSettings) now set it as well as prefs, as savePrefs and the other cross-page writes already do, so the two copies agree. - Routing's groups can answer before /api/state, so the page can be drawn before any settings are in. Each suffix choice records the mode it was drawn with, and load() redraws one whose mode the incoming state changes (redrawSuffix), with its place's own redraw (Routing's groups, the editor's line). Siblings checked: every other read of `prefs` outside the Settings page. loadFonts already falls back to bootPrefs, the update row's "Automatic updates are off" is drawn only on Settings, and the other cross-page settings (agent order, usage order, panel cards, quota left, desktop longest, codex credits, auto-reset, check-ins, update pill) already read state.settings. suffixMode was the only one. Verified: - new internal/gui/tests/name-suffix-saved.test.cjs: Routing opened first with Off saved (then Settings and back), Routing drawn before a late state with "Not on names I set" saved, and the provider editor opened first with Off saved; no settings write, no page error. Chromium and WebKit, en/zh/zh-TW/ja/de, 1100 and 560px: 30/30 each. - On the old app.js it fails 30/30 in each engine (expected 'Off', actual 'On'; expected 'Not on names I set', actual 'On'). With the fix but without redrawSuffix() in load(), the late-state case fails 10/10 in each engine. - 86 related GUI files (name-suffix-*, model-suffix-own, plain-names, names-fold, gateway-mode, group-*, panel-routing, settings-*, routing-*, currency, webdav-settings-refresh, codex-memories-settings, click-scroll, i18n-keys): WebKit 376/376; Chromium 375/376, the one being routing-blocked en, which fails the same on the old app.js (5 of 8 runs of a traced copy): the view scrolls 0 -> 42 -> 201px in the frames after the day click and the test takes its "before" position during that. Not caused by this change. - 186 more GUI files that open Settings or Providers: Chromium 943/943, WebKit 935/935. - go build/vet -tags nogui ./internal/gui (embedded assets). No Go source changed. |
||
|
|
ca0867e412 |
gateway: a Grok model's 2500.0 in a call's arguments reaches the agent as 2500, on every route and protocol, deltas and done alike (#1431, timonwong)
Grok's models (grok-4.6, grok-4.7) write a whole number in a tool call's
arguments with a fraction of zeros. Codex reads exec_command's
yield_time_ms and max_output_tokens as u64/usize and write_stdin's
session_id as i32, though their schemas say "number", and serde turned the
whole call away ("failed to parse function arguments: invalid type:
floating point `2500.0`, expected u64 at line 1 column 77"; `17277.0`,
expected i32), so the turn stopped at its first command. Reported 11 of 11
turns through a sub2api relay in front of grok-4.6 and grok-4.7.
What changed (grok_integral.go):
- grokModel/grokServed: a Grok model is one whose id, as magpie names it
or as it is sent upstream, has a last `/` segment beginning "grok"
(relay/grok-4.6, x-ai/grok-4, the plugin's grok-4.7).
- wholeNums: a streaming scanner over the arguments' JSON text. A number
written with a fraction of zeros and no exponent loses the fraction
(2500.0 -> 2500, -3.00 -> -3, [1.0,2.0] -> [1,2]). Strings, keys, other
fractions (2.5, 2.50) and exponents (1.0e3) stay as written. It holds
only a run of ".0…" until the next byte, so arguments cut into deltas
anywhere come out the same as whole, and the deltas add up to what the
done events say.
- Relayed (passthrough): intTidy, beside nsTidy and built like it, on
Responses (function_call items, argument deltas and done, the completed
response, a whole reply), Chat (tool_calls deltas and messages) and
Messages (tool_use input and input_json_delta), streamed or whole. A
line that carries no call passes as it came. Plugin-served Grok and the
built-in subscription go through the same passthrough.
- Translated: translate sets Request.Grok; encoder() wraps the client's
encoder in wholeEncoder (outside usageEncoder) and render() rewrites the
Result's calls (wholeResult), so every client protocol is covered at
those two choke points.
- Any other model's reply is relayed byte for byte, as before.
- gateway-routing.md says so, and lists the tests.
Scope, against the reporter's 9fd6855b: that commit rewrote only fields a
tool schema typed "integer" or a fixed list of Codex's (codexInts), only
on Responses, and left deltas as they came, so the deltas disagreed with
the done events, and the list missed Codex's other integers (shell's
timeout_ms, sleep's duration_ms, tool_search's limit, the multi-agent
tools' timeout_ms). Codex's own schemas say "number", and JSON has one number type, so 2500 is the same value as
2500.0 for every client; this drops the zero fraction everywhere in a
Grok model's call arguments instead. Its rollout fixtures are kept
verbatim (testdata/grok_integral_args) and its test helpers adapted.
Tests: TestWholeNumsAnyCut (every split point and byte by byte),
TestGrokCallsGiveCodexIntegers (relayed Responses, streamed with deltas
cut whole, byte by byte and at every '.'/zero, and whole),
TestGrokCallsGiveCodexIntegersTranslated (Chat upstream -> Codex),
TestGrokCallsGiveIntegersOnEveryProtocol (Chat relayed, Messages relayed,
Messages translated from Chat), TestGrokCallsByUpstreamName,
TestGrokPluginCallsGiveCodexIntegers (fake plugin, $FAKE_GROK),
TestOnlyGrokCallsGetIntegers (gpt-5.5 byte for byte on Responses, Chat
and Messages relayed; translated keeps 2500.0), TestGrokModel. Each
recorded case checks that the field Codex's own error names is now an
integer of the Rust type it named, wherever Codex reads the call.
With grokModel forced false, every Grok test fails; with the delta
rewriting turned off (intTidy and wholeEncoder), every streamed case
fails on deltas != done.
Run: gofmt -l (clean); go vet -tags nogui ./... on darwin, GOOS=linux
and GOOS=windows; go build ./... and GOOS=linux/windows -tags nogui
builds; go test -tags nogui ./... under a temp HOME; -race -count=3 on
the new tests. End to end: the built binary (`serve`, sandbox HOME, port
47812) in front of a fake upstream streaming 2500.0 a byte per delta on
Responses and Chat; a client reading /v1/responses and
/v1/chat/completions, streamed and whole, got integers in deltas,
arguments.done, output_item.done, completed and output for grok-4.6
(relayed) and grok-4.7 (translated from Chat), and 2500.0 unchanged for
gpt-5.5. The same client against a binary of the parent commit failed
all 12 Grok checks.
Not verified: a real Grok model or a real Codex turn; Messages and Chat
Grok replies have not been seen in the field; a relayed Messages
tool_use input that is rewritten has its keys re-marshalled in sorted
order; exponent forms (2.5e3) are left as written.
Co-authored-by: timonwong <timon86.wang@gmail.com>
|
||
|
|
7d0e415e62 |
plugins: two plugins that sign in to one provider are both listed, each row says which serves it, and Use for picks the other (neiko on Discord)
neiko on Discord installed a third-party plugin beside a plugin of their
own, added by its folder. Only the third-party one could be seen. Both
plugins' auth hooks name the same provider id. The host's auths() keeps
the last plugin in plugins.json for an id, as OpenCode does, so the
user's own plugin silently lost it:
- its row read "Signs in to nothing magpie can use";
- its provider was gone from the Add sheet;
- keepUnloaded could list the id a second time, from the
plugin-providers.json kept before.
A package-name clash and a display-name clash were tried as well. Both
plugins stay visible in those cases, because a folder plugin's name is
its path.
What changed:
- host.js:
- init gives each loaded plugin the ids it signs in to (provides) and,
for each id another plugin serves, that plugin's spec (servedBy).
- auths() honours plugins.json's new prefer map (id -> spec).
- Only the serving plugin's provider.models hook runs for the id.
- plugin:
- Clashes() turns provides and servedBy into {id, by, with} per spec.
- Prefer() records a pick and restarts the host.
- Remove drops the removed plugin's picks.
- Re-adding a plugin under a new spec carries its picks over.
- remove, off, options and prefer find a plugin by its exact spec
before its package name (indexOf), so acting on one plugin never
reaches another of the same package.
- keepUnloaded no longer re-adds a provider from before when another
plugin serves its id now.
- GUI:
- /api/plugins rows carry clashes, with the provider's listed name.
- POST /api/plugins/prefer {spec, provider}.
- The serving row says "{other} signs in to {name} too; this one
serves it".
- The other row says, in amber, "{name} is served by {other}, which
signs in to it too", with a "Use for {name}" button.
- New strings in zh, zh-TW, ja and de.
- CLI: `magpie plugin` prints the same lines, with the command that
fixes it, and `magpie plugin use <name> <provider>` makes the pick.
- Docs: docs/subsystems/provider-plugins.md, docs/reference.md and
site/public/docs/{,zh/,ja/}plugins.html.
What was run:
- Reproduced under a sandbox HOME with magpie web on 3562: a fake npm
plugin and a folder plugin, both on id "acme".
- Old binary: the local row had providers [] and /api/providers
listed acme twice.
- New binary: both rows carry the clash; prefer moves acme to the
folder plugin; remove clears the pick and leaves the other plugin
serving acme.
- New Go tests:
- TestTwoPluginsOneProvider, a real Bun host: Prefer, restart,
off/on and Remove.
- TestClashes, TestPluginNamedExactlyFirst and
TestKeepUnloadedLeavesAServedID.
- TestPluginsClashRows (gui) and TestPluginListSaysClash (CLI).
- Each fails when its piece is broken by hand: prefer ignored in
host.js, the keepUnloaded id check removed, name before exact spec,
Remove keeping picks, no provides, no clashes in the rows, no
prefer route.
- plugin-clash.test.cjs, in Chromium and WebKit, in en, zh, zh-TW, ja
and de, at 1100 and 560px: 22/22 pass. It times out on the old
plugins.js.
- gui-de, gui-ja, gui-zh-tw, i18n-keys and every plugin-*.test.cjs:
248 tests.
- 245 passed in the 4-wide run.
- plugin-moved-signin (webkit) failed once in that run, then passed
3/3 alone and 26/26 in a smaller 4-wide rerun.
- gofmt -l; go vet ./...; GOOS=linux and GOOS=windows go vet -tags
nogui ./...; go build ./...
- The full go test -tags nogui ./... under a temp HOME with the caches
pinned: 44 packages ok, none failed.
- After rebasing onto
|
||
|
|
059171006b |
library: DeepSeek Harness is given a server's ${NAME} as a !!js expression it reads from its environment (#1435, CnsMaple)
A Library MCP server whose headers or env named a variable as ${NAME}
(#1250) was refused for dsh, because dsh had no reference syntax in
refsOf. dsh does have one: a YAML !!js value is evaluated as the row
activates (dsh 0.2.0-rc.2: cordis-plugin-include's entryListSchema
reads the tag, quoted or not; cordis-plugin-loader's interpolate
evaluates it across the whole config), and dsh-mcp-client's README uses
GITHUB_TOKEN: !!js process.env.GITHUB_TOKEN. A stdio server needs it so:
it inherits dsh's environment without credential-shaped names
(scrubbedParentEnv).
What magpie writes now (refsOf fmtDsh -> refDsh, dshRef):
- a whole ${NAME}: !!js process.env.NAME (bare, as the README has it)
- text mixed with variables: the text as JSON string literals joined
with (process.env.NAME ?? ""), YAML-quoted. The user's text is only
ever inside a JSON string literal, so nothing in it runs as code, and
NAME is the envRef pattern [A-Za-z_][A-Za-z0-9_]*. An unset variable
inside a mixed value is "", not "undefined".
dshUnref reads exactly those forms back as ${NAME}, so an entry magpie
wrote reads as same and a sync changes nothing; any other !!js still
keeps the entry the user's (hasJS), as before.
The reporter asked for the value to be resolved into a literal when the
file is written. This writes a reference instead, keeping #1250's rule
that the token never lands in an agent's file as plain text. dsh
evaluates it from its own environment at load time. Their side note
that a quoted !!js isn't evaluated is not what dsh 0.2.0-rc.2 does: its
own --dump-config prints !!js quoted, and the quoted forms evaluated in
the run below.
Since AgentView.NoEnvRefs comes from refsOf, dsh's chip is no longer
greyed and the GUI notice drops it with no string change.
docs/subsystems/library.md says what dsh gets.
Run:
- real dsh 0.2.0-rc.2 `dsh headless` in a sandbox (HOME and DSH_HOME
under a scratch dir, never ~/.dsh), on a cordis.patch.yml written by
magpie's mcpFile: a stdio fixture got T1435_A=<token> (whole) and
"Bearer <token>" (mixed); an injection string with quotes, backticks,
${, backslashes and "'); process.exit(3); ('" arrived verbatim; an
unset variable in a mixed value gave "pre--post"; a streamable-http
fixture received Authorization: Bearer <token>, the whole-variable
header and the literal header. magpie read every entry back as same.
A whole !!js process.env.UNSET makes dsh leave the server out as an
invalid config.
- go test -tags nogui ./... (temp HOME, pinned caches): green.
- break by hand: refsOf without fmtDsh, encode writing raw values, and
decode not unref'ing each make TestEnvRefsDsh fail (decode also
TestDshImportKeepsJS).
- gofmt -l; go vet ./...; GOOS=linux and GOOS=windows go vet -tags nogui
./internal/library; go build for darwin, linux and windows (nogui).
Not run: dsh on Windows (the reporter's system), and a real LLM turn
calling a tool through the server (no dsh account in the sandbox; the
MCP servers had connected before dsh stopped for its missing LLM
credential). No GUI test was run, as no GUI file changed.
|
||
|
|
a3a8e25792 |
plugins: a repository tagged magpie-plugin shows the icon its package.json gives (yetone: 非官方的插件能否支持 icon)
magpie.icon, an https URL or a data:image URI, is read with the rest of package.json and kept through pluginOwnIcon as an installed plugin's is; the card shows it, else the puzzle piece. Ran: go test ./internal/plugin ./internal/gui ./internal/provider; the plugin GUI tests in Chromium and WebKit (en/zh/ja/de, 980 and 440). |
||
|
|
849faea3a3 |
plugins: a card's kind and Unofficial chips without fills, provider and middleware each their own hue (yetone)
Unofficial is a hairline with a small amber dot; what a plugin is reads as its glyph and word, periwinkle for a provider and teal for middleware, apart from the community badge's green. Ran: plugin-github, plugin-middleware and plugin-check-updates GUI tests in Chromium and WebKit (en/zh/ja/de, 980 and 440), light and dark looked at. |
||
|
|
2c1b593675 |
library: Zed takes MCP servers, in its settings.json context_servers (lc on Discord)
lc on Discord ("agent未识别到zed"): the MCP server editor's agent chips
had no Zed. magpie already edits Zed's settings.json for its model, and
Zed reads its MCP servers from context_servers in that same file, so Zed
is now an MCP target like the other agents.
What Zed takes, from its own source (zed-industries/zed 2c99f547):
- crates/settings_content/src/project.rs ContextServerSettingsContent is
untagged: a command as command/args/env (enabled, remote, timeout
beside it), a url as url/headers (enabled, timeout, oauth), or an
extension's entry as settings. docs/src/ai/mcp.md writes the same.
- crates/context_server/src/transport/http.rs only POSTs (and DELETEs):
streamable HTTP, no SSE. So an SSE server is refused for Zed and its
chip is greyed with the existing reason (AgentView.NoSSE).
- stdio_transport.rs hands command.envs over as written and http.rs sends
headers as written: no ${NAME}. Zed is refNone, so a server naming a
variable is refused and its chip greyed (AgentView.NoEnvRefs).
Changes:
- targets.go: "zed" gets mcpFile{agent.zed's settings.json, fmtZed}; the
path is agent.zed's for every OS (~/.config/zed, $XDG_CONFIG_HOME/zed,
%APPDATA%\Zed, $MAGPIE_ZED_CONFIG_DIR).
- mcp.go: fmtZed under "context_servers", encoded as Cursor/Desktop are;
magpie owns url/headers/command/args/env only, so enabled, remote,
timeout and oauth the user sets on magpie's entry stay. The file is
JSONC and edited in place (edit.SetJSON/DelJSON), so comments, other
settings and the user's own servers are kept.
- mcp.go, sync.go: an entry Zed's extensions configure (settings, no
command or url) is never written over: a library server by that name
is a Problem for Zed (errZedExtension), asking for another name.
- envref.go: a comment citing why Zed has no syntax.
- docs/subsystems/library.md: Zed's file, format, no SSE, no ${NAME},
the extension guard, and the new tests.
Tests:
- internal/library/zed_mcp_test.go TestZedMCP, on a Zed-style JSONC
settings.json with comments, trailing commas, a user stdio server
(enabled:false, timeout), a user remote server and an extension's
entry, at the default path and under MAGPIE_ZED_CONFIG_DIR: found
servers, the page's chips, written entries, SSE/${NAME}/extension
refusals, user keys kept through an edit, uncheck and remove leaving
only the user's, comments kept, a Sync rewriting nothing.
- internal/gui/tests/library-zed-mcp.test.cjs: Zed's chip on every row
and in the editor, a click gives it the server, greyed with the reason
for SSE and ${NAME}; Chromium and WebKit, en/zh/zh-TW/ja/de, 1100 and
560px, no sideways scroll. No new strings.
Broken by hand, each went red and was restored: no zed target, no
extension guard, SSE allowed, enabled owned by magpie, wrong key (Go);
Zed without noSSE (GUI).
Run (temp HOME, Go caches pinned): gofmt -l internal/ (clean); go vet
-tags nogui ./... on darwin, GOOS=linux and GOOS=windows; go vet
./internal/library ./internal/agent; go build ./..., GOOS=linux go build
-tags nogui ./..., GOOS=windows go build ./...; go test -tags nogui ./...
(all ok); node --test library-zed-mcp.test.cjs (22/22, both engines).
Not verified: a real Zed reading the written file (the live
~/.config/zed was never touched); Linux and Windows beyond vet/build;
Zed found in WSL (not in ownFolder, so no MCP target there); a
project's .zed/settings.json (not added).
|
||
|
|
189586612e |
agents, gateway: Codex's subagents go on a model set in its row, a ChatGPT account's (willz on Discord)
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.
|
||
|
|
aa82288fb6 |
usage: Volcengine Ark's Coding Plan and Agent Plan windows, read with the account's access key (#1427, yayoinoyume)
Ark tells a Coding Plan's or Agent Plan's 5-hour, weekly and monthly
windows only to the account's control plane (GetCodingPlanUsage,
GetAFPUsage on open.volcengineapi.com), signed with the account's
AccessKey ID and Secret, never to the plan's inference key. An Ark
provider's editor now takes that access key, and the Usage page shows the
plan's windows as a card.
From yayoinoyume's feat/volcengine-usage, reviewed and reworked:
- Signing is volc-sdk-golang's (base/sign.go GetSignRequest, 06f9ef66):
signed headers sorted (content-type;host;x-content-sha256;x-date), only
Action and Version in the query. The branch signed them unsorted and put
Region in the query. TestVolcSignMatchesSDK pins two signatures the SDK
made.
- The card is in PlanQuotas beside the Step Plan's (its cache, last
reading, refresh and `magpie quota wait`), not in the subscription
cards. One per access key and plan.
- The plan is the endpoint's: /api/coding asks GetCodingPlanUsage,
/api/plan GetAFPUsage, pay-as-you-go none, in the host's region. The
branch asked both and showed whichever answered.
- Dropped guessed reply fields; the Coding Plan reply is the one
pi-ark-usage, dsh-ark-quota and ArkCodingPlanUsage read. AFP buckets are
read in the forms dsh-ark-quota and arkcli read (Used of Quota or Total,
or Percent), plus Tier; none was seen from a real account.
- An Ark provider is known by its host, not the preset id, so a copy or a
custom provider takes the key too.
- The Secret is never sent to the page (accessKey {id, secretSet}); a
blank Secret keeps it, a save without the ID keeps both, Remove
(clearAccessKey) drops both, an ID with no Secret is refused.
- The pair goes where keys go: AddCopy, Restore, Mirror, backups and
davsync carry it only with keys (withoutKeys), and a keyless restore or
sync keeps this machine's. CLI: access.key, access.secret.
- A changed access key drops the cached cards (ForgetPlanQuotas).
- UI strings in zh, zh-TW, ja and de.
Nothing was tried against the real Volcengine API: there was no access
key. A local fake checked the real request PlanQuotas makes against
volc-sdk-golang's own GetSignRequest (both actions, two regions); it
lives outside the repo, so go.mod is unchanged.
Run: gofmt -l; go vet (darwin), GOOS=linux and GOOS=windows go vet
-tags nogui on provider, gui, backup, davsync and the root; go test
-tags nogui ./internal/provider ./internal/gui ./internal/backup
./internal/davsync . under a temp HOME; node --test
volc-access-key, zhipu-team, gui-ja, gui-de, i18n-keys in Chromium and
WebKit. Fail without the change: TestVolcSignMatchesSDK (unsorted
headers), TestAddCopy, TestBackupBalanceToken, TestNoKeys,
TestTakeBalanceToken, TestSyncBalanceToken (pair not carried),
TestProviderSaveKeepsVolcengineSecret (blank Secret dropped),
volc-access-key (Remove not posted).
Co-authored-by: YAYOI27 <111715843+yayoinoyume@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
ea1c5d0457 |
gateway: Antigravity's 429 to a Claude Code / Agent SDK system prompt is the prompt turned away, not a quota, everywhere, and the agent gets 400 (#1425)
Antigravity answers the system prompt of Claude Code and the Claude Agent SDK (Claude Desktop's chats) with 429 "Resource has been exhausted" at whatever quota is left (#666). The detail text already said it was not a quota, but when Antigravity was the last one left (a direct model, the reporter's case) the route still fell through to failureOf: the try was counted 额度用尽 on the Routing page and in the account's tally, and the 429 went to the client, so Claude Desktop asked ten times over. What changed: - codeassist.go: antigravityTurnedAway(p, system, said) needs the Antigravity account, a Claude Code / SDK system prompt and the "resource has been exhausted" words. A real quota 429 ("You have exhausted your capacity ... reset after ...", QUOTA_EXHAUSTED) stays a quota and rests the account as before. turnedAwayStatus = 400 (the Anthropic and OpenAI SDKs retry 408, 409, 429 and 5xx, not 400); ledger ErrType "prompt_turned_away". - gateway.go: such a try is failPrompt (was failRefused in a group, and failQuota as the last), no rest, no usage re-asked, no passing retry, no auto reset. The last one left answers the agent 400 with the vendor's words and the hint; in a group the next member is asked, and if it fails only on a quota the agent is told the turned-away 400. The in-reply (stream error event) form is told the same way. - continuation.go / translate: the hint is appended to the in-reply form too, and the ledger row carries the ErrType. - routing.js: the hint joins HINTS, so the Routing page shows Antigravity's words as "It said: ..." and the explanation on its own line, translated in zh, zh-TW, ja and de (i18n.js). Tests (each fails on the old gateway.go/continuation.go/routing.js): - TestAntigravityTurnedAwayLastIsNoQuota (stream and not): 400, asked once, failPrompt, no rest, ledger 400 prompt_turned_away. - TestAntigravityTurnedAwayInsideTheReplyLast, TestAntigravityTurnedAwayThenAQuota, TestAntigravityQuotaOnAClaudeCodeTurnStaysQuota (scope: a real quota 429 still rests and reaches the agent as 429), TestAntigravityTurnedAwayHintIsTheRoutingPages. - gui/tests/routing-ag-turned-away.test.cjs: row status word, account tally, "It said" line and the hint line, in en/zh/zh-TW/ja/de. The real-quota fixture's words are from public reports, not captured from a real account. Run: gofmt -l internal/ (clean); go vet ./internal/gateway/ and GOOS=linux / GOOS=windows go vet -tags nogui ./internal/gateway/; go build ./..., GOOS=linux and GOOS=windows go build -tags nogui ./...; go test -tags nogui ./internal/gateway/ ./internal/gui/... under a scratch HOME with pinned caches (ok); GUI node tests (new test, routing-*, gui-de, gui-ja, gui-zh-tw, i18n-keys) in Chromium and WebKit. |
||
|
|
c3e673b061 |
codex: the account Codex is signed in to goes by the name it is saved under, so two Team seats of one email are weighed apart (#1424, lingqiqi5211)
One email in two Team workspaces is saved as "me@example.com · Team" and "me@example.com · Team · <workspace>" (codexName). codexAccount still named the account Codex is on by codexUser alone, "me@example.com · Team". With Codex signed in to the second seat, its candidate used the second seat's credentials under the first seat's name. Everything keyed on Account.User then read the first seat's records: Smart's allowance (98% used, so the seat with 2% left was ranked last), rests and sticks (restKey/who), the usage cap, per-account proxy and model list, a gateway key's account list (LoginID), OwnPaused, and the Routing page's name. - codexLiveName (logins.go): codexName over the saved logins, shared by liveLogin and codexAccount, so the provider's Account.User is the saved name. - SessionIdentities: a saved Codex identity goes by its saved name, and the one from auth.json by that of the saved one of its workspace and member, so a session of the second seat is told as that seat's. - providers-accounts.md: the Codex naming rule. Stored records: nothing keyed under a name that goes away. The first seat keeps "email · Team"; the second seat's rests, caps, proxy, model list and usage are already under its saved name, written while it wasn't the one in use. Records written under the first seat's name while the second was in use can't be told apart now. Rests run out and readings are read again, so none is migrated. Tests: TestCodexTeamSeatsOfOneEmailWeighedApart (gateway): Team A 98%, Team B (Codex on it) 2% with its week renewing first, another email 20%. On the old code it fails with "sent to ws-other; want the seat Codex is on" (and, with its name check, "goes by \"me@example.com · Team\""). With session_identity.go reverted, TestCodexWorkspaceSessionIdentities (provider) fails with "the seat Codex is on: [me@example.com · Team]". Run: gofmt -l (clean); go vet, GOOS=linux and GOOS=windows go vet -tags nogui on provider, gateway, usage; go build -tags nogui ./... on darwin, linux, windows; go test -tags nogui ./internal/provider ./internal/gateway ./internal/usage under a temp HOME with pinned caches: all ok. Not tried with real accounts: no two Team seats of one email are available here. |
||
|
|
319b72c7a5 |
gateway: the loop guard's long-reply test streams 1000 lines, not all of gateway.go, and waits a minute (69c21cdd's macOS -race CI red)
TestLongReplyThatDoesNotLoopGoesThrough/gemini failed on macOS -race in run 37946485821: "the stream never ended". It streamed all of gateway.go (199KB, ~20k events of 11 bytes) through the gateway on each protocol, and askStream waited 20s. Locally under -race each protocol took ~5s with the guard and ~5s without it (5.0s off, 6.0s on for gemini), so the time is the gateway translating 20k events, and CI's runner is ~4x slower: a bound with no room, not a slow guard. - The test streams gateway.go's first 40KB, cut at a line end (1001 lines, several times the guard's 256-unit window). - askStream waits a minute. - TestLoopGuardLeavesOrdinaryRepliesAlone feeds the first 256KB of each file (app.js and i18n.js run to megabytes): 72s -> 37s under -race. Ran: gofmt -l, go vet (darwin, GOOS=linux, GOOS=windows), go test -race -run 'TestLong|TestLoop' ./internal/gateway (long reply 3.9-4.7s per run for all 4 protocols). |
||
|
|
ab72c8eb85 |
gui: TestCoveredWindowStaysUp hides magpie and checks the window in one step, so the activation its open asked for can't unhide magpie in between (macos-latest, CI run 37942751214)
Cause: on macos-latest the helper exited 1 with "window up: timed out waiting until magpie is hidden" (6.06 s: the whole 5 s wait). It passed on 3cd0b5f7's own run and the ten green runs after it, so it is a flake, not a missing window server: the open and covered steps before it passed on the same machine. Opening the window (openMain -> showHere -> Focus) asks the Mac to activate magpie, which it does through the window server and the run loop, not at once. On a loaded CI machine that activation can still be pending when the test hides magpie a moment later; it lands right after the hide and unhides magpie, which puts the window back, so every poll found it open. NSApp hide: itself is synchronous here (hidden with no window ordered in, in the same main-thread block), active or not, and under full CPU load. That the CI log has no line between is inferred from this: the only thing that puts a hidden app's windows back is unhiding it, and the helper's only activation is the open's. Change: the test's hide step hides magpie and reads whether the window counts as up in one block on the main thread, and hides again when it finds magpie shown again. No production code changes: a user can't hide magpie with Command-H before it is active. Not skipped: the test needs nothing CI lacks. Run: - The old step with a late activation injected (queued from another goroutine while the hide block runs, not committed): FAIL 3 of 3, "window up: timed out waiting until magpie is hidden" in 5.8-6.3 s, the CI failure. The new step with the same injection: ok, 3 of 3. - Guarantee still checked: windowUp with its "open as magpie was hidden" clause removed fails the new test, "hidden with magpie: the window counts as up=false, want true". - go test -count=10 -run TestCoveredWindowStaysUp ./internal/gui: ok. Before the change, load alone didn't reproduce it here: three copies at a time, 15 runs, with a busy loop on every core, all passed. - gofmt -l: nothing. go vet ./internal/gui (darwin), GOOS=linux and GOOS=windows go vet -tags nogui ./internal/gui, GOOS=windows go vet ./internal/gui: clean. go test ./internal/gui (darwin, under a temp HOME with pinned caches): ok. |
||
|
|
570b8fdb2a |
provider: swapping LoginUsage's or Claude /usage's stand-in waits for the readings still running through the old one (data race in CI run 37942751214)
Cause: TestClaudeAccountBackOnceItsWindowRenews (
|
||
|
|
d7f3c20be3 |
grok: a Codex subagent's plain agent_message goes to Grok as the user's message, not refused with 422 "unknown item type" (plugins#61, opencode-grok-auth 0.1.11)
Grok's Responses backend refuses a request carrying Codex MultiAgentV2's
agent_message item (a subagent's task, or its reply kept in the lead's
history) with 422 unknown item type "agent_message". plugins#61 counted
512 such 422s on a moved Grok subscription from 10-03 to 10-09, and fixed
it in @magpie-community/opencode-grok-auth 0.1.11 (on npm).
What changed:
- the grok mover's min goes from 0.1.10 to 0.1.11, so keepMovedCurrent
updates moved users' plugin, and its comment says the fix.
- grokBody (the built-in's request shaper, which also runs for the Grok
plugin's provider, plugins.go) does what the plugin does: an
agent_message whose content is a non-empty list of input_text /
input_image parts goes as {"type":"message","role":"user"}, with
"From <author> to <recipient>\n\n" (or From / To alone, or nothing)
as its first part, in its place among the input. One with
encrypted_content on the item or a part, an empty or null content, or a
part of another kind is left byte for byte, so magpie reads no sealed
task and the gateway's sealed-task guard still sees it. A body with no
agent_message is returned as it came.
Tests:
- provider: TestGrokBodyAgentMessageGoesAsUser (the plugin's task, reply
with an image between a call and its output, sender alone / recipient
alone / none) and TestGrokBodyLeavesSealedAgentMessage (the plugin's
seven sealed or malformed items, and an ordinary history byte for byte).
- gateway: TestGrokGetsAgentMessageAsUser (built-in, streamed and not,
through the handler) and its parity TestGrokPluginGetsAgentMessageAsUser
(the Grok plugin's provider, streamed, relayed).
- With origin/main's grok.go back, TestGrokBodyAgentMessageGoesAsUser,
TestGrokGetsAgentMessageAsUser and TestGrokPluginGetsAgentMessageAsUser
FAIL ("the agent_message went as" the untouched item); with the change
they pass.
Run: gofmt -l (nothing); go vet, GOOS=linux and GOOS=windows go vet
-tags nogui on ./internal/provider ./internal/gateway (clean);
go test -tags nogui ./internal/provider ./internal/gateway under a temp
HOME with pinned caches (ok). Not tried against the real Grok backend:
no live Grok request was made.
|
||
|
|
acdaa28aa1 |
codex: a conversation that searched the web declares cached search to the ChatGPT backend, so its compaction and turns aren't refused "response protection is unavailable" (#1270, #1099)
The ChatGPT backend answers HTTP 200 and only `event: error` "response
protection is unavailable" to any request whose input replays one of its
own web_search_call items while the request doesn't declare the web_search
tool. Probed on a real ChatGPT Pro account (gpt-6.1-sol, 2026-10-09) with
a real searched turn: refused with tools [], with function tools only and
with a compaction_trigger; answered once {"type":"web_search",
"external_web_access":false} is declared. A Responses Lite request refuses
a top-level web_search (400) but takes it in an additional_tools item. So
xjdata's case (Codex compacting a custom provider's conversation itself, a
plain /responses with tools [] that magpie's codexCompact never sees) failed
every time once the session had searched, and congee949's thread
descriptions too.
What changed:
- provider.DeclareSearch / declareSearch: when the input holds a
web_search_call and nothing declares search, add cached-only web_search to
the tools, or for Lite (header, reasoning.context all_turns, or an
additional_tools item present) to the additional_tools item (prepended
when there is none). With no tools offered, tool_choice becomes "none" so
the model can't search; a forced choice is kept. Otherwise the body is
unchanged (bytes untouched when no web_search_call is in it).
- codexBody (every ChatGPT account route: serveAgent, codexCompact, warmup,
magpie's codex/* models) calls it; codexUpstreamOn calls it for Codex's
own sign-in relayed to the backend's /responses.
- Not changed: siwc, API-key passthrough to api.openai.com, the legacy
/responses/compact endpoint (no evidence they are refused so; not probed).
- docs/subsystems/gateway-routing.md names the cause and the fix.
Tests: internal/gateway/search_history_test.go replays the backend's real
items from that probe against a fake that refuses as the backend did.
TestSearchedConversationCompactsOnAnAccount, ...FunctionTools and
...OnCodexSignIn fail on the old code with "event: error"; ...CompactionTrigger
fails with "asked 2 times, not once". TestUnsearchedConversationUnchanged
and provider TestDeclareSearch cover what's left alone.
Run: gofmt -l (clean); go vet, GOOS=linux go vet -tags nogui, GOOS=windows
go vet -tags nogui on internal/provider and internal/gateway; HOME=<temp>
go test -tags nogui ./internal/provider ./internal/gateway (ok). Real
account, sandbox magpie on 127.0.0.1:3491 under a scratch HOME, replaying
the real searched history: old binary -> /v1/responses codex/gpt-6.1-sol
text compaction with tools [] and /backend-api/codex/responses (standard
and Lite compaction_trigger) all "response protection is unavailable";
new binary -> all response.completed, Lite with a compaction item.
Codex CLI 0.161.0 end to end was not done: on a custom provider it
offered no web_search tool, so no search could get into its history.
|