main
99
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
99009b9333 |
provider: a plugin's model pool counts a bare id with its @sizes, so a routing group's cursor/grok-4.7 is weighed on Cursor Models, not a used-up Other Models (Will on Discord)
Will's routing group keeps cursor/grok-4.7. Cursor's plugin (0.2.2/0.2.3) lists Grok 4.7 only at its context sizes, grok-4.7@256k and grok-4.7@500k, and its usage() builds the Cursor Models window's models and Other Models' notModels from those ids. quotaOfPlugin matched a model against those lists by the whole string, so bare grok-4.7 (which the plugin still runs, at its default size) fell into Other Models: used up, so the Routing page showed it 0% left and weighRouted ranked it last, while composer-2.5 showed ~84%. The window's lists are now an idSet: a bare id is in it when one of its id@size is, and an id@size when its bare id is. Only the trailing @size is taken off, so distinct models stay apart: another @size of a model, another family member (grok-4), and other suffixes (ZCode's -Trial) still match by the whole id. Every route that asks a plugin window whether it counts a model goes through w.matches -> Limit.applies (routing's For, Pace, Count, Renewal, fallback's Pooled, CapHeld, QuotaWindow.Counts for the usage page's holds), so the one change reaches them all. host.js's contract comment says so for plugin authors. The plugin's own lists are also fixed in magpie-community/plugins (cursor 0.2.4 adds each listed id's bare name to its pool), for magpie versions before this one. Verified: - TestPluginPoolCountsBareIDOfSizes (the reporter's literal ids) fails on the old code: "grok-4.7: used 100, want 16", "the usage page's holds count grok-4.7 in Other Models", "grok-4.8@1m: used 100, want 16"; passes with the change. - gofmt -l clean; go vet, GOOS=linux and GOOS=windows go vet -tags nogui on internal/provider and internal/plugin; go build for darwin, linux, windows. - go test -tags nogui ./... green but TestNoCommandBypassesProc, red on main since |
||
|
|
2da8fbe488 |
tests: every test that runs plugins on the bun on PATH runs it once first, through testenv.Bun, so a bun that has never run here doesn't time out internal/provider's and internal/plugin's plugin tests (#1306)
|
||
|
|
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. |
||
|
|
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
|
||
|
|
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). |
||
|
|
3691c2047d |
plugin, agent: bun is given a SOCKS proxy through the bridge, so a plugin installs with only a SOCKS system proxy (#1409)
With only a SOCKS system proxy on macOS (127.0.0.1:4401), every plugin
install failed: netproxy.Env put socks5://… in bun add's *_PROXY, and Bun
takes only http:// and https:// proxies, so it stopped with
"UnsupportedProxyProtocol downloading package manifest". The plugin
host's own fetches already went through netproxy.Bridge (hostEnv), but
bunCommand, which runs bun add, update and remove, did not.
- netproxy.EnvForBun rewrites a SOCKS5 HTTPS_PROXY/HTTP_PROXY/ALL_PROXY
(any case) to the loopback HTTP bridge in front of it; others as they are.
- bunCommand uses it, so add, update, remove and the host's run all get it.
- Sibling: an agent CLI installed with bun is updated by `bun add -g`
with the same env (updateEnv in cliupdate.go); npm is left as it is.
- provider-plugins.md says so.
Run:
- TestInstallThroughSOCKSProxy (real bun 1.4.2, fake registry, SOCKS
proxy only): on the old bunCommand it fails with the reporter's exact
"error: UnsupportedProxyProtocol downloading package manifest"; passes
with the fix, through the SOCKS proxy.
- TestEnvForBunBridgesSystemSOCKS (system SOCKS proxy via Env) and
TestUpdateWithBunBridgesSOCKS fail with EnvForBun / the bun branch
broken ("bun is given …socks5://…"), pass with it.
- Real use: a throwaway test with a real SOCKS5 server and only
*_PROXY=socks5://… installed @magpie-community/opencode-cursor-auth
0.2.2 from registry.npmjs.org through it; on the old code the same
failed as UnsupportedProxyProtocol. Scratch HOME, not committed.
- gofmt -l clean; go vet ./...; GOOS=linux go vet -tags nogui ./...;
GOOS=windows go vet ./...; go build; GOOS=linux go build -tags nogui;
GOOS=windows go build; go test -tags nogui ./... (scratch HOME) all ok.
|
||
|
|
4b56c8672d |
plugin: an update Bun holds back says why, instead of saying it was made (sweanng424 on Discord)
What: Update, Update all, the hourly auto-update and the move's install ask npm for the plugin's newest version and have bun install that exact version (installAt), then check it is the one installed. When bun won't, the error says why, reason first for the status pill: "<pkg> 0.1.21 isn't installed yet: Bun's minimumReleaseAge (in a .bunfig.toml) holds back versions published less than 2 days ago, and magpie installs it once it is older (bun's own words)". Another bun error (a registry of the user's own without the version) is "<pkg> <v> isn't installed: <bun's error: lines>". Specs stay as they were (name@latest); pinned and git plugins, and npm not answering, install as their spec says, as before. Why: the Factory card stayed at 0.1.18 with 0.1.21 out, through clicking update and restarts. bun add pkg@latest resolves "latest" by bun's own config: a minimumReleaseAge in the user's ~/.bunfig.toml or $XDG_CONFIG_HOME/.bunfig.toml makes it install the newest version older than that and exit 0. 172800s (2 days) gives exactly 0.1.18 (0.1.19-21 were published 10-08). So every update path succeeded with nothing changed, and the page said "updated to v0.1.21". Asked for 0.1.21 itself, bun fails with "blocked by minimum-release-age: 172800 seconds". The same code serves every moved plugin (Cursor, Devin, Kiro, Qoder, ...), so all of them had it. npm view: 0.1.21 is latest; npmmirror has it too. Run: - TestUpdateBunHoldsBackSaysWhy (new): real bun 1.4.2 against a fake registry with 0.1.18 (3 days old) and 0.1.21 (1 hour), a bunfig with minimumReleaseAge = 172800. On the old code it fails: "Update with 0.1.21 held back by Bun: <nil>, want bun's reason (installed 0.1.18)". Breaking Update alone fails at "Update all: <nil>", breaking the hourly update alone at "the hourly update: <nil>". Without the bunfig it installs 0.1.21 and keeps the spec @latest. - Real use: magpie web (nogui build) under a sandbox HOME, real npm, with the bunfig: add factory-auth@0.1.18, spec @latest, POST /api/plugins/upgrade and /update return the reason above, the card stays 0.1.18; bunfig removed, upgrade installs 0.1.21. - Ruled out on macOS and the Windows box: plain upgrade 0.1.18 -> 0.1.21 works there without a bunfig, host running or not. - gofmt -l; go vet (darwin, GOOS=linux, GOOS=windows, -tags nogui) on internal/plugin, internal/provider, internal/gui; go build (darwin), GOOS=linux and GOOS=windows -tags nogui builds; go test -tags nogui ./... under a temp HOME, all ok. |
||
|
|
a8ca790855 |
plugin: the host takes a proxy set after it started (#1363)
iamyhzhao's moved Grok account reads as signed out after nearly every reboot. The plugin host (Bun, host.js) reads MAGPIE_*_PROXY once, at its start, and nothing restarted it when the proxy changed. After a reboot magpie can start before Clash or another proxy app sets the system proxy, so the host had no proxy for its life; the Grok plugin renews by running `grok models`, which got no proxy from the spawn wrapper, couldn't reach auth.x.ai, and token() said the sign-in had expired. Moving to the built-in and back installs and removes the plugin, each of which restarts the host, which is why that worked. Now the host remembers the proxy it was started with (hostProxy of env()), and get() replaces a live host whose proxy is no longer what a new host would get (proxyMoved, looked at no more than every 15s). Run: new TestPluginHostTakesANewProxy (real host on the cliproxy fixture: no proxy, then HTTPS_PROXY set, the CLI gets it; unchanged proxy keeps the same host) passes, and fails with the check disabled. go test -tags nogui ./internal/plugin ./internal/provider ./internal/gateway in a sandbox HOME: ok. gofmt -l clean; go vet for darwin, linux and windows on ./internal/plugin: clean. Not verified: the reporter's proxy setup (Clash with system proxy is the likely case, not confirmed), a real reboot, a real Grok account. |
||
|
|
baecbb1be0 |
provider, gateway: Cursor moved onto its 0.2.x plugin gets Fast back, in the agent picker (#954) and for a Routing group's member, on the models Cursor has a fast variant of (#1360, loosheng: 插件 0.2.x 后 Cursor 的快速模式不见了,保存 cursor/<model>:fast 报 "has no fast mode magpie can ask for")
Plugin 0.2.x lists Cursor's picker once per context size (claude-opus-5-5@300k, @1m) and no -fast model, so CanFast, which looked for a -fast sibling, said no for every model: FastFor in route.go, canFast in the group editor, SetFastPick and cleanFast all go through it. The plugin itself still ran service_tier priority as the fast variant. opencode-cursor-auth 0.2.2 (magpie-community/plugins fb253f9) now says fast on a model listed at a size Cursor has a fast variant at; host.js passes it on as plugin.Model.Fast, CanFast takes it, and a plugin before 0.2.2 is still read by its -fast sibling. The Cursor mover asks for 0.2.2. On the way a fast request translated for a moved Cursor (Anthropic, Responses, chat not streamed) lost its service_tier: buildChat sets it only for host "cursor", and a moved provider's host is empty; buildHost names it. Tested: the fake plugin lists sized models with fast on one (FAKE_FAST=tier); TestCursorPluginGroupFast (CanFast per size, @1m:fast refused, a group's @300k:fast sends priority), TestCursorPluginGroupFastSibling (the old -fast list), TestCursorPluginFastPick (moved: picker fast on chat, chat streamed, Responses and Anthropic sends priority, codex's doesn't). Without groupfast.go, host.js's fast or buildHost they fail. go test -race -tags nogui on gateway, provider, plugin, agent, gui; go vet, GOOS=linux/windows vet and build. With the built binary under a temp HOME, the real npm 0.2.2 plugin and Cursor's API answered by a preload (a picker fixture, the agent API's Run caught): /api/state offers fastFor on @300k and composer-2.5, not @1m or grok; /api/groups canFast the same; saving @1m:fast is refused, @300k:fast saved; opencode's picked @300k reaches Cursor's Run with fast=true on chat, Anthropic and Responses, codex's with fast=false. origin/main's binary in the same HOME offers none and refuses @300k. Not tried with a real Cursor account. |
||
|
|
7ddea2c2e0 |
plugin tests: TestHostRunsAsMagpieBun puts the Bun on PATH at the old name as a hard link, not a fresh copy, so macOS's first-run check of a new program no longer comes out of the host's one-minute start under a full go test run (#1306)
A copy is a new program, which macOS checks before its first exec; the host's init ran it inside the test's minute. Timed here with load ~130 and syspolicyd not pegged: a fresh copy's first `--version` took 0.9-1.6s, a hard link of the same bun 0.04-0.3s; with syspolicyd busy the same check took 26-55s a program in #1284. Where no link can be made (another filesystem), the copy is run once before the minute starts. Run in Docker (golang:1.26.8, linux/arm64, bun 1.4.2): -count=3 of the test with bun on a bind mount (copy path) and beside HOME (link path), all PASS; go test -tags nogui ./internal/plugin ok; go vet for linux, windows and darwin clean. Not run: the test on macOS itself (Go tests are kept off this Mac while syspolicyd is overloaded), so the red under a slow first-run check is shown by the timings above, not by a failing run. |
||
|
|
55209c5529 |
plugin tests: TestHostStreamCreditBoundsTheQueue waits for the given-up call to leave the host, which it does once its abort's write is known, after the child may already have read it (red on ubuntu CI at 2b5f19fc; a 50ms pause before the write is known fails the old check every time)
Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
cdf325f178 |
plugins: a package that is only a command (an MCP server like magpie-x-search) isn't listed under Unreviewed, isn't added as a plugin, and one added before says what it is instead of a host stack (#1327, congee949)
A repository tagged magpie-plugin whose npm copy (from that repository) names no file to load (no main, no exports) was listed and installed from npm without the entry check the GitHub route has; it then failed every host start with "has no entry" and a stack. Now: - the Unreviewed list checks npm's copy the same way: one that names no file needs the index.js the repository's own route needs (askTagged); - `magpie plugin add` / the Plugins page refuse a package that names no file, has no index file and is neither pi's nor a middleware, take the package just installed for it out again, and say it isn't an OpenCode or magpie plugin, and, for one with a bin, to add an MCP server under Library → MCP servers (notPlugin); - the host says the same, without a stack, for one already on the list. Run: TestCommandOnlyPackageIsNoPlugin (magpie-x-search 1.2.0's real package.json) fails without the change (Add = <nil>; with only host.js reverted, the stack message), TestTaggedRepos with the real GitHub package.json and npm latest of cyberElar/magpie-x-search fails without it (listed), both pass with it; TestNotPluginKeepsPlugins (index.mjs, main, exports, middleware, pi, a file stay plugins). go test -tags nogui ./internal/plugin under a temp HOME ok; go vet -tags nogui for darwin, linux, windows. The built binary in a sandbox HOME: `magpie plugin add magpie-x-search@latest` from real npm prints the new error, and plugins/package.json no longer lists it. Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
8412a5d1a0 |
gateway tests: the keepalive tests count only the idle time a client waits through — the keepalive after the vendor's first words, and nothing after the reply's last event (#1163)
* gateway tests: the quiet mid-reply tests wait for, and count, the keepalive after the vendor's first words, as a held stream's before them is the agent's too (TestQuietStreamMidReplyKeptAlive failed once on main at GOMAXPROCS=2, load ~30) The failing body began ": keepalive" before the role chunk and "hel". That ordering is allowed, not a bug: quietFast lets watch keep the agent alive 40ms into the try, and a vendor slow to answer (status 0, nothing held) or one that flushed response.created and paused before "hel" (only the role chunk, a lead, held) gets the held stream's keepalive first, as TestSilentHeldStreamKeptAlive requires. Content that reaches the holdWriter is scanned and flowed under h.mu, which keepQuiet and keepAlive take, so no keepalive goes out after content the writer has and before it is relayed. A vendor delayed 150ms before its first bytes, or between response.created and "hel", gives the failing body every time, with the keepalive sent at status 0 or with only the role chunk held. The tests released the vendor at the first keepalive and required "hel" before it, so a slow first write made them fail, or test the held path instead of the mid-reply one. readQuiet now releases the vendor at the first keepalive after the given text, and aliveAfter asserts a keepalive between "hel" and "lo": TestQuietStreamMidReplyKeptAlive and TestQuietStreamsKeptAliveEveryProtocol's mid-reply cases. TestQuietKeptAliveOnlySoLong counts only the keepalives after "hel", as the held ones before them have their own 200ms. The every-protocol check matches `"lo"` quoted: bare, it matched the anthropic cases' content_block. With a 150ms vendor delay injected, the old assertions fail all 5 mid-reply cases and the new ones pass. With keepQuiet's passing-branch keepalive removed, all 6 mid-reply tests fail, delay or not. go vet; go test -tags nogui -race -count=20 -run 'TestQuietStream|TestSilentHeld|TestQuietKeptAliveOnlySoLong' ./internal/gateway; the whole package with go test, and as a prebuilt binary at GOMAXPROCS=2 three times (load 18-28), all pass. * gateway tests: TestTranslatedStreamKeepsClientAlive stops counting the client's idle timeout once it has the reply's last event (red on ubuntu CI at ef5fba2c, run 37471004500: responses_to_chat "idle_timeout=true completed=true") — the gateway writes and flushes finish_reason, usage and data: [DONE] back to back in chatEncoder.finish, then serve's bookkeeping (NoteServed, saveSticks, usage.Append) runs before the handler returns and net/http sends the stream's close. On a loaded runner that, or the test's own goroutine being held, passed the 150 ms the test still counted after [DONE]. No client's idle timeout counts that wait: Codex hangs up on response.completed, and the clients that read on to the close give minutes. Chat's marker is now data: [DONE], the stream's last event, instead of finish_reason two events before it. With a 200 ms sleep after enc.finish() on the Chat path, the old test fails 20 of 20 exactly as CI did and the new one passes 20 of 20. The new test still fails 20 of 20 with the sleep before enc.finish() (completed=false), with readSSEAlive's keepalives taken out (#436), and with the Chat keepalive followed by a blank line again ("events with no data"). go test -race -count=20 -run '^TestTranslatedStreamKeepsClientAlive$' ./internal/gateway, go vet and go test -tags nogui ./... pass. * plugin tests: the fake plugin's windows renew at times set when the host loads it, so the gateway's TestPluginFailover passes when an hour begins between two readings of an account's usage (waitingsnow) The fake plugin told each account's five hours to renew an hour after its usage was read, and its week a day after. TestPluginFailover weighs the accounts by what the plugin tells (provider.Allowances), and Smart routing puts first the account whose week renews sooner, to the hour; accounts alike keep their order. A request that fails on an account's quota, or in its stream, drops that account's reading (staleAllowance), and the next request reads it again. A 401, as in the gone round, is failOther and drops nothing (rest, gateway/routing.go). When an hour began between a@fake's read after the quota round and its read after the stream round, a@fake's week renewed in the hour after b@fake's. b@fake then went first in the gone round, failed with a 401 and kept its first reading, so the request with both out was tried as b, a, full, which fails the test at plugin_failover_test.go:210. An hour that began after the first readings but before a@fake's read after the quota round put b@fake first in the stream round instead, which failed b@fake and read it again too, and the test passed. The test fails every time an hour begins between those two reads of a@fake, and passes otherwise: the 8 such runs below failed and the other 13 passed. The reads are 12 to 76ms apart in the runs below without the hold, and a reviewer's -race build measured 33 to 106ms in 6 runs, so a run at a random time fails about once in 34,000 to 300,000. CI's Linux job installs bun 1.4.2 and runs the test in its Test and Race steps. Found by a review of the tests that compare renewals to the hour; no CI run was checked for it. The windows now renew an hour and a day after born, when the host loaded the plugin, however often an account is read, as a vendor's windows renew at times of their own. Run once in a process, as CI's Test and Race steps run it, TestPluginFailover reads its accounts from the one host started after its plugin.Add, so they renew together. Run again in one process (-count), a run can still weigh an account by the previous run's host, born perhaps in the hour before, until that account fails and is read again: provider's allowances and usage readings outlive fresh(t) and plugin.Settle. That account then goes first, and the test's rounds fail it and read it again before the checks that depend on order, so it still passes; clearing those caches is left for its own commit. TestPluginUsage now reads a@fake's card a second time from its refresh button, 2ms later, and wants each window to renew when it did the first time, as a vendor's do. That is the check that fails without this change. Its other bounds (the five hours in 50 minutes or more, the week in 23 to 25 hours, full@fake's five hours used up) hold while the host is under 10 minutes old; no other test reads these times. Verified under a temporary HOME with Go's caches pinned, every test run with -tags nogui and bun 1.4.2. With 01aaa8e9's index.js put back, TestPluginUsage fails every run: plugin_usage_test.go:109: a@fake's 5 hours renews at 2026-10-07 12:16:35.567 +0000 UTC read again, at 2026-10-07 12:16:35.539 +0000 UTC first and with this one it passes, also with -count=5. With 01aaa8e9's index.js, TestPluginFailover passes unless an hour begins between those two reads. failgap.sh, a scratch script, builds the gateway's test binary once and runs TestPluginFailover with the old plugin and with this one. It makes the plugin's usage hook tell its windows from Date.now() plus a shift, set at the first reading so that an hour begins a set time later (Go's clock is left alone), and logs each reading. Its copy of the test can hold 2s before the stream round, which puts its lines 5 further down. With the hour 30ms after the first reading, the old plugin failed 3 runs of 3, its two reads of a@fake either side of the hour (at 23 and 42ms, 20 and 34, 18 and 37): plugin_failover_test.go:215: fake-claude with the others out: 200 {...} (tried [b a full]) At 45ms it failed 1 of 3 (44 and 69ms). At 60, 80 and 120ms it passed 9 of 9, a@fake read twice before the hour, or once after it and b@fake read again. Held 2s before the stream round, with the hour 50 or 1100ms after the first reading, it failed 4 of 4 (a@fake read at 24 to 34ms and 2044 to 2121ms). This plugin passed all 19 of those runs. That shows only that the test passes with it: its windows are fixed from born when the host loads it, before failgap's first reading moves the clock, so the moved hour never reaches them. Held 2s with the hour 30 minutes away, both plugins passed 2 of 2. born.sh, run as a process per run, logged the host and the born behind each of the plugin's reads in 20 more runs of this one: 9 a run, each run's from one host and one born. It logs the plugin's reads, not what routing weighed. gofmt -l internal/provider/plugin_usage_test.go prints nothing. go vet ./..., go build ./..., GOOS=linux go build -tags nogui ./..., GOOS=windows go build ./... and GOOS=windows go vet -tags nogui ./internal/provider/ pass. go test -tags nogui ./... passes, 43 packages ok, with bun 1.4.2 on PATH, and so do TestPluginFailover and TestPluginUsage* with -race -count=20. * gateway tests: TestTranslatedStreamKeepsClientAlive fails when the stream doesn't close within 5s of its last event, rather than waiting on it with no limit After the reply's last event the read had no time limit, so a gateway that never closed the stream would hang the test until the package timeout (yetone on #1163). The idle timeout still stops at the last event; a 5s limit on the close follows it. |
||
|
|
6d830a3c95 |
plugin: Bun on Windows is given an ASCII way to the system's roots, so a user folder with a Chinese name no longer fails bun add with UNABLE_TO_VERIFY_LEAF_SIGNATURE (#1232)
Bun 1.4.2 on Windows reads NODE_EXTRA_CA_CERTS as ANSI and takes it as UTF-8: C:\Users\<中文>\.cache\magpie\bun\system-ca-*.pem became "锟斤拷" and "load failed: No such file or directory". An 8.3 short name doesn't help (Windows gives 测试 none). Bun opens a relative path from where it runs, which is under the same user folder, so the path is given relative to Bun's dir when it isn't ASCII. Tried on the Windows box with Bun 1.4.2 and a local HTTPS server signed by a test root, from a cwd under 测试: absolute path -> warn + FAIL UNABLE_TO_VERIFY_LEAF_SIGNATURE; relative path -> 200. The new test's Windows cases ran there (windows/amd64 test binary): PASS. |
||
|
|
470d1e71a5 |
provider: a plugin account's usage reading begun before a request was answered no longer overrules the answer, so a 401's lapse mark isn't taken off by a clean reading already out (TestPluginFailover, about 1 run in 90) (waitingsnow) (#1176)
TestPluginFailover failed about 1 run in 90 at "a refused isn't marked". With notePluginLapse logged, a failing run read: a background reading of a's allowance (loginReading, after the "stream" round's StaleAllowance) began at 31.985; the upstream answered a's request 401 at 32.001 and notePluginSaid marked a lapsed at 32.003; the reading ended clean at 32.011 and notePluginLapse(200) took the mark off; the test read the logins at 32.042. Users meet the same: a reading of the Usage page or of routing's allowances that is out when a request is refused clears the mark the refusal left, and routing goes on trying the refused account; one that ends refused marks an account a request has just gone through on. What plugin accounts' sign-ins are told of is now counted under loginsMu (pluginSignInTold, pluginSignInSaid). A request's answer, a sign-in and the plugin's own signIn events are told now and apply as before. pluginLoginQuota takes the count before plugin.AccountUsage, and what its reading found applies only when nothing was told of that account since (notePluginLapseSince), as Allowances leaves out an account renewed while it read. A count rather than a time: Windows' clock moves in ticks of up to 15.6ms, and a reading begun just after an answer would read as begun with it. TestPluginUsageReadOlderThanAnswer holds the fake plugin's usage on an httptest server ($FAKE_USAGE, whose 401 the fake now reads as an expired sign-in) while a request's answer is noted, both ways, and checks that readings begun afterwards still mark and clear. Each direction fails without the fix. TestPluginFailover now reads the allowances the "stream" round left stale before the "gone" round: the fake's usage doesn't ask the upstream that refuses, so a reading that asks after the 401 finds the sign-in good and rightly takes the mark off. A review reproduced that with a delay injected before the count is taken. Left as it is: a reading whose own renewal of the token tells "renewed" before it asks skips the "expired" it then finds; the next 401 marks the account. Not tried with a real plugin account. Under a temp HOME with Go's caches pinned and bun 1.4.2: gofmt, go vet -tags nogui ./..., builds for darwin, GOOS=linux -tags nogui and GOOS=windows (and windows vet of provider and gateway) pass; go test -tags nogui ./... passes; the changed provider tests and TestPluginSaysSignIn/TestPluginFailover pass with -race -count=20; the report's `go test -tags nogui -race -cpu 1,2,4 -count=30 -run '^TestPluginFailover$' ./internal/gateway/` passed 3 rounds (270 runs), where it failed 3 of about 54 runs before the fix on the same loaded machine. |
||
|
|
e24b332051 | plugin: a failed write to the plugin host ends the host before its caller hears of it, so a call freed by the failure finds the host already gone (TestHostWriterFailureWakesWaiters, red on macOS CI) (#1214) | ||
|
|
a12f6fc13a |
plugins: a repository tagged magpie-plugin shows within ten minutes, not up to six hours later (lee04052822 on X: tagged it, and magpie didn't show it)
GitHub's search was asked at most every six hours. An author who had opened the Plugins page before tagging their repository, as lee04052822 had, didn't see it until the six hours were up. It is now asked at most every ten minutes. A search is one request of the ten a minute that GitHub allows without a token. What each repository's package.json, npm and entry file said is kept while its last push stays the same, so asking again costs one request, not three per repository. An answer GitHub or npm didn't give (a 503 for the entry, say) isn't kept, and is asked again next time. Against real GitHub: the topic has two repositories, mu-zi-lee/magpie-doubao-translate and SadWood/magpie-gaccode. Both are listed as github: installs, the first time in 3.8s and in 0.2s once the ten minutes are up. Both installed and loaded in a sandbox magpie, giving the Doubao Translation and GACCode providers. TestTaggedRepos checks that asking again after ten minutes reads only flaky-entry again, and it fails with 21 reads when the per-repository cache is turned off. go test -race -count=2 ./internal/plugin and go vet pass. |
||
|
|
01aaa8e925 | plugins: the built-in market lists OpenCode Zen Free (@magpie-community/opencode-zen-free-auth), OpenCode Zen's free models, with the bundled opencode icon | ||
|
|
f6f960e42c |
plugins: a GitHub repository tagged magpie-plugin shows on the Plugins page under Discover, as an unofficial plugin nobody at magpie has reviewed
GitHub's search for topic:magpie-plugin (not archived, not a fork, not magpie-community's own, which the market lists) is asked at most every six hours and kept on disk, so a restart or GitHub being down shows what it said last. Each repository is read from its package.json on the default branch. When npm has that package published from the same repository, it is installed from npm, because a repository often builds its dist only to publish and Bun runs no build for a git install. Otherwise it is installed as github:owner/repo, and only if the file it loads (exports, main or magpie.middleware) is in the repository. Its page warns that it is unofficial, says whether npm or the repository is what gets installed, and shows the repository's README until it is installed. MAGPIE_PLUGIN_MARKET=off asks GitHub nothing. Checked against real GitHub with the topic set to opencode-plugin in a sandbox: 43 repositories came back, and 32 were kept (30 from npm, 2 from git). github:ndom91/open-plan-annotator, one of the git ones, really installed and loaded. alfaoz/opencode-see-image, whose dist isn't in its repository, failed to load before the entry check and is now left out. The test fixtures are GitHub's and npm's real answers. Ran: go test -race -count=3 ./internal/plugin, go test ./internal/gui, go vet, and plugin-github.test.cjs (Chromium and WebKit, 980/440, en/zh/ja/de, 18/18). The full GUI suite passed 3343 of 3348. The 5 failures were WebKit number-units, purpose-filter and routing-failed-open, which pass 3/3 on their own and don't touch the Plugins page. |
||
|
|
d9b2cdc593 |
plugin tests: TestHostJSCancellation's ready-wait cancels 1,000 bootstrap waits rather than 30,000, so it finishes inside its 10 s under a full suite's load
ready-wait failed twice on 10-07 in full `go test -tags nogui ./...` runs on
main ("signal: killed" at 10.00s) and passed alone. The cause is the test's
size, not the host's cancellation. The case runs 30,000 fetch+abort rounds,
each waiting at least one setImmediate turn. That takes ~4s alone, 4.4-8.5s
beside a full go test ./... plus CPU and fresh-script load loops, and more
than 10s when starved (taskpolicy -c background with every core busy: killed
3 of 3). The cost per round stays flat from 5,000 to 120,000 rounds (no growth,
so no leak), and the counts at the end are exact: retainedWaiters 0, one
reaction on the shared ready promise, one drain listener.
Those counts are what catches a leak, and they catch it at any size. With
1,000 rounds, leaving the waiter in readyWaiters fails with retainedWaiters
1000, and a reaction per request on the shared ready promise fails with
retainedReadyReactions 1000.
Before: starved, 3 of 3 runs killed at 10.04-10.26s; -race -count=20 79.9s.
After: starved, 5 of 5 pass (ready-wait 0.6-5.6s, mostly node startup);
-race -count=20 9.9s.
No production code changed.
|
||
|
|
06420a7121 |
tests: a fake program is a file macOS has already let run (testenv.Program), so a fake's first start no longer eats a test's wait under load
macOS checks a newly written executable before its first exec: 0.27s alone, 2-5s while other new programs start (a go test ./... starting its test binaries). The same file again takes 0.009s, and the pass is kept across a rename and an in-place rewrite. |
||
|
|
05683637d5 |
plugins: the Bun the plugin host runs on is kept as magpie-bun, so a proxy app's PROCESS-NAME rule can route the requests plugins (Cursor and the other moved subscriptions) make (#1048)
magpie downloads its own Bun into ~/.cache/magpie/bun/<version>/. It is now written there as magpie-bun (magpie-bun.exe on Windows), and a Bun downloaded before is renamed in place the first time it is looked for, so nothing is downloaded again and no second copy takes disk. The host, bun add and bun update all run it, so all of their traffic carries the one name. A $MAGPIE_BUN the user set is run as it is. Verified: TestBunDownloadedAsMagpieBun, TestBunDownloadedBeforeTakesTheName and TestHostRunsAsMagpieBun fail without the change (the Bun is .../bun, the host runs .../bun). TestHostRunsAsMagpieBun starts the real host on a copy of Bun 1.4.2 and `ps -o comm=` names it magpie-bun on macOS. On the Windows box the three tests pass with the Windows build of Bun 1.4.2, a running bun.exe renames to magpie-bun.exe, and Get-Process/Win32_Process name it magpie-bun. Linux not run here (CI's ubuntu job runs the test with Bun on PATH). go test -tags nogui ./... green; -race -count=10 on the new tests green. |
||
|
|
14af0f100b |
provider, plugins: a plugin's browser sign-in finishes from the address the browser ended on, for magpie on a server or in Docker (Chicring)
The built-in sign-ins that come back to a port on this machine (Codex,
ChatGPT API, Devin, Kiro, Zed, Gemini, Antigravity) already took the
pasted address. Their plugins didn't: a subscription moved onto its plugin
(Devin, Kiro, Zed) or a plugin's own browser sign-in (Trae) showed only
"the account shows up here as soon as you're done", and a browser on
another computer than magpie's ended on a page that won't load with no way
to finish.
Now a plugin's "auto" sign-in whose page names a port on this machine to
come back to (redirect_uri, auth_callback_url, callback on localhost, or
Zed's native_app_port: loopbackPorts) sets pasteCallback, so the GUI shows
the same Callback URL field as the built-ins, and `magpie plugin login` /
`magpie accounts add` ask for it too. SubmitSignInCallback opens the pasted
address on 127.0.0.1 at that port, never through a proxy, so the plugin's
own listener finishes the sign-in as it would for the browser; an address
on another port or host is refused, a page that sends the browser on to a
page that comes back to the same port (Kiro's AWS sign-in) becomes the next
page to open. Command Code's page posts its key from script, which leaves
no address to paste, so it keeps offering the plugin's API key way.
Verified: TestPluginSignInTakesAPastedCallback (fake plugin with a loopback
sign-in, $FAKE_LOOPBACK) fails without the wiring in pluginBegin ("a
sign-in that comes back to a port here takes no pasted address:
{... PasteCallback:false ...}") and passes with it, -race -count=10;
TestLoopbackPorts covers the community plugins' page addresses.
go test -race ./internal/provider ./internal/plugin and the root package's
Plugin|Login|SignIn tests pass; vet, gofmt, darwin/linux nogui/windows
builds pass. With the real community Devin, Zed and Kiro plugins in a
sandbox HOME, each sign-in offered the field and a pasted address reached
the plugin's listener and the vendor (bogus code refused by Devin 401, Zed
"couldn't be decrypted", Kiro 400). signin-paste-plugin.test.cjs (new),
signin-callback and signin-paste-codex pass in Chromium and WebKit, en and
zh, at 360px; no new strings. Not tried: a real successful sign-in from a
browser on another computer.
|
||
|
|
51d2cf88ea | plugin host: a plugin's node:http2 connection goes through magpie's proxy like its fetch does (#1035, cataglyphis: Cursor opus-5-5 high ends after ~148s with "Cursor: 502 Bad Gateway" cursor_error) — the Cursor plugin runs agent.v1.AgentService/Run over node:http2, which Bun connects directly and ignores the proxy that host.js gives every fetch; the built-in sent the same request through s.client, so a user who needs magpie's proxy (setting, env or the system's) to reach Cursor had their sign-in, models and usage proxied but every Run went out directly. host.js now wraps http2.connect: for a URL proxyFor gives a proxy to (provider/account via, global, SOCKS bridged), it opens an HTTP CONNECT tunnel (Proxy-Authorization from the URL) and runs TLS with ALPN h2 over it; "direct", a caller's own createConnection and unproxied URLs keep Bun's connect; a tunnel that fails ends the session with "proxyconnect tcp: …". TestPluginHTTP2TakesTheProxy (an HTTP/2 vendor at an unresolvable host reached only through a CONNECT proxy; direct and a dead proxy fail with the reason) fails without the change ("h2: getaddrinfo ENOTFOUND h2vendor.invalid"; it carried 0) and passes -race -count=10. With the owner's Cursor account in a sandbox HOME and a logging proxy set, Claude Code's captured request on cursor/claude-opus-5-5 high: before, the proxy saw api2.cursor.sh only; after, it also sees agentn.global.api5.cursor.sh and the reply streams. The reporter's 502 wasn't reproduced here (direct runs answer in 4–20s). go test -race ./internal/plugin, the gateway/provider Plugin|Proxy|Cursor tests, gofmt and vet pass. | ||
|
|
87487c5ef0 | plugin, provider: a plugin can press its vendor's daily check-in itself (auth.checkin), and magpie runs it as its own check-ins (Lemon on Discord #feedback: provider 每日签到的方法能放到插件里面去做吗; ARNO earlier asked for Qoder's) — the check-ins magpie had (WorkBuddy, Trae CN, MiniMax Code, Qoder) are each written in magpie against the vendor's endpoint, even for a subscription that moved to its plugin, so a plugin author had no way to give their provider one. A plugin's auth hook may now have checkin(getAuth, provider) beside usage: the host lists the provider with checkin: true (plugin.Provider.Checkin) and answers checkin({provider, account}) (plugin.AccountCheckin), refreshing the account as usage does, with {outcome, credit, streak, message}; outcome is claimed, done, ineligible, inactive, captcha or failed, and a throw or an outcome outside these is failed with the reason. internal/provider/plugin_checkin.go runs these through checkin.go's checkiner, as the built-ins are: each signed-in account in use once a Beijing day (first look 2 minutes after start, then every 30), a failure asked again after wbCheckinRetry, every other outcome settled for the day, results kept in plugin-checkin.json by provider|account; a failure is recorded as failed and resets nothing. captcha is new (CheckinCaptcha): the vendor wants a captcha, which magpie never solves, so it is said as "check in in its own app", not as a failure, and not asked again that day. The results mark the account's Usage card (checkinBy "plugin:<provider>", WithCheckins), so the card's check-in row, its Auto check-in and Check in now work for it (POST /api/settings/plugin-checkin {provider, on}, POST /api/usage/plugin-checkin {provider}); Settings → Usage gains a Plugins tab, shown while such a plugin is signed in, a Daily check-in row per provider with Off/On and how each account went; magpie accounts checkin and c on the TUI's Usage page include them (the TUI's "no account" line names check-in plugins). The switch (settings.PluginCheckins, by provider) is off by default; for WorkBuddy, Trae CN, MiniMax Code and Qoder it follows that vendor's existing switch until set on its own, and magpie's own check-in through the plugin's fetch leaves a provider whose plugin checks in to it (pluginChecksIn), so no account is checked in twice. The Settings page's save carries PluginCheckins over (TestSettingsSaveKeepsWhatItDoesNotSend's fixture names it). No plugin declares the hook yet; Qoder's check-in stays magpie's own here. TestPluginCheckinRunsAsTheBuiltinsDo (the fake plugin with FAKE_CHECKIN, five accounts answering claimed, done, captcha, a throw and an unknown outcome: off presses nothing; on, each outcome kept with its vendor; a minute on nothing pressed again; past the retry only the two failures; the next day all five; PluginCheckins and the cards marked; another provider's press asks none) and TestPluginCheckinTakesOverTheVendors (a qoder plugin without the hook leaves magpie's Qoder check-in its account, with it none; PluginCheckinOn's fallbacks) pass -race -count=10, and the latter fails with pluginChecksIn stubbed false; TestTUIChecksPluginsIn passes -race -count=20. plugin-checkin.test.cjs (the card's row with the captcha said, Auto check-in posting {provider, on}, no page move; the Settings Plugins tab at 440px with the row fitting, no sideways scroll, On posting) passes on Chromium and WebKit in en, zh, ja and de; usage-checkin, i18n-keys, gui-ja, gui-de and settings-* pass. go test -tags nogui ./internal/..., go vet, gofmt -l, linux (nogui) and windows builds pass. Not tried with a real plugin or account: no published plugin has the hook yet. docs/subsystems/provider-plugins.md (the hook's contract) and docs/reference.md say it. | ||
|
|
2bd49cc545 |
plugin: magpie plugin update in a terminal restarts the app's plugin host (#952, mxzs123) — the running magpie noticed another magpie's plugin change only by plugins.json, which an update leaves as it was (the spec stays, bun installs a newer version), so the app's host went on answering with the old code (0.1.17 after the update to 0.1.18, still 403) until it was killed. The stamp checkList compares now also covers what is installed: Dir()'s package.json and bun's lockfile; and Restart takes the stamp as it is, so this magpie's own install isn't taken for another's and restarted twice. In a sandbox (serve on 3476 under its own HOME, the Factory plugin at 0.1.17, unpinned spec; a request, then magpie plugin update in another process, then a request): on 2d85a086 the host process stayed the same (58657) with 0.1.18 installed; with this change it was replaced (60004 → 60583) and the old one stopped. TestTerminalUpdateRestartsTheHost is new and fails without the change; go vet and go test -race -tags nogui ./internal/plugin/ ./internal/provider/ under a temp HOME pass.
|
||
|
|
3ab27186c9 | cleanup: drop eight functions nothing calls — catalog.ProviderEnv/ProviderAvailable, subscriptionRun.attached, hasTool, kiroSignedIn, SetModelSame (SetModelPrefs calls setModelSame itself; the comments name it), sessions.claudeFiles and plugin.SetConfig. Found with deadcode -test -tags nogui and checked by git grep across .go and .js; the ones only the GUI build or one platform calls stay. vet on darwin, linux, windows; catalog, gateway, provider, sessions and plugin tests pass. | ||
|
|
bc00ee1df2 |
plugin, provider: a program a plugin starts has magpie's proxy, and a CLI login with no link says why (𝕏 on Discord)
The Grok plugin's sign-in said only "grok login gave no link to open". The plugin host has magpie's proxy only as MAGPIE_*_PROXY, so the `grok login` the plugin runs went out with none, where the built-in's had it (netproxy.Env); where x.ai is reached only through a proxy, it failed. host.js's spawn wrapper now gives a started program HTTPS_PROXY and HTTP_PROXY (the account's own proxy, none for direct, else magpie's) when the plugin set none. grok 1.0.46, unable to reach x.ai, prints "Error: error sending request for url (https://auth.x.ai/oauth2/device/code): …", whose URL the built-in's runCLISignIn took for the page to open; a line saying error gives no link, and a login with none ends with the CLI's last line. The plugin does the same from 0.1.8, which the grok mover now asks for. plugin_cliproxy_test.go and TestCLISignInSaysWhyNoLink fail before this. Checked with grok 1.0.46 itself through the plugin host and the published plugin's code, given a proxy that refuses: the CLI goes through it and the error names what it said. |
||
|
|
2f135ed4d5 | plugin, gui, docs: Plugins says which plugins are providers and which are middleware, lists the community's ready-made middleware, and edits a middleware's options in its row (the owner) — the Plugins page still read as if every plugin were a subscription, and nothing showed what a middleware does or let you set its options without editing plugins.json. Every Discover card and Installed row now carries a Provider or Middleware chip (both for a plugin that is both), also for one that is off (isMiddleware/middlewareOnly from the server, read from its files); Discover splits into Subscriptions and Gateway middleware ("runs in magpie's gateway on what every agent sends and gets back, whichever provider serves it"), the latter from market entries with "kind": "middleware" and no providers or icon (the built-in market.json lists the five new @magpie-community/middleware-* packages: param-override, model-map, system-prompt, word-guard, think-tags, mostly New API's channel settings with the same JSON). A middleware row has Options: a JSON editor in the row, starting from the package's magpie.options example when none are set, which validates it is an object, saves (POST /api/plugins/options) for the next request, clears, and cancels with Esc. magpie plugin options takes a short name (model-map for @magpie-community/middleware-model-map; plugin.ShortName, also in SetOptions). en/zh/ja/de. Docs: usemagpie.ai/docs/plugins (en/zh/ja) gains Ready-made middleware and Options and the registry's kind; reference.md, both READMEs and docs/subsystems/gateway-middleware.md likewise. Tested with a real DeepSeek account and real Claude Code through magpie web --gateway in a sandbox HOME, with all five installed and options set by short name: a mapped model named back, max_tokens capped, a prompt appended for claude only, word-guard's 400 and param-override's 403 in the agent's shape, <think> stripped from Chat and Responses streams and a whole Anthropic reply, reasoning_content moved into the text; /api/plugins showed the kinds and the example. TestSetOptionsShortName is new; TestBuiltinMarket checks a middleware entry has no providers or icon and no other kind. plugin-middleware.test.cjs (Chromium and WebKit, en and zh: the chips, Discover's two sections, the options editor with its errors, no select, no scroll, no left border) fails without the change; the other plugin tests and click-scroll pass in WebKit (plugin-market's section name updated; plugin-move fails only under parallel load and passes alone). go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass. | ||
|
|
9e13f18fb9 | gateway, provider: a request turning reasoning off stops Factory's DeepSeek V4.1 Flash, Kimi K3 and GLM-5.2 thinking (#899, tedestar) — they offered only low/high/max (GLM-5.2 high/max), so an agent's none (or Claude Code's thinking disabled) was asked for low, or nothing, and Fireworks thought anyway. droid 0.231.0 lists off for them and sends reasoning_effort "none"; through OpenRouter pinned to Fireworks, none came back with 0 reasoning tokens on DeepSeek V4.1 Flash (2 completion tokens; 19 at the default, 16 reasoning at low) and Kimi K3 (8; 71 at the default), and on Baseten's GLM-5.2 (2; 114 at the default), while GLM-5.3 and GLM-5.3-Flash turn none away ("Reasoning is mandatory for this endpoint and cannot be disabled") and stay at low, their least (Qwen3.8 Max and MiniMax have no off in droid either). The three now have none among their levels, in the built-in and in @magpie-community/opencode-factory-auth 0.1.17, which the mover's min is raised to; and a translated request that turned reasoning off (Effort reads it as low) is asked for none on a model with none among its levels, whatever the provider, else as before (a refusal still falls back to the lowest level on). No Factory account was at hand to send through Factory itself. TestFactoryThinkingOff (the built-in: chat, Responses and Anthropic's disabled; GLM-5.3 still low) and TestPluginThinkingOff (the plugin host, a model whose variants have none and one without) are new and fail without the change. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass (TestSavedButSignedOutClaudeServed timed out once under load and passes alone). | ||
|
|
eebd0b4222 | plugin, gateway, gui: a plugin can be gateway middleware — onRequest, onEvent and onResponse run in magpie's gateway on what agents send and get back (moejs) — a plugin could only be a subscription provider, run in the Bun host, a process away from the gateway. A plugin may now declare middleware (package.json magpie.middleware, or a *.middleware.js / .mjs file), run in-process by moejs (pure-Go JS engine): compiled once, one runtime per request from a pool, so every hook of a request shares ctx.state. onRequest(body, ctx) sees the agent's body in its own API before routing; what it returns is the body the next middleware and the gateway get, undefined leaves the bytes as they were, ctx.reject(status, msg) answers with an error in the agent's API's shape. onEvent(ev, ctx) sees each SSE event's JSON (export const events = [...] filters by name; an object changes it, null drops it, undefined passes the bytes through untouched; data: [DONE] is no JSON and passes by). onResponse(body, ctx) sees a whole JSON reply, with ctx.status. ctx has protocol, model, stream, path, agent, the plugin's options and state. A hook that throws, returns a non-object or runs past its limit (250ms request/response, 50ms an event, 1s load; interrupted) leaves the request or reply as it was and is counted and logged (60 lines a minute); a compressed reply passes as-is. Files are reloaded within a second of a change. Only agents' requests run through it (serveAgent at the gateway's agent entry points and the Codex backend), never magpie's own titles, classifier or stand-ins; it sees bodies before redaction. A middleware-only package is skipped by the Bun host. The Plugins page marks a middleware with a chip, its hooks, calls and µs per call (JSON in and out counted), failures in red with the last error, and a load error. en/zh/ja/de; docs/reference.md and usemagpie.ai/docs/plugins#middleware (en/zh/ja) with an alias example. Tested with a real DeepSeek account through magpie web --gateway in a sandbox HOME: an alias middleware served "fast" and named it in message_start, a reject came back as 403 permission_error, a Chat stream uppercased; real Claude Code (sandbox CLAUDE_CONFIG_DIR) did Read, Bash and Glob tool turns through three middlewares with 0 failures: ≈1–1.6µs an event, 0.2–0.7ms an onRequest that parses and rewrites Claude Code's ≈80KB body. Real use found two bugs fixed here: [DONE] parsed as JSON (SyntaxError on every Chat stream) and stats leaving out the JSON time. internal/middleware tests (chain, undefined keeps bytes, reject, fail-open on throw/spin/string with the runtime reused, async hook, load errors, off and Bun skip, reload, split/CRLF/filtered/dropped stream events, [DONE] untouched, whole reply and gzip, 64 in parallel; BenchmarkEvent ≈0.9µs, 11 allocs) and TestMiddlewareThroughGateway (alias routed, events by protocol, redaction still upstream, reject in Anthropic's shape; fails without the change) are new; plugin-middleware.test.cjs (Chromium and WebKit, en and zh: chip, hooks and stats, failures with last error, broken and off, no left border) fails without it. The other plugin GUI tests and click-scroll pass in WebKit. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass (internal/provider failed once under load and passes alone). | ||
|
|
4788cf2bbd | plugin, gui: a plugin's account whose model list failed says why, rather than listing Auto alone (gnayiab on X) — with magpie in WSL, Cursor's plugin account offered only Auto: the plugin's models hook couldn't get the account's list and magpie, with no list kept from before, showed the plugin's defaults with nothing saying so, its editor even calling them "vendor list", and Refresh answered "1 models". The host now keeps why a models hook fell back (what it threw, else how its last fetch failed) for the provider and each account; plugin.Cached carries it with the accounts it reads again from the sign-ins; provider.ListError gives it for a plugin's account. The editor says "<provider> couldn't list its models: <why>" under the models (amber, no left border) and no longer calls the defaults the vendor's list, and Refresh fails with it. A list kept from before, or one that comes in, says nothing. en/zh/ja/de. Tried with the real Cursor plugin (0.1.9) in a sandbox: a key Cursor refuses shows "Cursor couldn't list its models: Cursor didn't take this API key" and Refresh says it; the owner's real account lists 72 models with nothing said, and Claude Code -p through the gateway ran a Bash tool on cursor/auto, composer-2.5 and claude-opus-5-thinking. The plugin itself, in WSL (magpie-desktop), listed 254 models and streamed chats with tools. TestFallenBackListSaysWhy (hook returns its defaults, throws, or hands back its own) fails without the change; plugin-list-error.test.cjs (Chromium and WebKit, en and zh) too; TestFallenBackListKeepsTheLastOne, account-models, plugin-signin and click-scroll pass. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass. | ||
|
|
f55a13f23e |
gateway: a translated reply cut off mid-stream goes on where the upstream has a real prefill (#853)
* gateway: a Chat upstream gets a mended tool exchange — Kimi turns a broken one away 400 ("tool_call_id is not found"): a turn's tool results all go ahead of its own text, in the calls' order; a result whose call is gone gets a synthetic call (unknown_tool) so it survives; an unanswered call gets a synthetic error result; a thinking-only assistant turn, which an aborted stream leaves behind, is dropped
* gateway: a translated reply whose stream the upstream cuts off mid-way goes on instead of failing — the same conversation is asked again with what the client already has of the reply sent back as its last message, for the model to go on from (vk-relay dropping Kimi K3 and DeepSeek streams mid-reply: "API Error: Server error mid-response"). The reply so far goes back with its reasoning for models that read reasoning_content back (Kimi, DeepSeek), a full echo of it is dropped, and the continuation's fresh thinking isn't shown again; the client waits with keepalives between tries, up to two. A reply a tool call of has begun can't be prefilled, and a refusal isn't asked again: both end with the error in the stream, as they used to. A stream's own error event mid-reply ends its read at once rather than waiting on a vendor that keeps the connection open. TestStreamCutMidReplyContinues (reply goes on, one message), TestStreamCutContinuationEchoDropped, TestStreamCutAfterToolCallEndsAsBefore, TestStreamCutContinuesOnlySoLong, TestStreamCutMidThinkingContinues(WithoutReasoning), TestStreamErrorMidReplyContinuesAtOnce and TestContinuationUnecho cover it; TestStreamErrorKeptOpenFailsOverAtOnce and the severed-stream tests pass unchanged. go vet ./internal/gateway, go build (darwin, GOOS=linux, GOOS=windows), go test of gateway, provider, catalog and imagemcp pass.
* gateway: a Chat upstream's mended tool exchange — an orphaned or late result reaches the model as a user message, the Responses path's policy, so no call is made up and no id is used twice; an empty assistant is dropped only inside a pending exchange, where it would break adjacency, and passes through outside one; a second answer to a call is dropped, the first stands; the session replay harness gives way to fixed fixtures
* gateway: a cut reply is asked to go on only where the upstream goes on from a prefill — a trailing assistant message isn't one on most Chat APIs: the model answers it again from the start, reworded, and the client, an exact echo alone dropped, kept its part and the re-answer spliced as one reply with no error to tell (api.deepseek.com did so 5 times in 6). It goes on only where the prefill is real: an Anthropic Messages upstream natively (a model that turns one away answers 400 and the reply ends as it used to; the newest Claude among them), a Chat one in its vendor's own mode — DeepSeek's prefix, asked of its /beta endpoint with "prefix": true on the message, Kimi's "partial": true — a relay on this machine or the LAN serving one of their models taken to front the vendor, as geminiCompat takes one to front Gemini; everywhere else the reply ends with the error, as it used to, for the agent to try the turn again. reasoning_content goes back with the prefill by the same decision (reasoningBack named any kimi model on any host); the prefill's trailing whitespace, which Anthropic turns the request away for, is trimmed, the client having it already; and the tries' usages are summed into the reply's, the client's and the ledger's, where only the last try's was kept while the vendor billed each (a cut try's is only what it said before the cut). New tests: a re-answering upstream is never asked to go on (the client reads the error, not the garble), DeepSeek's /beta prefix mode and Kimi's partial on the wire, an Anthropic upstream's native prefill (text, thinking-only, whitespace trimmed, a refused prefill ending as it used to), the tries' usages summed, chatPrefill and prefillHow themselves. go vet, builds (darwin, nogui) and go test -count=1 -race ./internal/gateway pass (2146; gui's TestSetPortMovesTheGateway fails on main too).
* tests: three environment- and drift-broken suites pass again — port: a moved gateway finishes the connections it has (Relisten's handover), so answers() pooled a keep-alive one from its start-up polls and the old port answered from it forever; it now dials with keep-alive off, asking what the test means to: does the old port take NEW connections. plugin: TestBunIsGivenTheSystemsRoots read the shell's ambient NODE_EXTRA_CA_CERTS as the user's own (it wins by design); the test sets its own. accounts: forgetting the account in use puts the one left first since
|
||
|
|
dad4b98729 | gateway, gui: the Cline plugin's provider has ClinePass's Upstream tick too (ARNO on Discord: 这个功能可以也加到cline的插件里面吗) — "DeepSeek models only from DeepSeek's own API" was the built-in ClinePass's alone: the Cline plugin's provider (@magpie-community/opencode-cline-auth) is an account, whose editor had no such tick, whose save didn't keep pinUpstream and whose requests clinePin didn't touch. Now ClinePinnable is ClinePass, a cline.bot provider or the Cline plugin's; its editor shows the Upstream tick (one upstreamField for both editors), its save posts pinUpstream, the account keeps it (Save, allProviders), and a DeepSeek model's request reaches the plugin with "providerOptions":{"gateway":{"only":["deepseek"]}} on Chat and Anthropic Messages, as ClinePass's does. The plugin 0.1.3 has the same as an option of its own for OpenCode. TestClinePluginPinsDeepSeek (the fake plugin as cline, with a deepseek-v4-pro): unticked nothing is pinned; ticked and saved, the tick is kept and both DeepSeek requests are pinned, fake-1's not; it fails on the old code (not pinnable; the tick not kept). clinepass-pin.test.cjs: the plugin's editor shows the tick as saved and Save posts it off when unticked (WebKit, en and zh); it fails on the old app.js. model-first-pick, plugin-moved-signin, click-scroll, gui-ja and gui-de pass on WebKit. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. | ||
|
|
c38728145b |
plugin: a slow reader pauses its stream instead of racing a watchdog (#812)
* plugin: a slow reader pauses its stream instead of racing a watchdog #664 gave a plugin reply a soft watermark (softQueuedBytes 4 MiB, softQueuedEvents 4096) and a 10-second idle watchdog: past the watermark, a reader that made no progress for stallIdle had its call failed with errStalledReader, and hardQueuedBytes/hardQueuedEvents (64 MiB / 65536) cut the reply outright. Both are guesses at how long a reader may take, and both end a reply that reader was still consuming. The host now hands each fetch a credit window instead. Go grants streamWindow (512 KiB) up front and returns credit as the reader takes bytes, so the child stops producing at the window instead of being timed out; the window is charged per delivered byte plus frameOverhead (2048), so a run of tiny frames cannot fill it with metadata. streamBudget (64 MiB) and maxFrame (64 KiB) bound a whole reply and a single frame, and a child that sends past its window is a protocol violation (errProtocolOverrun) rather than a slow reader. The plugin side grew the matching gate (makeGate, gate.take/spend/add, waitDrain): a fetch holds at the window until Go credits it, and its pending writes are cancellable. Cancellation and shutdown now have one owner per call - a five-state lifecycle (sending, active, draining, aborting, released) whose single transition helper disposes the queue, detaches the watcher, commits the terminal answer, creates the abort and returns capacity exactly once - so a host failure, a Close, a cancelled context and an already-delivered reply can no longer race each other over who cleans up. A reply the host already delivered keeps its bytes and its vendor outcome when the host dies. Tests: the JS cancellation cases run the production sections with controlled stdout backpressure (stopped gate, split chunk, terminal suppression, acquired-body cancel, 30,000 cancelled bootstrap waits with no retained waiter or ready reaction); the Go cases drive lifecycle orderings directly (terminal-before-failure, failure-before-terminal, abort acknowledged before capacity returns, blocked enqueue), and the shutdown, body-close and mid-setup-cancel cases block on explicit channels instead of sleeping. TestFlowBigReplyProd reads 100 MiB non-SSE and SSE through the real host and checks lengths and SHA-256, including a 12 s reader pause. CI installs Bun 1.4.2 on Linux and runs that case in its own step, since the plugin sandbox skips when bun is not on PATH. * plugin: the stream-head credit test waits for the credits before the reply ends (CI, macos-latest: TestHostStreamHeadChunksFinal failed on the macOS runner with "the credits were given back didn't happen", while the same package passed on ubuntu-latest and locally) — the test sent the child's final answer together with its 50 chunks, so the call could be marked stopped and its pending credits dropped before waitFor saw them; a completed fetch needs no credit, so waiting longer could not recover them and the expectation was a race, not a real accounting bug. The final answer is now withheld until io.ReadFull has taken the 140 payload bytes in order and exactly 140 + 49*frameOverhead credits have been observed with frameOverhead still owed, and only then does the child answer and the test assert a zero-byte EOF read. No production code changed. Tests: go test -run '^TestHostStreamHeadChunksFinal$' -count=500 ./internal/plugin/ and -count=200 -race pass; go test -count=1 and -race -count=1 ./internal/plugin/... pass; gofmt -l and git diff --check clean. |
||
|
|
21702d01ff |
Add German translation; harden paths against unset env vars (#770)
* Add German translation and harden paths against unset env vars Add a German locale to the GUI, tray menu, usage alerts and website home page, and refuse to start when the home folder is unset or relative so nothing is written under the working folder; the pi plugin no longer adopts the user's PI_CODING_AGENT_DIR, and an inherited APPIMAGE no longer selects portable mode. Scripts refuse to run from the wrong folder or empty a shared out folder, and a new internal/testenv sandboxes every test package from the user's real files, which also fixes the Claude picker and Termux installer tests on Windows. * Makefile: know magpie's folder by its go.mod, so paths with spaces work The check compared $(CURDIR) with $(dir $(abspath …)), which make splits on spaces, so a checkout under 'My Projects' or 'John Doe' refused every target. It now reads go.mod relative to the working folder and wants magpie's module there: make -C and a symlinked Makefile work, make -f from another folder still stops before any recipe runs. * i18n: German for the strings main added since, with a parity test 75 strings main added (group order, patterns, slow-start wait, usage caps, RTK savings …) get their German, and two that main dropped go. gui-de.test.cjs checks, as gui-ja does, that de has every zh string with its placeholders and tags, and that the window reads in German when picked or on a German system. * appdir: ignore relative folder variables when reading, keep them for children CheckEnv no longer unsets anything. magpie reads the XDG folders, APPDATA/LOCALAPPDATA and every agent's folder variable through appdir.Getenv/LookupEnv, which pass over a relative value; the environment stays as it is, so the agents magpie starts get the variable as the user set it. That covers what proc.UserPath lends from the login shell too, which is set after main's check and reached the desktop app unchecked. On Windows a path rooted on the current drive (\Users\x, Git Bash's /c/x) counts as usable, and magpie version and help answer without a home, before run makes any folder. * Sandbox the tests main added since internal/wslrun gets testenv's TestMain, and four tests main added that set HOME alone set their home with testenv.SetHome, so on Windows they don't read the runner's profile through USERPROFILE. * agentenv: PI_CONFIG_DIR and OPENCODE_DB take relative names on purpose omp puts $PI_CONFIG_DIR under the home and OpenCode a relative $OPENCODE_DB in its data folder, so neither depends on the working folder: appdir no longer passes them over. * i18n: German for the Library's multi-select and System One's decision API The 19 strings main added since (#791's skill multi-select, the decision API snippets) get their German; the one main replaced (four APIs → five APIs) goes. * appdir: tidy the import into magpie's group; say what Go makes of /c/x The appdir import joins the group of magpie's own imports where a file has one. rooted's comment now says that Go takes a Git Bash path like /c/x as \c\x on the current drive, not C:\x. |
||
|
|
564033d044 | plugin: Bun is given the roots the system trusts — installing @magpie-community/opencode-kiro-auth failed with "error: UNABLE_TO_VERIFY_LEAF_SIGNATURE downloading package manifest" (chenyyi001 on X): Bun checks TLS against the roots it ships, not the system's, so behind a box that re-signs HTTPS with a root the user installed (a company proxy, a VPN, Surge's or Proxyman's MITM) magpie gets through and bun add, and a plugin's own requests, don't. bunCommand, which starts every bun (installs and the host), now sets NODE_EXTRA_CA_CERTS to the system's roots unless the user set it: on macOS the certificates in the System and login keychains the user or an admin marked as trusted (security trust-settings-export; one denied, or only kept in a keychain, stays out; Apple's own aren't added), written once a run beside Bun's cache; on Linux the distribution's CA bundle (SSL_CERT_FILE first); on Windows the ROOT store. NODE_USE_SYSTEM_CA does nothing for bun 1.4.2's installs. Reproduced with a local registry behind a re-signing proxy: bun add through bunCommand fails with the reporter's error on the old code and installs opencode-kiro-auth@0.1.7 with the proxy's root trusted; TestBunIsGivenTheSystemsRoots fails on the old code and passes, TestTrustedIn and TestSystemRootsAreTheTrustedOnes cover the trust settings. Not tried: a real keychain-trusted root (changing trust settings is left to the user), and the Windows store on a Windows machine (the test box was unreachable; it builds). go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui of internal/plugin and internal/proc under a temp HOME pass. | ||
|
|
fa1e3947f9 |
plugin: isolate stalled streams and guard GUI refreshes (#664)
* plugin,gui: a stalled stream can't wedge the host, stale reads can't draw, and the browser suite runs in CI internal/plugin: the host has one reader of the child's stdout, and it handed each message to its call by sending on that call's channel. A caller that stopped draining a streamed body (a slow HTTP client on a plugin-backed chat stream) filled the channel; the reader then blocked, and every other call through the host -- all plugin providers and accounts, moved built-ins included -- stopped being dispatched until the stalled stream was drained. A second call's answer arrived only after the first was drained. Each call now queues its head and chunks in a bounded backlog (4 MiB of chunks, 4096 of them) drained by its own reader, so the host's reader never waits on one consumer; a call whose consumer stops reading is given up on alone, with the limit as why. Aborts go through a goroutine too (abortLoop), so a child that stopped reading its stdin can't make an abort wait either. host_test.go covers the stalled consumer, the bounds and the abort path. internal/gui: loadUsage() and refreshUsage() both read the Overview's usage?period=, and neither checked after the await that its answer was still the newest: a 24 h answer landing after a 7 d one drew the old period's figures under the new period's name, and leaving the tab could still write the Overview's cost into the shared header. Both now claim one shared read generation before the request and draw only when it is still the newest and the page, tab and period are unchanged. providersWhileFetching() also stopped for good after one failed poll or reload; it now asks again. usage-stale-answer.test.cjs and providers-fetching-retry.test.cjs lift the real functions out of app.js and drive them with a faked api/page/timers, so they need no browser; usage-refresh-flight.test.cjs gains the browser case for the same race. Makefile/.github: `make test-ui` runs every internal/gui/tests/*.test.cjs through Node's test runner -- auto-discovered, UI_TEST_CONCURRENCY files at a time (default 2) -- and a new `ui` job in Test runs it on ubuntu-latest with Playwright's Chromium and WebKit. A missing browser fails the job; it never skips to a pass. internal/gui/tests/README.md points at `make test-ui` instead of a hand-kept command line. Verified: node --test on the two new files 21/21; go test -count=1 ./internal/plugin/ ok; go test -race -count=1 ./internal/plugin/ ok; go vet ./internal/plugin/ and go build ./... clean; node --check on the changed JS; actionlint on the workflow. Not verified: the full browser suite is not green yet. `make test-ui` is 1641 passed / 70 failed of 1711 across 225 files, and 12 of the 19 failing files fail the same way on a pristine main (a known baseline: a fixture boots the page in web mode while asserting the desktop window's narrow behaviour). The `ui` job will be red until those baselines are dealt with -- nothing is skipped or masked -- and the job has not yet run on a GitHub-hosted runner. * plugin: distinguish stalled readers from large responses * plugin: clarify best-effort abort delivery |
||
|
|
f8a3f16dc6 | plugin: the community's plugins are looked for updates every hour, not every six (#681, ChenstarMr on v0.1.808: told TraeCN 0.1.5 would come by itself, installed it by hand eleven minutes after it was out) — KeepUpdated ran CheckUpdates 30 s after the gateway's magpie started and then every updateEvery, six hours, so a fix published to a @magpie-community plugin and announced on its issue could take most of a working day to reach the reporter. updateEvery is now an hour: one look at npm's metadata for the plugins installed (no download unless a community plugin, unpinned and switched on, has a newer version; someone else's still waits for a click), cheap at that pace. Bun's own check (KeepBunUpdated, a GitHub release lookup with a two-day hold) keeps six hours under its own bunEvery. The Update button's tooltip on a community plugin says "within the hour" / 一小时内 where it said within a few hours. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass (TestProvidersAnswerWhileListsComeIn, TestUserPath and TestClaudeSignedOut failed once under load with four other test runs going and pass on their own); the plugin-*.test.cjs GUI tests pass in webkit. | ||
|
|
0ae7330242 | plugin: a pi package's sign-in screen on pi's terminal UI is asked in magpie, and an empty reply a package makes of a vendor's error says the error (all pi plugins: Kiro's login hung, pi-zcode's quota ran out as an empty answer) — pi-provider-kiro asks how to sign in through ctx.ui.custom, a component of pi-tui (a SelectList, then Input fields for a start URL and region), and magpie gave pi's runner no UI, so custom came back undefined and the sign-in waited forever. pi's runner now has a UI for the sign-in's questions: select, confirm, input and editor become magpie's sign-in prompts, and custom draws the package's component and asks its list or field the same way, answering through the component's own onSelect/onSubmit, until the package calls done. A component may keep its parts to itself (Kiro's gives only render and handleInput) and each package loads its own copy of pi-tui, so the parts are found by what drawing walks: every container goes through its children, so the arrays iterated while it renders are what it shows, in order; the text above the list is its question. Outside a sign-in the UI answers as pi's no-UI context does. session_start now reaches every package at startup, which Kiro needs to keep the context it asks through. Separately, pi-zcode turns ZCode's 1005 quota error into a reply with no content; magpie keeps the small JSON or text bodies a package's fetch gets while it streams, and a stream that ends empty answers that body's message and code (429 for quota or rate limits, else 502) instead of an empty message. TestPiPackage drives a fake package that asks on real pi-tui classes (a list, then a start URL field) and fakes ZCode's empty quota reply; it fails on the old code (no first prompt) and passes. With real accounts in a sandbox HOME: Kiro's "Kiro Login" (API Key, Web Login, Device Code) came as a select, Web Login gave the app.kiro.dev sign-in, and after signing in kiro-plugin/claude-sonnet-4-5 called get_weather and answered with its result. pi-cursor (1.4.38, latest) answers plain turns but its models say no get_weather tool exists, though the package sends it (toolCount 1, its MCP schema) and Cursor's stream carries fields the package doesn't know; that is between pi-cursor and Cursor. The ZCode quota path wasn't tried with a real account. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. | ||
|
|
67092380fb |
plugin: pi packages run as pi runs them — a program a login starts can't take the host's stdin, a select is answered with its option's id, and the extensions get pi's runner, pi's auth.json and session_start (owner: 都补上,用真实账号测) — tried with real accounts, five pi packages had problems that the fake one didn't show. pi-devin-plus runs devin auth login with stdio "inherit" when it finds no Devin sign-in, and the program read magpie's next requests from the host's stdin as its answer ("Login canceled") while writing over its answers; host.js now gives every program started through Bun.spawn and Bun.spawnSync (all of node:child_process goes through them) nothing to read and stderr to write. pi-zcode asks its region with onSelect and options { id, label }, pi's own shape, and checks the id ("bigmodel"); pi.js answered with the label, so the login said "Login cancelled". Packages read their sign-in back from pi's auth.json (readStoredCredential: pi-zcode for its plans, pi-cursor for its token, pi-provider-qoder through the registry, pi-antigravity syncs its accounts through it) and register what they find on session_start (pi-zcode its plans' models, pi-devin-plus and pi-xai-oauth refresh their catalogs), and a provider registered after loading went into a queue nothing read. Now each package is bound with pi's ExtensionRunner as pi's session binds it (bindCore: no UI, tools or session of magpie's), so a later registerProvider reaches the runtime; each pi provider's first account is kept in $PI_CODING_AGENT_DIR/auth.json as pi keeps it, written when it changes; and a provider newly signed in, and each one signed in when the host starts, is sent session_start, after which magpie lists its models again if they changed. A runner that can't be made leaves the packages registered as before. TestPiPackage now has a select answered with an id, a login that starts a program on the terminal, and a session_start that reads the sign-in from pi's auth.json and adds a model; it fails on the old code (the label answered; the model never listed, auth.json empty) and passes. With real accounts in a sandbox HOME: pi-zcode signed in through Z.ai and lists 11 models (none before); its requests reach ZCode but the account's Start Plan answers 200 {"code":1005,"msg":"exceed quota limit"}, which pi-zcode reads as an empty answer (its bug). pi-provider-qoder signed in through Google, 16 models, tool round trips on Kimi-K3 and Efficient. pi-devin-plus signed in, its catalog refreshed on session_start (53 models), tool round trips on swe-2 and claude-opus-5.5; pi-xai-oauth (grok-4.7) and pi-antigravity (gemini-3.8-flash) tool round trips; @rahularya01/pi-cursor answers text (its agent doesn't call a client's tools, upstream). pi-provider-kiro wasn't signed in: it needs pi's terminal, a Builder ID or an IdC login. go vet, builds (darwin, GOOS=linux -tags nogui, GOOS=windows) pass.
|
||
|
|
d62fecd468 | plugin: reuse config parsing and avoid request body string copies (#696) | ||
|
|
1baad0d2d4 | plugin: pi packages are plugins as OpenCode's are — a pi coding agent package (its extensions) adds providers to magpie: their models, their sign-in, renewals and requests (owner: 我希望全面兼容 pi 插件!!!!!!!!!!) — a package is pi's when its package.json has a "pi" manifest, the "pi-package" keyword, or pi-coding-agent among its dependencies (IsPi, mirrored in host.js). Adding or updating one installs @earendil-works/pi-coding-agent beside it, and the plugin host loads every pi package through pi's own loader (pi.js: discoverAndLoadExtensions and ModelRuntime, with the credentials kept in plugin-auth.json), so an extension's registerCommand, registerTool and pi.on are taken and left unused, and its registerProvider calls become magpie providers. A provider's models keep their context, output, images and the reasoning levels their thinkingLevelMap allows; its OAuth login runs as pi runs it, each onPrompt asked in magpie's sign-in in turn, onAuth's page opened, a later prompt being the code pasted back; a provider with apiKey takes a key; magpie renews through refreshToken and saves the result. Requests arrive as Anthropic Messages and go to pi's stream: system and tools, images, tool results, thinking budgets and effort as the nearest level the model has (and the least it allows when it can't turn thinking off: Antigravity's gpt-oss refused requests without one), max_tokens held to the model's; the answer comes back as Messages SSE or JSON with thinking and its signature, redacted thinking, tool calls (their thought signatures kept for the next turn), usage, and a failure before anything is said as the vendor's status with Retry-After. The Plugins page's search also finds pi packages that bring a provider. Tried with the owner's real Antigravity account through pi-antigravity, next to the Grok OpenCode plugin in the same host: 14 models listed, Gemini streaming with usage, Claude Sonnet thinking with signatures, gpt-oss, tool round trips on gemini-3.8-flash, gemini-3.1-pro and claude-sonnet-4-6, a 400 and Antigravity's 429 passed on with their status. A Claude Code turn didn't get through: Antigravity answers 429 to any system prompt with Claude Code's billing header or its "Claude agent" line, through magpie's built-in Antigravity just the same. TestPiPackage (a fake pi package: models, an OAuth login asking twice with a wrong code then a right one, a key, streaming and JSON answers, thinking and tool signatures back, a 429, a renewal) and TestSearchFindsPiPackages pass. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestClaudeRunEndsWhenTheClientLeaves, TestProvidersAnswerWhileListsComeIn and TestSavedButSignedOutClaudeServed, timing tests that pass alone. | ||
|
|
ac1a8a4f85 | gui: Trae CN's plugin shows Trae's logo (owner: trae cn plugin 没有 logo) — the registry gives it the icon "trae", which magpie didn't have, so its tile and provider row had none. Adds lobe-icons' mono TRAE mark as icons/trae.svg (taking the text colour, as qoder's does). TestMarketIconsAreBundled checks every icon the built-in list, the older plugins and the registry name is among the GUI's pictures; it fails without trae.svg. Seen on the Plugins page of a sandbox built from this tree. go vet, builds (darwin, GOOS=linux -tags nogui, GOOS=windows) and go test -tags nogui ./internal/plugin ./internal/gui pass. | ||
|
|
37809ac722 |
usage: WorkBuddy's credits are said beside their share — on the Routing page, the Usage page and the menu bar, used or left as the share is (#659)
Moody-Sin: the Routing page showed a WorkBuddy account as "已用 71%" only, which for a plan counted in credits says little. A quota window now carries its own count when the vendor counts in amounts — amount of limit, in unit (QuotaWindow, QuotaSpan for quota --json and /v1/magpie/quotas, and the routing trace's Weighed, from the window its share is of: Allowance.Count). WorkBuddy's built-in fills it from the resource summary, and @magpie-community/opencode-workbuddy-auth 0.1.8 tells it from auth.usage (amount / limit / unit, passed through host.js); the workbuddy mover's min is 0.1.8. The Routing page's stage and its list of what each account did, the Usage page's meter, the menu bar's tooltip and the TUI say "3,550 / 5,000 credits · 71% used", or "1,450 / 5,000 credits · 29% left" with Left on (积分 in Chinese); a window counted in shares alone reads as before. quota-credits.test.cjs (WebKit, en/zh, used/left, Routing and Usage pages) fails on the old assets and passes; TestQuotaWindowCount, TestAllowanceCount, TestTraceTellsTheCount, TestPluginUsage (the count through the host) and TestWorkBuddyAccounts cover the Go side. Tried with the real WorkBuddy and WorkBuddy AI accounts in a sandbox magpie: moved onto the local plugin, /api/usage/quotas gives 33.29 / 3900 and 13.47 / 2250 credits and the Usage page reads "33.29 / 3,900 积分 · 已用 0.9%", "3,866.71 / 3,900 积分 · 剩余 99.1%" after a click on the count; not moved, the built-in gives the same for WorkBuddy. The Routing page with two real accounts of one provider wasn't tried (one account each). go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. |
||
|
|
123594489a | plugin: a host stopped mid-renewal saves the new token first, and two hosts never write each other's sign-ins away — a stopped host (quit, plugin update, restart) was killed 2 s after stdin closed even while it renewed a sign-in through auth.refresh, after the vendor had spent the old refresh token, so the account was signed out; and while a host restarts the old one and the new one both rewrote all of plugin-auth.json from what each had read, so one host's save could drop the other's accounts or newer token. host.js now tells magpie how many renewals are under way (event "renewing"), and stop gives a host still renewing after the 2 s up to 15 s in all, until the renewal is saved; with none it stops as fast as before. Every change to plugin-auth.json (setAuth, removeAuth, settle, take, auth.set, a renewal's save, and magpie's SignOut when no host runs) is made under plugin-auth.json.lock (O_EXCL, broken when older than 10 s), reading the file afresh and changing only its own accounts; a renewal's save checks under the lock that the stored sign-in is still the one it renewed, so an older host never writes a stale token over a newer one. TestStopLetsARenewalEnd (a 4 s refresh, the host restarted in the middle: old code kept the spent token) and TestTwoHostsKeepEachOthersSignIns (two hosts saving 40 accounts each at once: old code lost 38 of 80) fail on the old code and pass; TestStopWithoutRenewalIsQuick keeps the quick stop. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestProvidersAnswerWhileListsComeIn and TestClaudeSignedOut, timing tests that pass alone and in their packages rerun. Not tried with real plugin accounts. | ||
|
|
860ed6618d | plugins: a 「国内镜像」 switch on the Plugins page downloads the list, plugins and Bun from mirrors in China first (Discord, akic404: GitHub/npm are slow from the mainland; asked for ghproxy) — Plugins › Mirrors in China, off by default, kept in settings (chinaMirror). On, source asks a copy of the very file before its official address: npm's registry (packages, versions, search) at registry.npmmirror.com, Bun's release zips at registry.npmmirror.com/-/binary/bun, and the community registry.json at cdn.jsdelivr.net/gh/magpie-community/plugins@<branch>; bun add gets BUN_CONFIG_REGISTRY=npmmirror unless the environment names a registry. No proxy: gh-proxy (MAGPIE_MIRRORS' GitHub fallback) handles nothing the switch sends, since a proxy could hand back other code. A mirror asked first that can't answer (404/403/5xx, e.g. a version it hasn't synced) falls through to the official address; npm's weekly downloads stay api.npmjs.org's (npmmirror counts its own). Checked by hand: npmmirror's bun-v1.4.2 darwin-aarch64 zip has the SHA-256 pinned in bun.go, jsDelivr's registry.json is byte-identical to raw.githubusercontent's, and bun 1.4.2 add through npmmirror locks the same sha512 integrity npmjs gives. TestChinaFirst, TestChinaComesFirst, TestChinaMirrorAnswers, TestChinaMirrorsMissIsAskedOfficially, TestDoFaithful, TestBunInstallsFromChinaMirror and TestPluginsMirrorSwitch cover it. A live run with the switch on fetched the list from jsDelivr, npm info from npmmirror and Bun from npmmirror (checksum passed); a sandbox magpie web showed the switch in the Plugins head, saved it, kept it after a reload, and a click didn't move the page. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried from inside mainland China. | ||
|
|
69368007b0 |
termux: support Android builds and installation (#644)
* termux: build natively and run plugins on Bun 1.4.2 Select nogui for Android builds, open browser and OAuth links with termux-open-url, and write an executable Termux:Boot script that starts the gateway with shell-quoted paths. Bun 1.3 has no official Android build. Upgrade the default plugin runtime to 1.4.2, add the official Android arm64 and amd64 download targets, and update SHA-256 checksums for every supported platform. Document Termux installation, startup and source updates. Validation on Termux arm64: - Native build and go vet pass. - A fresh config downloads and verifies Android Bun 1.4.2 and loads a test plugin. - Boot startup can be enabled and disabled. - The Web page, authenticated state API, unauthenticated API rejection, gateway healthcheck and Web quit action work. - Browser argument preservation, boot path quoting and permissions, and plugin tests pass. Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-01a10014-56c1-73d3-b3ee-2ea569713989 * site: install Android builds in Termux Detect Termux through TERMUX_VERSION or its PREFIX and select the Android CLI asset instead of a Linux binary. Install into PREFIX/bin by default, keep MAGPIE_BIN_DIR overrides, and suggest the Web or terminal interface without Linux desktop setup instructions. When the release has no Android asset, explain how to build from source. Add android/arm64 to release-cli and stop the build loop on failure. Document the installer and its release-asset requirement. Shell syntax and all 11 site tests pass, covering platform selection, custom paths, missing assets, checksum rejection and existing Linux installation paths. The Android arm64 release build succeeds on Termux; with a simulated release feed, the installer verifies and installs that binary into PREFIX/bin and the installed command runs successfully. Amp-Thread-ID: https://ampcode.com/threads/T-01a10014-56c1-73d3-b3ee-2ea569713989 Co-authored-by: Amp <amp@ampcode.com> * ci: check Android releases and the installer Build both linux/amd64 and android/arm64 through make release-cli in the CLI test matrix. Run the site and installer tests in the Linux job with Node 22, and check the installer's shell syntax. The release workflow in yetone/magpie-releases already calls the source Makefile's release-cli target, uploads dist/magpie-cli-*, and generates checksums for every artifact. The Android target added to TARGETS is therefore included without changing that workflow. Replace the full Termux README section with a short platform note beside the Linux and Windows installation requirements, removing duplicate commands, the fork URL and temporary release availability details. Both CLI release builds, all 11 site tests, shell syntax checks and actionlint validation pass locally. Amp-Thread-ID: https://ampcode.com/threads/T-01a10014-56c1-73d3-b3ee-2ea569713989 Co-authored-by: Amp <amp@ampcode.com> --------- Co-authored-by: Amp <amp@ampcode.com> |
||
|
|
cb82822f3d | source: add opt-in China mirror fallback (#566) |