3ee97def30cc15466a869ff9f6bfc0c9348c4e0e
2500
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3ee97def30 |
claude: a server or container without Claude Code can have magpie download Anthropic's own build, asked first and checked against its manifest (Jorben)
A Claude subscription signs in and answers through the local `claude`, so
on a server or in the Docker image, which has none, it couldn't be used
(Jorben on Discord: could it be downloaded on first use, as Bun is).
- internal/claudecode: Anthropic's native build, as claude.ai/install.sh
takes it: downloads.claude.ai/claude-code-releases/latest names the
version, its manifest.json the platform's SHA-256 and size (linux
glibc/musl, darwin, win32). A build that doesn't match both is not kept.
It goes to <cache>/claude-code/<version>/claude, one version kept. Find
takes the machine's own claude first, then the download.
- magpie claude-code [install [--yes]|remove]: says the version, platform,
size, the URL it comes from, where it goes, that its checksum is checked
and that it is Anthropic's software under Anthropic's terms, then asks
[y/N]. magpie accounts add claude offers it when there is no Claude Code
(not on Windows, where one in WSL may be the one).
- the gateway's claudeBinary and the sign-in's claudeExecutable take
claudecode.Find; their "not installed" errors name the command.
- Docker docs (en, zh, ja): Claude sign-in needs `docker exec -it magpie
magpie claude-code install` first; kept in /config/cache.
The Dockerfile is unchanged: distroless cc-debian12 has glibc.
Run:
- go test -tags nogui ./... under a temp HOME with pinned caches: all ok
- gofmt -l: none; go vet (darwin), GOOS=linux and GOOS=windows go vet
-tags nogui ./...: clean; darwin, linux, windows builds
- TestClaudeSubscriptionRunsTheDownloadedClaudeCode fails on the old
proc.FindTool lookup ("Claude Code is not installed"); taking the
checksum compare out fails TestInstallRefusesABuildTheManifestDoesntName
- real: magpie claude-code install --yes downloaded Claude Code 2.1.296
darwin-arm64 (230 MB) into a sandbox HOME, checked; it ran --version
- real, in gcr.io/distroless/cc-debian12:nonroot with the image's /config
layout: install downloaded 2.1.296 linux-arm64 (245 MB) to
/config/cache/magpie/claude-code, it ran `2.1.296 (Claude Code)` there,
and magpie accounts add claude went on to print the claude.ai sign-in
link and ask for the pasted address
- the claudecode tests built for linux/arm64 pass in debian:12-slim
|
||
|
|
f9113626c7 |
docs: LESSONS.md gets the review of 10-09 and 10-10 — exemptions scoped by protocol position, a documented security gap is a bug, two sessions never share a checkout; user-set values, error cause and timing tests seen a second day
Reviewed 68aab298..c98160c0 (92 commits) plus the 10-09 ticks: security/privacy probe of every auth, network, telemetry and credential change, fix-again chains, tags vs green runs (all v0.1.1137-1154 on green, 16 assets), commit bodies and community merges. No lesson reached a third day, so nothing moves to the code standards. Filed #1469 for TestKimiHeldBurstTellsNoSpeed. |
||
|
|
c431225a08 |
gateway: a browser that reaches loopback under a web site's name (DNS rebinding) is refused unless it sends an enabled gateway key
What changed
- lanGuard refuses (403) a loopback request that carries a browser's
marks (Origin, Sec-Fetch-Site/Mode, a Mozilla/ user agent) when its Host
isn't this computer's: an IP, localhost, *.localhost, a container
runtime's name for the host (host.docker.internal and kin),
MAGPIE_PUBLIC_URL's or MAGPIE_ADDR's host, or the machine's own name.
An enabled gateway key still lets it in.
- Why: a page whose DNS answers 127.0.0.1 after it loads is same-origin
with the gateway. Its GET sends no Origin and its POST one equal to the
Host, so corsGuard's foreignPage let it by, and on loopback no key is
asked. The nightly security review reproduced it on origin/main: Host
and Origin rebind.attacker.example:3425 from 127.0.0.1 got 200 on
GET /v1/models, POST /v1/chat/completions (spends subscriptions) and
GET /v1/magpie/quotas (account names, balances). The subsystem doc
listed it as a known gap.
- Agents send none of a browser's marks and pass under any name; browsers
and webviews calling localhost or an IP are unchanged.
- Tests: TestCORSKeyThroughTheServer and the Omarchy test now set a real
Host (httptest's default is example.com). TestForeignPagesRefused's
"own origin behind a port-443 name" row from loopback with no forwarding
header now sends a key: without one it is the rebound page.
- docs/subsystems/gateway-routing.md describes the check in place of the gap.
Ran
- TestReboundPageRefused: fails on origin/main ("a rebound page's GET:
200"), passes with the change.
- go test -count=1 ./internal/gateway/ ./internal/gui/ ./internal/access/
(macOS, go1.27): ok. gofmt -l clean; GOOS=linux and GOOS=windows go vet
./internal/gateway/ clean.
Not tried: a real browser against a real rebinding DNS name.
|
||
|
|
4225ba1efb |
site: a partner's /go link and the home page's sponsor card say they came from magpie, as ref=magpie (yetone)
goTarget adds ref=magpie to the partner's key page or website, keeping the address's own query and fragment; an address that names its own ref (a referral code) keeps it as it is. The home page's sponsor card links to yylx.io/?ref=magpie. Run: node --test site/*.test.mjs (27 pass), with new cases for the query, the fragment and a partner's own ref. |
||
|
|
a21a2b54f1 |
gateway: a Codex title the model was cut short on before any text goes back as response.incomplete with its reason and usage, not as a completed reply with nothing in it (#1466, Edward4226)
codexTitle read the title model's reply with compactReply, which kept
the output and usage of response.completed and response.incomplete alike
and dropped how it ended, and writeTitleReply always wrote status
"completed". A title cut short at max_output_tokens (all of it spent
reasoning) reached Codex as a finished reply with output [].
What changed:
- compactResult keeps the response's status and incomplete_details
(from the terminal event's response, or its type).
- codexTitle: when no title can be read and the reply was incomplete,
writeTitleEnd streams response.created then one response.incomplete,
status "incomplete", the vendor's incomplete_details and usage. A
title read from the reply still completes with it; Titles off still
completes with nothing (writeTitleReply is writeTitleEnd "completed").
- Compaction (codexCompact) is left as it was: a summary cut short with
text in it is still used.
Codex: codex-rs/codex-api/src/sse/responses.rs takes response.incomplete
with a reason other than interrupted/content_filter as a stream error
("Incomplete response returned, reason: max_output_tokens"), which
core/src/responses_retry.rs retries up to the provider's
stream_max_retries; then the hidden title turn fails and
tui/src/temporary_structured_request.rs sets no title, as it did for
the empty completed reply. This is what Codex does when its own backend
answers so.
Run:
- TestCodexTitleIncompleteStaysIncomplete (Responses upstream, #1466's
two-field schema) and TestCodexTitleIncompleteFromChat (Chat upstream,
finish_reason length) fail on the old code (one response.completed,
status completed) and pass now; the same test's whole title under the
same schema completes. Existing TestCodexTitle*/TestTitle* pass.
- gofmt -l clean; go vet -tags nogui ./internal/gateway on darwin,
GOOS=linux, GOOS=windows.
- go test -tags nogui -count=1 ./... under a temp HOME: all ok (with
./internal/gateway and ./ rerun after this change).
- Real use: the built binary's gateway in a sandbox HOME with only the
owner's DeepSeek provider, the issue's title request on
deepseek-flash. max_output_tokens 16: old response.completed,
output [], status completed; new one response.incomplete, reason
max_output_tokens, usage 16 reasoning tokens. 2048: a title and
description, response.completed, before and after.
|
||
|
|
ea84c99c78 |
gateway: a DeepSeek model's tool call the client kept no thinking for goes with reasoning_content, so DeepSeek no longer turns the request away (#1462, Clear8Heart)
DeepSeek's Chat API in thinking mode refuses a whole request with 400 "The `reasoning_content` in the thinking mode must be passed back to the API" when an assistant tool-call message since the user's last message comes back without reasoning_content. OpenCode (and any client that continues a turn another model made, or that dropped the thinking) sends such turns, and magpie relayed them as they came. What changed: - chatReplaysReasoning(host, model) names the Chat upstreams that want reasoning back: a DeepSeek host, a model named deepseek wherever it is served (OpenCode Go's deepseek-v4.1-flash, OpenRouter), and Command Code's plugin. buildChat uses it as before. - Passthrough (gateway.go, provider.Chat): for such an upstream, not Mistral (#494), withReasoningOnToolTurns gives every assistant tool-call message without reasoning_content one: its thinking from another field (reasoning, OpenRouter's) when it has some, else " ". The gate takes the member's model, so a group reaching a DeepSeek member is covered. - Translated (buildChat, Responses/Anthropic -> Chat): a tool-call turn with no thinking gets " " too. - Every tool turn, not only the last: DeepSeek also refuses an earlier tool turn of the current user turn missing it. - " ", not "": DeepSeek's Chat takes "", but relays that omit empty fields drop it, and DeepSeek's Responses API refuses an empty reasoning_text. A vendor that refuses the field still gets the #494 retry without it. Checked against api.deepseek.com (owner's key, deepseek-flash, Chat): last tool turn missing -> 400; "" -> 200; " " -> 200; real -> 200; an earlier tool turn of the current user turn missing -> 400; a tool turn before the latest user message missing -> 200; answers missing -> 200. Not changed, found on the way: DeepSeek's Responses API refuses a function_call with no reasoning item (and empty reasoning_text), and its Anthropic API a tool_use with no thinking block; magpie relays those natively as they come. OpenCode Go's deepseek-v4.1-flash could not be tried for real: the owner's workspace answers "requires Global regions". Run: - New tests fail on the old code (TestChatToolTurnWithoutReasoningRelayed: 400 "reasoning_content ... must be passed back"; TestTranslatedToolTurn WithoutReasoning: 400), pass now; TestChatToolTurnWithoutReasoning NotForMistral and the Mistral/MessageReasoning tests pass. - gofmt -l clean; go vet -tags nogui ./internal/gateway on darwin, GOOS=linux, GOOS=windows. - go test -tags nogui -count=1 ./... under a temp HOME: all ok. - Real use: the built binary's gateway in a sandbox HOME with only the DeepSeek provider. Old: last tool turn missing 400, earlier one missing (stream) 400, plain answer 200. New: all three 200. |
||
|
|
282c9feb23 |
agent: a packaged Claude Desktop gets magpie's gateway in the real %LOCALAPPDATA%\Claude-3p too, which 2.31226 reads, not only the package's (bg5eau on X)
bg5eau on X: Claude Desktop 2.31226.1, installed as an MSIX package on
Windows, never reached magpie. magpie wrote deploymentMode and its
configLibrary profile only into the package's
LocalCache\Local\Claude-3p, and that build reads the real
%LOCALAPPDATA%\Claude-3p. Copying configLibrary there and setting
deploymentMode 3p fixed it for them.
What the build does: 2.31226.1's asar (index.chunk-Cyt2WTko.js) resolves
the 3p userData with Ou() = path.join(LOCALAPPDATA, "Claude-3p"). 2.7032.0
has the same code (GY()). It reads deploymentMode from
Ou()/claude_desktop_config.json and the gateway from Ou()/configLibrary.
The path didn't change; the package stopped redirecting it into
LocalCache. An install's folders don't say which build it is, so:
- desktopdir.Sets returns every set of folders: Find's choice first, then
the other one for an MSIX install. Everywhere lists every set's folders.
- Claude Desktop's switch writes and removes deploymentMode, the profile
and _meta.json in each set. The package's set keeps its own stash keys
(".package"). A stash that an older magpie wrote for the package's
folders moves onto those keys, so turning it off still restores them.
- The check runs on each set, names the folder when there are two, and
checks Claude-3p's deploymentMode as well.
- The siblings: the library's MCP servers go into every Claude-3p file
that exists, and Cowork skills and Desktop sessions are read from every
set.
Run:
- go test ./internal/{desktopdir,agent,library,sessions,usage}: green.
- The new tests (TestClaudeDesktopMSIX, TestClaudeDesktopMSIXStashBefore,
TestClaudeDesktopMSIXBoth3p) and TestCallsCoworkMSIX are red against
the old code. StashBefore also goes red with desktopAdoptStash disabled.
- gofmt; go vet for darwin, linux and windows.
- On the Windows box, Windows test binaries for the four packages:
- desktopdir and library pass.
- agent and sessions fail exactly the set that main's binaries fail
there (#1388, and newly filed #1468).
- Every Desktop test passes.
|
||
|
|
74c48ac060 |
agent: the managed daemon's subcommand helpers are no stale Codex copy (#1461)
Codex keeps a long-lived helper next to the managed app-server (codex
app-server daemon pid-update-loop). It matches the codex startsWith
pattern, carries no --managed-daemon, sits under no app bundle or
Application Support folder, so codexCopyKind called it "cli": a helper
that started before magpie's last change to what Codex reads counted as
"A Codex in a terminal" forever, a daemon restart didn't end it, and the
only way to clear the row was killing a process the user can't name.
codexCopyKind now answers "helper" for an app-server daemon <sub>
command line, and StaleCopies skips it: it reads no list and there is no
Codex to quit. The daemon itself (--managed-daemon) is told apart
before, and a terminal Codex still counts.
Red/green: TestCodexDaemonHelperIsNoStaleCopy runs the helper alone from
before magpie's change — FAIL on the old code (one stale "cli" copy),
passes now; a terminal codex from before still counts 1.
Generated-by: GLM-5.3-Flash (ZCode)
Reviewed and landed by the maintainer (cherry-pick of b40a1dd0 from
ggbdpq:fix/codex-daemon-helper-1461, lychjck's report). Run on main at
|
||
|
|
c98160c045 |
gateway: a group's rule can pause one of its models at set hours, days or agents, leaving it out of the group then, not first, not a fallback, not kept by affinity (John on Discord)
John asked for a pause in a group's Rules, e.g. a model paused in its vendor's peak hours. A time rule could only send those turns to another member first: the paused-for model stayed behind it for failover, went first when the other was unready, and was kept by affinity mid-turn. What changed: - provider: Rule.Pause (`"pause": true`, typed `pause=<model>`). It holds by time/days and agents only; cleanRules and ParseRule refuse any other condition, and refuse a pause with none. Pause rules are not send-to rules: MatchesBesidesIntent, ruledEntry and Ruled() leave them out. PausedOut(g, ms, agent, at) drops members a pause holds for, from the group's own rules and each nested group's (a group named = all its models; a model named = wherever a nested group has it, #625). - gateway: serve and embeddings run PausedOut on ruleClock before planning, on every request. Every member paused -> 503 naming each rule (pausedError). The trace's GroupRef carries Paused. - GUI: the rule editor's "send to" is now an app-menu action button (send to / pause); a pause hides the request conditions (clearing them), keeps hours, days and agents, and isn't saved without one. The group card's rule reads "→ pause <model>", and the Routing trace says which model was paused by which rule. Strings in zh, zh-TW, ja, de. - CLI/TUI: `magpie group rule add <g> pause=<model> time=… days=…`. - docs: gateway-routing.md step 2; start.html en/zh/ja "Hours and days". Run: - gofmt -l clean; go vet -tags nogui ./...; GOOS=linux and GOOS=windows go vet -tags nogui on gateway, provider, tui, root; go build ./... - go test -tags nogui ./... under a temp HOME with pinned caches: all ok. - rules_pause_test.go (3 tests) fails with the PausedOut call in serve removed (the paused model answers mid-turn, as a fallback, and the all-paused case returns 200). grouprule_pause_test.go covers clean, parse/Line, Ruled, Pauses and nested PausedOut. - GUI: routing-pause.test.cjs in Chromium and WebKit, en/zh at 1100px and en/zh/zh-TW/ja/de at 420px: 14/14; all 7 Chromium cases fail on the old routing.js. routing-*, group-*, i18n-keys, gui-ja, gui-de, click-scroll: 618 pass, 4 fail under --test-concurrency=4 (routing-blocked, -idle-frame, -side-calls, -wb-refused), all of which pass run alone; routing-side-calls failed 1 of 6 alone and passes with the old routing.js too, so it is a load flake not touched here. |
||
|
|
34f2078db8 |
site: a partner's website and key links go through usemagpie.ai/go, which counts the click; fetches of the partner list are counted too
The app's own partner counts only come from users who leave the stats on, so a sponsor's reach and clicks are now also counted where the server sees them. /api/partners hands out each partner's website and keysUrl (and a region's) as usemagpie.ai/go/<id>/site|keys[/<region>]; /go sends a "magpie partner go" event to PostHog (partner, link, region, country) and 302s to partners.js's own URL. One in ten list fetches sends "magpie partners fetch" with weight 10 while someone is listed. Events carry a random id each, no install id or address. Ended entries leave the list but /go still resolves them, since added providers keep the links. Privacy text updated in README (en, zh), docs/reference.md and the site docs (en, zh, ja). Run: node --test site/*.test.mjs (27 pass); a scratch copy with a fake partner served rewritten links, 302'd /go to the real URLs and posted both events; PostHog's /i/v0/e/ answered 200 to the payload shape. |
||
|
|
4eb90b69f4 |
provider: after magpie switches Codex's account, Codex's accounts say the Codex app is still on the one before and to quit and reopen it (CavillZhang on X)
The Codex desktop app (ChatGPT.app 26.1007, bundled codex 0.162) reads auth.json once, as its app-server starts. codex-rs AuthManager only reloads it for the same account (reload_if_account_id_matches skips a different one; a full reload is the app's own login/logout). So after magpie's switch the running app keeps the spent account's token, its webview reads /wham/usage with it (straight to chatgpt.com, which magpie can't intercept), gets rate_limit.allowed=false, and hardBlocked stops the composer in every thread with "You're out of Codex and Work usage". Only quitting and reopening the app moves it. magpie now notes, like the managed daemon, the app's own app-server (…/CodexCLI.app/Contents/MacOS/codex … app-server, or the Windows Store app's codex.exe) running at a switch and the account it holds, until that process changes. Codex's accounts show "The Codex app is still signed in as …; quit it and open it again" with Got it; `magpie accounts` and the gateway log say the same. Run: go test ./internal/provider ./internal/gui (TestCodexAppStale goes red with the app not noted), the new GUI test in Chromium and WebKit, en zh zh-TW ja de, 900 and 480px; findCodexApp against the owner's running ChatGPT.app found its app-server; go vet darwin/linux(nogui)/windows, go build. |
||
|
|
61e296688e |
gateway: Codex's model list keeps codex-auto-review when the user picked models on the ChatGPT account, so auto-review runs on it again, not on the conversation's model at low (#1460)
10086gg reported that Codex's auto-review through magpie almost never used codex-auto-review. It ran on the conversation's own model at low effort, which costs more. Cause: codexModels narrows the ChatGPT backend's list to the models the user picked on the codex provider (CodexNativePicked). Hidden entries (visibility "hide") never reach magpie's catalog (parseCodexModels), so no one can pick them, and the narrowing always dropped them. codex-auto-review is one of those hidden entries. Codex 0.162 picks the reviewer this way (codex-rs/ext/guardian-reviewer/src/model.rs, select_review_model): 1. the entry's auto_review_model_override; 2. else codex-auto-review, if the list has it; 3. else the conversation's model, at low effort. So once the list was narrowed, every review fell to step 3. On a relay model it was charged to the relay; on a ChatGPT model it ran on that model. OpenCodex had the same bug: lidge-jun/opencodex#6712 recorded direct ChatGPT login as codex-auto-review/low and their proxy as Sol/low. Fix: the narrowing keeps hidden entries. No picker shows them, so the user's picks still decide what Codex's /model lists. A user who wants reviews on a relay model can still set Codex's auto-review model (#938), and that setting still wins (step 1). Which account pays changes for a user with picks whose conversation runs on a relay model: auto-review moves from the relay to the ChatGPT account's codex-auto-review. That is what Codex does without magpie. OpenAI's help article "Using Codex with your ChatGPT plan" says: "When you're signed in with a ChatGPT account, auto-review safety checks are free and don't count toward your plan's usage limits." New test TestCodexModelListPicksKeepHidden. It fails on the old code: after the picks only gpt-6-sol is listed, with no codex-auto-review. docs/subsystems/gateway-routing.md says this. Run: - gofmt -l internal/gateway - go vet -tags nogui ./internal/gateway, on darwin, linux and windows - go test -tags nogui ./... with a temp HOME and pinned caches: green Not run: a real Codex signed in to ChatGPT through the gateway. The reviewer choice was read from the Codex source. |
||
|
|
259c71078c |
docs: gateway routing says reasoning state goes through same-protocol translation as the vendor wrote it (#1445, XYenon)
What changed: gateway-routing.md gains the #1445 contract (sealed parts, the new IR events, each vendor's case, no cross-API envelope) and no longer says a translated Responses reply never carries sealed reasoning. Run: read against the code in the three commits before this one. |
||
|
|
d7186e51b9 |
gateway: Gemini's thoughtSignature stays on the call and text it signed through translation, and goes back instead of the skip value (#1445, XYenon)
What changed: parseGemini keeps a functionCall's and a text part's thoughtSignature. geminiParts and buildCodeAssistSent write them back; a later call of a signed step goes without one, and only an unsigned step or Claude on Antigravity gets skip_thought_signature_validator. The Code Assist decoder puts a call's signature in its id (signedID, as chat does since #687) and a text's in KTextSig; the Gemini encoder and renderer give them to a Gemini client where Gemini put them, with a clean id. Chat, Messages and Responses renderers leave a signature-only text part out. Why: a Gemini client on a Code Assist or translated path had every call's signature replaced by the skip value and text signatures dropped. Run: the new tests in reasoning_state_gemini_test.go (Gemini's documented thought-signature shapes) fail on the old code and pass now; a Chat client's call id carries the signature back to Google, and no other API's client gets the text's. gofmt -l; go vet -tags nogui on darwin, linux and windows; the full internal/gateway tests in a scratch HOME: ok. Not tried with a real account. |
||
|
|
13356b9da7 |
gateway: a Responses reasoning item goes through translation to Responses with its id, summary and encrypted_content, both ways (#1445, XYenon)
What changed: parseResponses keeps a reasoning item that has encrypted_content raw (Part.Sealed), and buildResponses sends it back as it came when it has no reasoning text and DeepSeek replay is off (sealedReasoning). The decoder says where an item begins (KThinkStart with its id) and hands over the done item (KSealed). The encoder streams the summary under the upstream's own id and gives the done item and the completed response's output as the upstream wrote them; renderResponses does the same for a reply that wasn't streamed. Why: the old code made its own rs_ id, dropped encrypted_content and left reasoning out of the next request, so a store:false conversation through a stream-only account or web search lost its reasoning each turn. Reasoning with text in it (another vendor's) is still left out for OpenAI (#1411); withoutBareReasoning and the foreign-reasoning retry still act on the built body. Run: the new tests in reasoning_state_responses_test.go (OpenAI's documented item and stream shapes) fail on the old code and pass now; the seal isn't handed to Messages, Chat or Gemini clients. gofmt -l; go vet -tags nogui on darwin, linux and windows; the full internal/gateway tests in a scratch HOME: ok. Not tried with a real account. |
||
|
|
64179e7383 |
gateway: Anthropic's redacted_thinking and a thinking block signed with no text go through translation to Messages as they came, each block apart (#1445, XYenon)
What changed: the IR carries a vendor's sealed reasoning (Part.Sealed,
SealedBy) and says where a thinking block begins (KThinkStart) and where a
sealed one is (KSealed). parseAnthropic keeps redacted_thinking raw and
buildAnthropic writes it back raw. decodeAnthropic, the Anthropic encoder,
the collector and renderAnthropic keep each thinking block apart, with its
own signature, including one signed with no text (aBlock still writes
"thinking": "",
|
||
|
|
e85be9d6f1 | readme: no stats strip under the hero screenshot (45+ agents, 4 wire APIs, 5 routing modes, 0 config files), in en and zh (the owner) | ||
|
|
f3d5d12ba3 | readme: the intent-routing and nested-routing screenshots are back under the Routing view, in en and zh (the owner wants them) | ||
|
|
793af76242 |
gui: Codex's subagents setting, unset, says the lead picks the model and only falls back to the main model, not just "same as model" (willz on Discord)
willz set Codex's "subagents" to 同主模型 (same as model), and a subagent
still ran on another model. Codex 0.162 lets the spawn_agent call's own
`model` argument win over `[agents] default_subagent_model`
(core/src/agent/child_config.rs:
requested_model.or(agent_default_subagent_model)). The default only
applies to a spawn that names no model, and the spawn_agent tool lists
up to 5 models the lead may name. So "same as model" was false whenever
the lead named a model.
This was reproduced with the real codex-cli 0.162.0 in a sandbox
HOME/CODEX_HOME, against a fake Responses server on port 3683 that logs
each request's model and x-openai-subagent header. The lead ran on
gpt-6.1-sol in every case:
a. lead names astra, default unset -> subagent on gpt-6-astra
b. lead names none, default unset -> subagent on gpt-6.1-sol
c. lead names astra, default sol -> subagent on gpt-6-astra
d. lead names none, default astra -> subagent on gpt-6-astra
magpie already holds every subagent to one model with the "subagent
model" setting (the gateway's codexSubagentPick,
|
||
|
|
338ea97909 |
provider: moved Cursor users get plugin 0.2.5: an account with a Grok Bot allowance shows it on its Usage card, and a model asked for by its bare name counts in its sized ids' pool (magpie-community/plugins#64, Morganwan90; Will on Discord)
Cursor's Grok Bot has an allowance apart from Cursor's monthly usage that only cursor.com's dashboard showed. Plugin 0.2.5 reads it from cursor.com/api/dashboard/get-sand-usage-status (the sign-in sent as the dashboard's WorkosCursorSessionToken cookie, as CodexBar does) and adds a "Grok Bot" window, or "Grok Bot (trial)": aside, so a spent Bot never stops Cursor requests, placed before Total so it is among the card's three rings. A missing or failed reading leaves Cursor's own windows as they were. 0.2.4 (not shipped to moved users until now) counts a model asked for by its bare name (grok-4.7) in the pool its sized ids (grok-4.7@256k) are in. The cursor mover's min goes from 0.2.3 to 0.2.5, so keepMovedCurrent updates every moved Cursor user. 0.2.5 on npm (registry.npmjs.org, latest) has an index.mjs byte-identical to plugins main 6bed2c1. The plugin PR was reviewed at 40d4df6 and merged: - bun test in packages/cursor: 73 pass, 0 fail; whole repo 1033 pass, 0 fail; bun scripts/check.mjs all 15 pass - botUsage broken 16 ways: 12 turn usage.test.mjs red; no timeout, no Origin, no res.ok check (the catch covers it) and an added bearer stay green - the request and parsing match CodexBar's CursorSandUsage.swift - not tried with a real Cursor account on the review side (the author saw the Bot ring on a Free account linked to SuperGrok) go test -tags nogui ./internal/provider passes (temp HOME, pinned caches). go vet -tags nogui ./internal/provider passes for darwin, linux and windows. gofmt -l is clean. No test pins a mover's min, as for 0.2.3. |
||
|
|
9bb2a467e4 |
agent: Snow CLI is an agent magpie sets up: a magpie profile in ~/.snow, made the active one, that Snow App takes in with "Sync Snow CLI API config" (#1457)
Snow CLI (npm snow-ai, MIT, MayDay-wpf/snow-cli) keeps one profile per file under ~/.snow/profiles (or $SNOW_CONFIG_DIR), names the active one in active-profile.json, and copies it over config.json at every start, headless `snow --ask` included. Each profile is the whole snowcfg: base URL, key, request method and models. Snow App (MayDay-wpf/snow-app) reads ~/.snow/profiles/*.json when "Sync Snow CLI API config" is pressed. magpie writes profiles/magpie.json from the profile the user was on (their settings kept, Snow's OAuth sign-in left out): baseUrlMode base at the gateway's /v1, requestMethod chat, the agent's key, the model as advancedModel and basicModel, and the catalog's window and output (200000 and 64000 when not known, the output cut to the window). It makes that profile active and copies it to config.json. The profile the user was on, whether there was an active-profile.json at all, and a magpie.json of their own are stashed; Disconnect, `magpie snow default`, or picking a model of one of the user's profiles puts those back. A Snow from before profiles (config.json alone) gets the default.json Snow itself would make, so its next start doesn't take magpie's config for the user's. Check reports a base URL, key or request method moved off magpie's; Sync rewrites the profile on a port move. A WSL distro's ~/.snow is found and written the same way. Registered: agents.go, the WSL kinds, SNOW_CONFIG_DIR in agentenv, the npm update spec (snow-ai), the icon (Snow App's own, trimmed to 96px), README, docs/reference.md, docs/subsystems/agent-wiring.md, the site's agent list and start.html (en, zh, ja). Run: - go test -tags nogui ./... in a scratch HOME: 44 packages ok. New: TestSnow, TestSnowOwnProfile, TestSnowNoProfiles, TestSnowUsersMagpieProfile, TestSnowCheckSync, TestAgentSnowDefault; snow added to TestOwnProviderNamedAsMagpies, the WSL more-agents tests and TestMaxTokensWithinContextWindow. All fail before this commit (no agent "snow"). - go vet -tags nogui for darwin, linux and windows; gofmt -l clean. - Real Snow CLI 0.9.2 in a sandbox HOME against a sandbox magpie (`magpie serve` on 127.0.0.1:3571, a fake OpenAI upstream on 3561): `magpie snow fake/pro` made the magpie profile active, `snow --ask` answered through the gateway (POST /v1/chat/completions, model pro, logged as agent snow), and `magpie snow default` left profiles, active-profile.json and config.json byte-identical to before. - GUI: agent-layout, agent-connect, agent-models-picker, agent-field-names and models-add-and-icons in Chromium and WebKit; a Snow CLI row checked at 520-960px in en, zh, zh-TW, ja and de in both engines. No new GUI strings. |
||
|
|
43cb6fe34e |
agent: Claude Code is told the window of Claude Desktop's models while Desktop is on magpie, so its Code tab no longer shows GPT 272K and Grok 256K as 200K (#1458)
Claude Desktop's Code tab runs Claude Code on ~/.claude/settings.json
and hands it the id Desktop lists a model by (mythos-magpie-<n>,
anthropic/magpie-<n>), with nothing of the model's window. Claude Code
2.1.293 (the one Desktop 2.31226.1 ships) gives an id it doesn't know
200000 unless the id has [1m] or CLAUDE_CODE_MAX_CONTEXT_TOKENS is set:
its catalog rows come from Anthropic's served catalog alone, and gateway
discovery keeps only id, name and description. Desktop sets that env
only for Cowork (yukon-silver-config contextWindowByModel), never for a
Code session, and reads the gateway's max_input_tokens only to add a
1M entry. magpie wrote the env from Claude Code's own main model alone,
and not at all on a [1m] or Claude main model, so GPT-6-Astra and Grok
4.7 ran as 200K. GLM 5.3 Flash is 1M, listed as [1m], so it ran at 1M
and showed magpie's auto-compact window, 300K.
While Claude Desktop is on magpie, the window written is now the least
of Claude Code's own model's and of the models Desktop is shown, leaving
out one listed by its 1M id (1M whatever the env says) and one named as a
Claude model Claude Code knows. Claude Code takes one value for every
model it doesn't know, so it is the least: never more than any of them
takes (an overflow is still turned into "prompt is too long" by the
gateway). A value the user set is still left alone. Sync writes it too,
so a model added or hidden for Desktop moves it, and turning
DesktopLongest on or off tells the agents again (catalog.Touched).
gateway.DesktopListed1M is the one rule desktopModels lists [1m] by, so
bb3b3f14's 1M label is unchanged.
Standalone Claude Code has the same limit: a magpie model picked in
/model runs on the one value; that is not changed beyond the Desktop
case.
Run:
- TestClaudeDesktopContextWindow, TestClaudeDesktopContextWindow1M fail
on the old claude.go/desktop.go ("" want 256000; "" want 1000000), and
each exclusion broken by hand turns them red.
- Real Claude Code 2.1.293 (from Desktop 2.31226.1's download), sandbox
HOME/CLAUDE_CONFIG_DIR, against a fake gateway on 3611, with the
settings.json magpie's agent code wrote (GPT 272K, Grok 256K, GLM 1M
under DesktopLongest, main on GLM [1m]): /context shows 256k for
GPT's and Grok's mythos-magpie ids (200k before), 272k for GLM's
[1m] id (the compact window).
- gofmt -l; go vet, GOOS=linux and GOOS=windows go vet -tags nogui on
internal/agent, internal/gateway, internal/gui; go test -tags nogui
./... under a temp HOME.
Not run: the Claude Desktop GUI itself.
|
||
|
|
d7a1b02032 |
redact: more kinds of personal data, each with its own switch: SSNs, passports, IBANs, birthdays, MAC addresses and serial numbers on by default; home-folder names, user@host, public IPs and cloud buckets off (lc on Discord)
lc asked in #feedback (message 1558300896142696549) for magpie's masking to cover more of what identifies a person or a machine. Under Settings > Privacy > Mask personal data, each kind now has its own Off/On (settings.redactKinds, redact.Options.Kinds). What each one masks: - US SSNs: dashed numbers, except ones never issued (000, 666 and 9xx areas) and well-known examples. Undashed ones only next to "SSN" or "social security". - Passport numbers, next to passport, Reisepass, 护照 or パスポート. - IBANs: compact or in groups of four, with the country's length and the mod-97 check. - Birthdays: a date only next to birthday, DOB, born, 生日 and the like. A date on its own stays. - MAC addresses, but not documentation examples or all-decimal groups. - Serial numbers, next to S/N, serial or 序列号. - The user name in /Users/x, /home/x and C:\Users\x. The name is masked and the path is kept. - User and host in a shell prompt or an ssh/scp/rsync target (user@host goes out as two placeholders, not as an email address), and on hostname/user lines whose value ends the line. Code such as `hostname = socket.gethostname()` is left alone. - Public IPv4 and IPv6 addresses. Private, loopback, link-local, CGNAT, documentation, multicast and well-known resolver addresses stay, and so do version numbers. - Cloud bucket names in s3://, gs://, oss://, cos://, abfss:// and storage URLs. The object path is kept. The last four are off by default. Agents send their working folder, the prompt and log lines in nearly every request, and the model then has to copy the placeholder exactly inside a path or URL. The first six are rare and sensitive, so they are on. Each placeholder is restored in replies, in streamed text and in tool-call JSON. Context rules run only in a window around their words (rule.matches), so masking with the defaults on 400KB of app.js takes 136ms (112ms before; 222ms with every kind on). Tests, run at this head: - gofmt -l is clean. go vet passes on darwin, GOOS=linux and GOOS=windows (-tags nogui) for internal/redact, gateway, gui and settings. - go test -tags nogui ./... under a temp HOME with pinned caches passed everywhere except internal/proc's TestNoCommandBypassesProc. That failure came from |
||
|
|
5b008f5a55 |
i18n: Chinese says 子代理 for subagent everywhere, not 子 agent in some places and 子代理 in others (willz on Discord)
willz asked what the difference between 子 agent and 子代理 is. There is
none: the zh and zh-TW GUI strings used 子 agent for the Agents page's
subagent row and subagent effort ("Subagents", "subagents",
"subagent effort" and its hint), and 子代理 for every other subagent
string (Codex subagents, routing's sealed subagent task, subagent model).
Both locales now say 子代理 throughout; zh-TW already used 子代理 for the
rest, so it takes the same term. The zh site docs (intent.html,
start.html) said 子 Agent / 主 Agent and now say 子代理 / 主代理, matching
the GUI's 主代理. ja already says サブエージェント everywhere; the READMEs
don't name subagents. The four GUI tests that expected 子 agent are
updated.
Run: omp-roles, profile-details, subagent-effort, tier-effort,
codex-subagent-model, routing-sealed-task, agent-tweak-fold,
claude-signin, codex-memories(-settings), gateway-mode, mobile-web,
usage-ledger-detail, gui-zh-tw, gui-ja, gui-de and i18n-keys in Chromium
(148/148) and WebKit (79/79); subagent-effort also at 420px wide in both
engines, with the zh screenshot checked (子代理推理强度).
|
||
|
|
9db2ed3dc6 |
gateway: temperature, top_p and top_k are left out for the Claudes that refuse them, so Haiku 5.5 no longer answers auto mode's classifier "temperature is deprecated for this model" (#1454)
Claude Code's auto mode classifier asks at temperature 0, and magpie's
own classifiers do too. Through the aihubmix relay, claude-haiku-5-5
answered 400 "`temperature` is deprecated for this model." (and the same
for `top_p` and `top_k`). magpie relayed sampling parameters as they
came, and built them from a chat request's temperature or top_p.
samplingAsTaken, beside thinkingOffAsTaken in forwardOnce, so relayed and
built requests and retries all go through it, leaves the three out for
the models Anthropic documents as refusing them:
- Opus 4.7, 4.8, 5 and 5.5, Fable 5 and Mythos 5 refuse them at any
value.
- Sonnet 5 and 5.5 refuse anything but the default.
- Haiku 5.5 takes only temperature 1 and top_p 0.99, never both, and no
top_k.
Left out, each model samples at its default. The model is read as the
vendor names it, from the body or, for Bedrock and Vertex, the path.
Older Claudes (Haiku 4.5, Sonnet 4.6, Opus 4.6 and before), other
vendors' models and later lines go byte for byte as they came. A Claude
under a name these rules can't read, answering that a sampling parameter
is deprecated or not supported (samplingDeprecated), is asked again
without them, once.
aihubmix itself took every value for the other models and refused it
only on Haiku 5.5. The other models are left out by the docs' rule, for
direct keys and relays that pass the vendor's 400 on.
Verified:
- Tests:
- TestSamplingAsTheModelTakesIt covers each refusing model's
spellings, a Bedrock path, and byte-identical bodies for Haiku 4.5,
Sonnet 4.6/4.5, Opus 4.6/4.1, Claude 3.7, Sonnet/Haiku 6, deepseek,
glm, kimi and gpt.
- TestHaikuFiveFiveSamplingOnTheWire uses a fake vendor answering
with the relay's literal 400s. It covers the classifier relayed,
top_p/top_k, magpie's own classifier built from chat, Haiku 4.5
keeping temperature 0, and an unreadable name retried once.
- Without the forwardOnce call, three wire cases FAIL. Without the
retry, the unreadable-name case FAILs.
- Live, through this build's `magpie serve` under a sandbox HOME with
the aihubmix key, in the classifier's shape (temperature 0, stop
</block>, thinking disabled), all 200:
- claude-haiku-5-5 at default and max effort and with thinking
omitted;
- claude-haiku-4-5, claude-sonnet-5-5, claude-opus-5-5 and
claude-sonnet-5;
- chat to claude-haiku-5-5 at temperature 0 and at top_p 0.5.
Straight to aihubmix, Haiku 5.5 at temperature 0, top_p 0.5 or
top_k 5 gives the 400.
- gofmt -l, GOOS=linux and GOOS=windows go vet -tags nogui
./internal/gateway, and go test -tags nogui ./internal/gateway under a
temp HOME with pinned caches.
|
||
|
|
4a3d7c6f04 |
gateway: an agent's thinking between_tools, Sonnet 5.5's thinking off, is read as thinking off on a model magpie translates for (#1454)
Sonnet 5.5 takes thinking turned off as {"type": "between_tools"}.
parseAnthropic read only "disabled" as off, so an agent that already
sends between_tools, routed to a model on another API (a group's
member, a fallback), had thinking neither off nor on, and was left to
that model's default. It is now read as ThinkOff, the same as disabled.
Verified: TestBetweenToolsReadAsThinkingOff fails without the change
("between_tools: ThinkOff = false"). gofmt -l, GOOS=linux and
GOOS=windows go vet -tags nogui ./internal/gateway, and go test -tags
nogui ./internal/gateway ./internal/agent all pass under a temp HOME
with pinned caches.
|
||
|
|
a9cb0ef988 |
gateway: Claude Sonnet 5.5 is sent thinking between_tools in place of disabled, and Opus 5.5, Fable and Mythos no thinking, so Claude Code's auto mode classifier no longer gets a 400 (#1454)
911Liam's auto mode, on claude-sonnet-5.5 through a relay, got 400 "To
turn thinking off on this model, send "thinking": {"type":
"between_tools"} instead of {"type": "disabled"}". Claude Code turns
thinking off for its classifier and side queries unless it knows the
model refuses that, and a model reached through a gateway can fall
outside what it knows. magpie forwarded the disabled as it came, and
turns thinking off itself too (an effort of none fitted, a budget that
doesn't fit, thinkingOffUnlessAsked for other vendors' models).
thinkingOffAsTaken, in forwardOnce next to adaptiveThinking, so that
relayed requests, count_tokens, magpie's own thinking-off and retries
all go through it, follows Anthropic's rules for each model. It reads
the model's name as the vendor gives it, from the body, or from the path
for Bedrock and Vertex.
- Sonnet 5.5: between_tools with no other field, and xhigh or max asked
as high (between_tools is a 400 above high).
- Opus 5.5, Fable 5.x and Mythos 5.x refuse disabled at every effort.
Thinking is left out, effort is low unless one was asked, and
classifierRoom is added to a reply limit below it, as Claude Code
itself does for these models.
- Opus 5 and Haiku 5.5: disabled is kept, with xhigh or max asked as
high.
- between_tools sent to any other Claude goes as disabled, or with no
thinking where disabled is refused too.
Other vendors' models, Sonnet 5, Opus 4.8 and older, and lines later than
these go byte for byte as they came. A Sonnet 5.5 under a name these
rules can't read is asked again with between_tools, once, on the 400
that asks for it (asksBetweenTools). A refusal of disabled at any effort
(thinkingOffRefused) is asked again with thinking left to the model,
beside alwaysThinks.
TestAdaptiveOnlyModelsNeverGetABudget's "disabled stays" case pinned
disabled for claude-opus-5.5, which the vendor refuses. It now checks an
older Claude keeps disabled and Opus 5.5 is sent none.
Verified:
- Tests:
- TestThinkingOffAsTheModelTakesIt and
TestThinkingOffLeavesOtherModelsAlone (deepseek, glm-5.3, kimi,
Sonnet 5, Opus 5 at high, Haiku 5.5, Opus 4.8 at max, Sonnet 4.6,
Haiku 4.5, Claude 3.7, dated Sonnet 5 and Sonnet 6 unchanged).
- TestClaudeFiveFiveThinkingOffOnTheWire: a fake vendor answers with
the reporter's literal 400, covering the classifier body,
count_tokens and the retry.
- Without the forwardOnce call, the wire test's Sonnet 5.5, Opus 5.5 and
count_tokens cases and the Opus 5.5 adaptive case FAIL. Without the
retry, its unreadable-name case FAILs.
- Live, through this build's `magpie serve` under a sandbox HOME with
the aihubmix relay key, in the classifier's shape (max_tokens 64,
temperature 0, stop </block>, thinking disabled):
- claude-sonnet-5-5 answers 200 at default and xhigh effort (sent
straight to aihubmix with disabled it gives the reporter's 400);
- count_tokens 200;
- claude-opus-5-5 200;
- claude-haiku-5-5 at max 200;
- claude-sonnet-5 200.
- gofmt -l, GOOS=linux and GOOS=windows go vet -tags nogui
./internal/gateway, and go test -tags nogui ./internal/gateway under a
temp HOME with pinned caches.
|
||
|
|
16539c2ea9 |
imagemcp: generate_video and the Magpie Image card name every video model the gateway makes videos with, not a Grok subscription alone (#1399)
shen-joe (#1399) wants images and videos from magpie in PI-Desktop and WorkBuddy. Neither app sends video generation to a custom provider: PI-Desktop has no video generation at all, and WorkBuddy's VideoGen/ImageGen post to copilot.tencent.com's /v2/videos|images/generations with the account token. So the way those agents get videos is Library → MCP → Magpie Image (`magpie mcp image`). Its generate_video tool told the agent it works only "with a Grok subscription signed in to Magpie (it has no other video model yet)", and the Library card said the same, while gateway.Videomakers also makes videos with a preset plan's videos (Volcengine Ark Agent Plan's Seedance), another magpie, and a custom provider whose model list marks video models (#1446). An agent reading that can tell a Seedance user they need Grok. - imagemcp.go: generate_video's description and the package doc name each kind of video model. - library/market.go: the Magpie Image card says "a video model set up in Magpie (Grok Imagine, Seedance)"; zh, zh-TW, ja and de follow. - Tests: TestVideoToolSaysEveryVideoModel and TestMagpieImageSaysVideoModels fail on the old text and pass now. Run: go test -tags nogui ./internal/imagemcp ./internal/library ./internal/gui ./internal/gateway (ok) and ./... (only TestNoCommandBypassesProc red, from |
||
|
|
f2611b7154 |
accounts: import ChatGPT accounts from Cockpit Tools' and Sub2API's exports, several files at once, from the window or magpie accounts import; the box says the source tool is signed out (#1453)
wlj521 asked for cockpit-tools' import: several auth.json files at once.
magpie's window already imported Codex's auth.json and CLIProxyAPI's
files; this brings it the rest of what Cockpit Tools exports and a command
for it.
What changed:
- login_import.go reads Sub2API's export ({accounts: [{platform, type,
credentials}]}), so every Cockpit Tools export format comes in (its own,
auth.json, CPA, Sub2API). An API key, another vendor's account and an
entry with only an access token (a web session: it can't be renewed and
would stop within days) are refused with the reason.
- ImportLogins and ImportGoogleAccounts no longer skip one file of several
that holds no account: it is a failed row with its place
(ImportedAccount.File), which the window and the command name by file.
- `magpie accounts import <codex|claude|antigravity|factory> <file>...
[--yes]`: says first what importing does to the tool the file came
from, imports nothing without a yes, only reads the files, and lists
each account's outcome.
- The window's import box names Cockpit Tools and Sub2API and says
plainly that the source tool is signed out of the account and the file
is only read. Claude's box no longer claims a refresh it doesn't do.
zh, zh-TW, ja and de for every new string; the retired strings removed.
- docs: reference.md and providers-accounts.md describe the import.
Decision: keep the refresh on import. It checks the file and makes magpie
the token's one holder at a moment the user chose. Storing the token
as-is, as Cockpit Tools does, only delays the rotation conflict and makes
which side gets signed out a race.
Run:
- go test -tags nogui ./... under a sandbox HOME (pinned caches)
- TestImportCockpitExports red on the old login_import.go (Sub2API rows
unread, the garbage file passed over); TestAccountsImport red with the
confirmation removed by hand; login-import.test.cjs red on the old app.js
- node --test login-import, factory-key, i18n-keys, gui-ja, gui-de,
gui-zh-tw (Chromium and WebKit; en and zh, results at 380px)
- gofmt -l; go vet, GOOS=linux and GOOS=windows go vet -tags nogui on .
and ./internal/provider; darwin, linux and windows builds
- no live ~/.codex: fixtures and a fake token endpoint only
|
||
|
|
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 |
||
|
|
b934d64a4e | tests: TestNoCommandBypassesProc leaves out internal/testenv, which only tests import and which can't import proc (its tests import testenv); main was red since 2da8fbe4's testenv.Bun ran bun with os/exec | ||
|
|
54675767f1 |
sessions: a folder draws its latest 100 sessions and a Show more for the next page, and opening a session draws only that session again (ChildhoodAndy on X)
With thousands of sessions the Sessions page drew every row of the open folder, 17,000 elements for 1,000 sessions, and drew them all again on switching to the page or to another agent, on each keystroke in the filter (every folder opens while filtering: 51,000 elements for 3,000) and on opening a session. Switching and scrolling stuttered. Now an open folder draws a page of 100 and a "Show N more" (data-unrolls) that adds the next page under the rows already drawn. A filter or another agent starts again at a page. A session opened from the Usage page is drawn however far down its folder it is. Opening a session replaces only that item and the one it closes. The folder box and the bar still pick every session, drawn or not. Measured with 3,000/2,500 sessions, before → after: - open the page: WebKit 449 → 159 ms, Chromium 312 → 165 ms - open a session: WebKit 287 → 67 ms - filter keystroke: WebKit 546 → 64 ms - elements drawn: 17,073 → 1,774 Run: sessions-manage-paging.test.cjs (new; Chromium + WebKit, en + zh, 1000px + 380px) and every sessions-*/session-* test, click-scroll and old-webkit, all green. |
||
|
|
ca3394ff80 |
docs: the community's plugins update by themselves every hour, not every six hours (en, zh, ja)
internal/plugin/updates.go's KeepUpdated first runs 30 seconds after magpie starts, then every updateEvery, which is time.Hour. #681 cut it from six hours. The Plugins page (plugins.js) already says "within the hour". site/public/docs/plugins.html, zh/ and ja/ still said six hours. Other "six hours" mentions were checked and left alone, since the code agrees with them: - the registry.json fetch on the same pages (market.go: 6*time.Hour); - the move retry in docs/subsystems/provider-plugins.md (migrate.go: moveRetry = 6*time.Hour). Bun's own check (bunEvery) is six hours, but no doc states it. No README or docs/ page states the plugin update interval. Run: grep across README*.md, docs/ and site/ for six hours / 6 hours / 六小时 / 6 小时 / 6 時間. No code changed. |
||
|
|
e5f04f38e8 |
plugin: a plugin on a pre-release whose release is out shows the update on its row, as the Plugins dot already did; versions are ordered as semver orders pre-releases, in Go and in the page
The Plugins dot comes from update.Newer, and the Installed rows (chip,
Update, Update all) from plugins.js's own newer(). They disagreed:
- newer() read only x.y.z, so 2.0.0-beta.1 with 2.0.0 out had the dot
and no row to update it from. Real case: opencode-antigravity-auth,
whose latest is 1.6.0 after 1.6.0-beta.0..2.
- update.Newer compared pre-releases as strings, so 1.0.0-beta.10 came
before 1.0.0-beta.9 (and 1.6.0-10 before 1.6.0-9): a missed update,
and the other way round, an "update" to an older beta.
- update.Newer didn't parse build metadata, so 1.0.1+build.1 was never
newer than anything.
Both now order as semver §11: no pre-release comes after any;
identifiers compared one by one, numbers as numbers, a number before a
word, and a shorter list before a longer one it starts. Build metadata
is ignored. A version that isn't x.y.z is never newer, as before.
Tests:
- internal/update TestNewer gains the pairs. On the old Newer 4 fail
(beta.10/beta.9 both ways, 1.6.0-10/-9, +build).
- internal/gui/tests/plugin-prerelease.test.cjs: seven installed
plugins, the dot's waiting list as update.Newer gives it, and each
row's chip and Update button, plus the Update all count, must match.
On the old plugins.js it fails at "v2.0.0 out" in both engines. No
strings change, so it's English only.
Reproduced in a sandbox HOME with the real package:
`magpie plugin add opencode-antigravity-auth@1.6.0-beta.2`, then Check
for updates. Before: /api/plugins/updates listed it, and the Plugins
nav had the dot, but the row had no chip and no Update, and there was no
Update all. After: the row shows "v1.6.0 out", Update, and Update all (1).
Update all then installed 1.6.0 and the waiting list emptied.
Run: gofmt; go vet -tags nogui for darwin, linux and windows on
internal/update, internal/plugin, internal/gui and the root package; go
test -tags nogui of the root package, internal/{gui,plugin,provider,
stats,update} under a temp HOME; GUI tests plugin-prerelease,
plugin-updates, plugin-card-update, plugin-check-updates, plugin-market,
plugin-release, plugin-git and plugin-local in Chromium and WebKit.
|
||
|
|
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. |
||
|
|
47a313f618 |
library: Pi's MCP servers go to ~/.pi/agent/mcp.json, the file Pi 0.99+ loads, unless pi-mcp-adapter is really loaded; an adapter left installed but not listed no longer sends them to mcp-adapter.json (sydney on Discord)
sydney reported that the Library wrote Pi's MCP servers to ~/.pi/agent/mcp-adapter.json while Pi 1.0 loads ~/.pi/agent/mcp.json. Pi's own source (npm @earendil-works/pi-coding-agent): - 0.99.0 to 1.1.0 have built-in MCP: dist/extensions/mcp/config.js loadMcpConfig reads <agentDir>/mcp.json (and a trusted project's .pi/mcp.json). An extension registering /mcp (pi-mcp-adapter) replaces it; "extensions": ["-builtin:mcp"] turns it off. - 0.87.1 and earlier have no MCP; pi-mcp-adapter 3.x/4.x reads mcp-adapter.json only, adapter 5.0.0+ also reads Pi's mcp.json on a Pi with registerMcpServer. - core/package-manager.js resolve() loads only settings.json "packages", "extensions" entries and <agentDir>/extensions/. npm/node_modules, git/ and the global npm root are install places, not loaded on their own. magpie counted any pi-mcp-adapter folder (npm/node_modules, git, global root) as loaded, so an adapter left behind with "packages": [] made it write mcp-adapter.json, which nothing reads, and move mcp.json away (heliar-k's #1097 is the same case). It also fell back to the adapter file when Pi's version was unknown. Now: - piDetect follows Pi's loader: a package is loaded only when listed (an object entry with "extensions": [] loads none), "-"/"!" entries turn one off, and <agentDir>/extensions/<name> loads; the other folders only give the version. - An unknown Pi version is taken as one with built-in MCP. - With no adapter loaded, servers in mcp-adapter.json are moved into mcp.json (both backed up first); nothing in either file is dropped. - With adapter 5+ loaded on a Pi with built-in MCP, mcp.json is left where it is (the adapter reads it) and new servers go to the adapter's file; with adapter 3/4, or on Pi before 0.99, the old move is kept. Tests: TestPiMCPUnloadedAdapter, TestPiDetectLoaded and TestPiMCPAdapter5KeepsPiFile fail on the old pimcp.go and pass now; older tests that relied on an unlisted adapter now list it. Run: gofmt -l; GOOS=linux and GOOS=windows go vet -tags nogui ./internal/library/; go test -tags nogui ./internal/library/ ./internal/agent/ under a temp HOME. Real Pi in a sandbox HOME: - Pi 1.0.4, reporter's layout (adapter 5.2.0 in npm/node_modules, "packages": []): `magpie library mcp add` wrote mcp.json and `pi mcp list` shows both the user's server and the new one connected. The old binary wrote mcp-adapter.json and Pi listed no servers. `magpie library sync` with pi off PATH (version unknown) moved that home's mcp-adapter.json back into mcp.json, with backups, and Pi connected both. - Pi 1.0.4 with `pi install npm:pi-mcp-adapter@5.2.0`: mcp.json is kept, the new server goes to mcp-adapter.json, and `pi-mcp-adapter doctor` connects both. - Pi 0.87.1: still mcp-adapter.json, as before. Not run: Windows or WSL Pi, project-level .pi/settings.json, a real interactive Pi session. Reported by sydney on Discord; same cause as #1097 (heliar-k). |
||
|
|
666c0fa4fd |
routing: the context window holds still while requests stream in, and the prompt-cache card shows the last request's hit, dimmed, or a dash with the reason instead of "——命中" and an empty bar (dumplings on Discord)
dumplings reported that on Routing, every new request made the context
window jitter and flash, and that the prompt-cache card read "——命中",
glued together, with an empty bar under it.
Causes, measured with a frame sampler while requests stream through
start/read/done:
- A live request's story is shorter than a finished one's. "Is
answering…" becomes "answered in 4.0 s…", which wraps onto two lines
at phone width. The log head also gains View usage and Replay once the
request is done. So the card under it moved up and back down on every
request: 45px at 420px wide, 6px at 900px.
- The state pill ("Live" or "Counted") changed width, which moved the
model name beside it by about 11px.
- While a request was under way the cache card dropped to "—" with a
0% bar, then came back. That was the flash.
- A flex item's leading space is dropped, so " hit" and " / 272K" were
glued to the figure before them.
Fix:
- routing.js holdStory keeps the log head and steps at the tallest
height seen while following live, at this width and language. A
picked request, a replay or a past day is drawn at its own height. A
ResizeObserver on the steps measures again as soon as the width
changes. Without it, a scrollbar appearing before the first request
left a stale height and a 6px shift (seen in WebKit under load).
- context.js stacks all three state words in one grid cell and shows
only the current one, so the pill keeps one width.
- While the next request is under way, the cache card keeps the last
request's figure, dimmed (.was), with "The last request's · this one
is on its way". When there is nothing to show, it gives a faint "—"
and the reason ("Read once the vendor answers" or "The vendor didn't
say"). The bar stays hidden but keeps its space. The remembered
figure resets when the agent or session changes.
- .ctx-big gets a gap. Width and opacity transitions are turned off
under reduced motion.
- The three new strings are added in zh, zh-TW, ja and de.
Verified:
- The new tests/routing-ctx-still.test.cjs streams two requests and
checks that the card, model, state, used, cache, figure, bar, grid,
legend and foot rects never move by more than 0.5px. It also checks
the figure-to-word gap and both empty states.
- It runs in Chromium and WebKit, at en 420/1100, zh 420/1100,
zh-TW 900, ja 900 and de 420. It narrows the viewport by 10px after
load.
- On the old assets it fails 16 of 16 (the card moves 6–52px). It also
fails with only the ResizeObserver removed.
- routing-live-patch now expects the held figure ("97%") on live steps
instead of "—".
- Under --test-concurrency=4 in both engines: 578/578 pass across
routing-*, context-*, click-scroll, group-context, panel-routing and
gui-de/ja/zh-tw.
- go test -tags nogui ./internal/gui passes.
- GOOS=linux and GOOS=windows go vet -tags nogui ./internal/gui are
clean.
|
||
|
|
e3bd766fae |
providers: a saved provider's editor keeps the add form's Fetch models button, "Fetch models again" once a list was fetched, to re-fetch after a key moves to another group (耍赖天都爱 on Discord)
耍赖天都爱 on Discord: 「provider中模型拉取一次就没有这个按钮了 能不能加上 有
的provider换一个分组要从新拉取模型」. The add form offers "Fetch models"
(获取模型); once saved, the editor's same action was a generic "Refresh"
(刷新) in the models footer, so after the first fetch the button looked
gone, while a relay key moved to another group serves another list.
The editor's button now says "Fetch models", and "Fetch models again"
(重新获取模型) when a vendor list was fetched before. Its title says it asks
with the key typed here and what happens to the picks. The fetch itself is
unchanged: it sends the typed form (asTyped), and Refetch replaces the list,
drops picks the new list no longer has and keeps ids added by hand. The
hints that named Refresh ("from models.dev · …", "magpie's list · …",
"Jev's names · …", Forget's title, the empty-list hint) name the button now,
in zh, zh-TW, ja and de. The TUI's "m fetch models" and `magpie provider
refresh` already re-fetch at any time; the CLI help names the app button.
Verified:
- new internal/gui/tests/provider-fetch-again.test.cjs (Chromium + WebKit;
en and zh at 900 and 420px, zh-TW and ja at 900, de at 420): a never
fetched relay shows Fetch models and its hint names it; a fetched one
shows Fetch models again; with group B's key typed it asks with that key,
shows group B's list, drops the pick B lacks, keeps an id added by hand,
the button is still there and asks again; nothing scrolls. On the old
app.js/i18n.js it fails: "waiting for ... getByRole('button', { name:
'Fetch models', exact: true }) to be visible" (en), "again has its zh".
- provider-typed, provider-picks-stay (labels updated), gui-zh-tw, gui-ja,
gui-de, click-scroll: 51 pass, 0 fail, both engines.
- go test -tags nogui . ./internal/tui ./internal/gui under a temp HOME: ok.
go vet -tags nogui . ./internal/tui, GOOS=linux and GOOS=windows: ok.
|
||
|
|
65dcc57369 |
tests: the root package's plugin tests run the bun on PATH once before their one-minute context, so a bun that has never run here doesn't time them out (ggbdpq, #1306)
TestAccountsOfAPlugin and the plugin login tests (fakeCoHome) point
MAGPIE_BUN at the bun found on PATH and do all their plugin work inside
a one-minute context.
|
||
|
|
e611fc65d6 |
codex: magpie restarts the codex app-server a change left on the old model list when no codex session is on it, and the Agents page offers Restart when one is (luci on Discord)
luci (Codex CLI 0.162, magpie 0.1.1150): after Codex was wired back to
magpie, codex in a new terminal listed Codex's own models although
config.toml was magpie's.
Cause, reproduced with real codex 0.162.0 in a sandbox CODEX_HOME:
codex sessions attach to a background app-server (the managed daemon)
that is per CODEX_HOME, not per terminal (socket under
$CODEX_HOME/app-server-control). It is detached, has no idle exit and
builds its model list once, at start. A daemon started while Codex was
off magpie keeps the official list for every later session, in this
terminal or a new one, until it restarts. magpie writes only PATH to
shell rc files and doesn't unwire Codex on quit, so nothing
per-terminal is involved. luci's "a new terminal fixed it" isn't
explained by the daemon. Codex's hourly updater restarting it is a
guess, not proven.
What changed:
- provider.KeepCodexDaemonCurrent(ctx, home, changed) compares the
daemon's start time with when magpie last changed what Codex reads at
start. The start time is the mtime of a pid file in that home that
names the running daemon's pid, so another home's daemon (ChatGPT.app,
other sandboxes) is never acted on.
- With no codex session attached, it runs
`codex app-server daemon restart` with CODEX_HOME set to that home.
- Otherwise it reports Behind and how many sessions are attached.
- isCodexClient leaves out apps' and editors' own codex, exec-server,
mcp-server and the daemon's own processes. It reads the whole ps line,
since ps leaves a path's spaces unquoted ("Codex Computer
Use.app/...").
- agent: Codex's Notice after a change runs this check first. It says
either that magpie restarted the daemon, or that the daemon is behind
and how to restart it (Agents page or the command).
- The GUI backend and `magpie serve` re-check every 15s. A session that
was open at the change is usually closed later, and that is when the
daemon can be restarted.
- Agents page: the stale "daemon" copy line has a Restart button that
POSTs /api/codex/daemon/restart (timeout now 2 min, as a running turn
is let finish). It reuses translated strings. On Windows, where
processes aren't listed on each look, the row shows the last check.
- Restarts are serialized by one mutex.
- WSL's Codex is not handled automatically. Its row keeps the advice
text, which names the restart command.
What was run:
- Real codex 0.162.0 in a sandbox CODEX_HOME, with a fake gateway, on
luci's sequence: the daemon started on the official config, then
magpie's config and catalog were written.
- With a TUI open, /model listed GPT-6.x. The check said Behind with
a session attached and did not restart.
- With the TUI quit, the check restarted the daemon. A new codex in
the same terminal listed Magpie Alpha and Beta.
- Earlier in the sandbox: a daemon restart under an idle attached 0.162
TUI reconnects, and /model then shows magpie's models. A running turn
is waited for up to 60s.
- New tests:
- TestKeepCodexDaemonCurrent(Fails), TestIsCodexClient (with the
space-in-path case from the real process list) and
TestCodexNoticeKeepsTheDaemonCurrent. They don't build or fail on
the old code. The space case failed before the ps-line fix.
- The GUI test "the daemon's line restarts it" times out on the old
app.js.
- Results:
- agent-stale-copies.test.cjs: 60/60 in Chromium and WebKit, in en,
zh, zh-TW, ja and de, at widths 980 and 440.
- go test (temp HOME) for ./internal/agent, ./internal/provider,
./internal/gui and the root package: ok.
- gofmt -l: clean.
- GOOS=linux and GOOS=windows go vet -tags nogui: ok.
- The Windows path is checked only by vet and not tried on a Windows
box.
|
||
|
|
bb3b3f1486 |
gateway: with "only each model's largest window" on, Claude Desktop shows a model of 1M or more as its 1M entry, "1M context window", not as what looked like the plain model (杰多夫 on Discord)
The cause: under settings.DesktopLongest, /v1/models lists such a model by its "[1m]" id alone (#1272). Claude Desktop 2.7032 (app.asar, read only) marks an entry as the 1M one only on the entry it adds itself: JLt emits `${id}[1m]` with description WLt = "1M context window" and supports_1m_context, and only for a model with supports1m. PIt gives supports1m to a bare id alone (LC(id) === id), and OIt folds a "[1m]" row only into a bare one listed beside it. So the lone "[1m]" row was shown by its name with no description, exactly like the plain entry it replaced: with the switch off the menu had "DeepSeek V4 Pro" plus "DeepSeek V4 Pro · 1M context window"; on, only "DeepSeek V4 Pro". The reporter read that as the switch keeping the plain model and dropping the 1M one. What changed: such a row now carries description "1M context window", the words Desktop gives its own 1M entry, and Desktop shows a row's description (PIt keeps it; JLt prefers it to its fallback). The id is unchanged: the "[1m]" is what gives a Claude Code session the 1M window, and magpie serves it as the plain model. Rows under 1M, and every row with the switch off, are unchanged (no description). What was run: - TestClaudeDesktopLongestOnly now checks each row's description, on and off. With the description line removed it fails: "on: vend/deepseek-v4-pro (...[1m]) described "", want "1M context window"". - Sandbox HOME gateway on 127.0.0.1:3561 with real providers, /v1/models as Claude Desktop asks for it, fed through a script that follows PIt/OIt/JLt: on, each 1M model is one entry "<name> · 1M context window"; off, the plain entry plus Desktop's own 1M one, as before. - gofmt -l; GOOS=linux and GOOS=windows go vet -tags nogui ./internal/gateway; go test -tags nogui ./internal/gateway under a temp HOME: ok. No GUI or string change. |
||
|
|
d9bf7c1ccf |
zcode: the API format the user picks for magpie's provider in ZCode's settings stays theirs; a sync no longer puts Anthropic Messages back on every start (杰多夫 on Discord)
What changed: zcodeRules wrote api.type "anthropic-messages" into provider_config.json, and zcodeProviderJSON wrote kind "anthropic" into config.json, on every sync (magpie's start, any catalog change). A user who switched the provider to OpenAI Chat Completions or Responses in ZCode had it switched back the next time magpie started. zcodeFormat now reads the format already there and keeps it, in both files: provider_config.json's api.type (ZCode 3.14 on) or config.json's kind (older ZCode), the one written last where they differ, mapped as ZCode maps them (anthropic-messages/anthropic, openai-chat-completions/openai-compatible, openai-responses/openai). A new provider, or a format ZCode doesn't have, is Anthropic Messages as before. Why it's safe: magpie's gateway answers all three at the same base address. Checked with ZCode 0.16.9's own agent CLI (glm/zcode.cjs -p, run from a copy under a sandbox HOME, never ~/.zcode) against a sandbox gateway with the DeepSeek provider: anthropic-messages asks /v1/messages, openai-chat-completions /chat/completions, openai-responses /responses, and a tool-using turn (Read a.txt) answered 200 on each, to deepseek-flash. What was run: - TestZCodeKeepsUserFormat (new): a format picked in provider_config.json, for each of the three, survives two syncs and is carried to config.json's kind; one picked in config.json is carried to provider_config.json; an unknown one isn't kept. On the old zcode.go it fails: "picked openai-chat-completions: kind "anthropic", api "anthropic-messages"". - Sandbox, real binaries: provider_config.json set to openai-responses, then `magpie zcode magpie`: the old build wrote anthropic-messages back, this one kept openai-responses (config.json kind openai). Then openai-chat-completions, resynced, and the real ZCode CLI ran a turn on the file magpie wrote, served over /chat/completions. - gofmt -l; GOOS=linux and GOOS=windows go vet -tags nogui ./internal/agent; go test -tags nogui ./internal/agent under a temp HOME: ok. The "bcww: Field required" 400 on Anthropic Messages is not part of this commit: ZCode's real requests (captured from its CLI, plain and with web search) to deepseek/deepseek-flash through magpie returned 200, and DeepSeek answers schema errors in serde's words, not "Field required". |
||
|
|
eb035e29e1 |
sessions: a session seen only through the gateway can be deleted: its row and recorded conversation go to magpie's trash, and Restore brings them back (lc on Discord)
lc ("只可列出?不可删除?agent claude code") saw Claude Code sessions named by
uuid under "No folder" at "0 B", with "These Claude Code sessions are read
only; magpie can list them, but cannot resume or delete them." Those rows are
gateway projections from the usage ledger: Claude Code ran with its own files
somewhere magpie can't see (another computer, a container, another config
folder), so nothing native won on the same agent+id. An agent with only such
rows was sent as not deletable, and every row was read_only, so the page had
no Delete at all.
What changed:
- sessions.DeleteGateway moves the session's recorded text folders
(gateway-conversations/<date>/<identity hash>) into
trash/sessions/gateway/<ts>-<hash>/files/<date> with a note marked gateway,
and records {agent, id, last} in gateway-sessions-hidden.json.
/api/sessions and /api/sessions/manage leave it out up to that last
request; a newer request lists it again. The usage ledger is untouched, so
Usage, /api/sessions/one and stats keep what it spent. A session with a
request in the last minute is refused as active.
- Restore moves the text back (skipping what expired or was cleared, refusing
rather than overwriting a same-day re-recording), checks each item is this
session's identity folder inside the store, and unhides it. Delete forever
keeps it hidden.
- The recording's promises hold in the trash: the hourly prune erases
trashed gateway text past 7 days, "Delete saved conversations…" erases it
too, and HasGatewayConversations counts it.
- The manage route marks an agent with gateway rows deletable and sends
those rows deletable, with gateway: true and the size of their recorded
text; the delete route sends gateway ids to DeleteGateway and native ones
to Delete as before.
- Sessions page: gateway rows group under "Seen through the gateway"
instead of "No folder", show "{size} recorded" only when something was
recorded (no "0 B"), and the note says why they can't be resumed and what
Delete takes and keeps. The delete dialog says the same for gateway rows
instead of "Their files are moved…"; the Trash marks them and notes the
7-day expiry. New strings in zh, zh-TW, ja and de.
- docs/subsystems/gateway-sessions.md describes the delete, hidden list,
restore, expiry and clear.
Verified:
- go test -tags nogui ./... (sandbox HOME) green; gofmt -l clean; go vet
./...; GOOS=linux and GOOS=windows go vet -tags nogui ./internal/sessions
./internal/gui; go build, GOOS=linux -tags nogui and GOOS=windows builds.
- TestSessionsDeleteGatewaySession fails on the old code with "Claude Code
with only gateway sessions isn't deletable: [{Agent:claude
Deletable:false}]"; sessions-gateway-delete.test.cjs fails there too.
TestGatewayDeleteTrashExpires, TestGatewayPurgeKeepsItHidden,
TestGatewayDeleteNothingRecorded and TestGatewayRestoreRefusesForeignItems
cover expiry, purge, an unrecorded session and a forged note.
- GUI: sessions-gateway-delete.test.cjs in Chromium and WebKit, en, zh,
zh-TW, ja, de, at 1000 and 390px (in-app dialog, Cancel sends nothing,
click doesn't scroll, no left-border accents, no page overflow); all
sessions-*, gateway-conversations, checkbox-look, old-webkit,
sess-agent-strip, i18n-keys, gui-ja, gui-de and gui-zh-tw tests pass.
- Real try: magpie web from this tree on a sandbox HOME seeded with three
gateway-only Claude Code sessions (two recorded): deleted a recorded and an
unrecorded one, checked the store, trash and hidden list on disk, restored
the recorded one (its transcript reads again), purged the other (stays
hidden). That try found the Trash page breaking on an unrecorded session's
null items; DeleteGateway now writes [] (TestGatewayDeleteNothingRecorded
fails without it) and the page reads a missing list as empty.
|
||
|
|
0d5fdbb2de |
provider: moved Cursor users get plugin 0.2.3: where a proxy or network blocks HTTP/2 ("h2 is not supported"), a Cursor chat goes over HTTP/1.1 as RunSSE + BidiAppend on api2 instead of failing (magpie-community/plugins#59, kazecreator)
Behind TLS inspection or a network that drops HTTP/2, every moved Cursor chat failed with 502 "Cursor: h2 is not supported" or never answered, while sign-in, models and usage worked. 0.2.3 falls back to Cursor's own HTTP/1.1 transport; a failure that says HTTP/2 itself can't be had keeps later Runs on HTTP/1.1 for 10 minutes. The cursor mover's min goes from 0.2.2 to 0.2.3, so keepMovedCurrent updates every moved Cursor user. 0.2.3 on npm (registry.npmjs.org) has an index.mjs byte-identical to plugins main 5b67ff4. The plugin PR was reviewed at f5c11b7 and merged: - bun test in packages/cursor: 66 pass, 0 fail, three runs - whole repo: 1026 pass, 0 fail; bun scripts/check.mjs all 15 pass - each guarded piece broken by hand turns http1.test.mjs red - not tried with a real Cursor account on the review side go test -tags nogui ./internal/provider passes (temp HOME, pinned caches). go vet -tags nogui ./internal/provider passes for darwin, linux and windows. gofmt -l is clean. |
||
|
|
83c963c466 |
gateway: a gateway MAGPIE_ADDR puts on the network asks callers from other machines, a container's host among them, for an enabled gateway key, as sharing does
What changed - lanGuard: a request whose peer isn't loopback (or that carries a forwarding header) is remote. While the gateway is shared, or MAGPIE_ADDR names a host other than loopback/localhost (OnNetwork: 0.0.0.0 in the Docker image, a server's address), a remote request is answered only with an enabled gateway key, on every route behind the guard; otherwise it is refused (403) as before. Before, a MAGPIE_ADDR on the network with sharing off took remote requests with any key. A remote request let in with a key is marked as shared-mode ones are, so quotas, quota cards, refresh and prices answer it. callerGuard follows the same condition. OpenToAnyone is replaced by OnNetwork. - Loopback is unchanged: any key still works on the gateway's own machine and inside the container (HEALTHCHECK included). - CLI keyNote and the console's Connect note no longer describe an open gateway; on the network they say other machines need a gateway key (`magpie gateway-key add <name>`). The console shows the Gateway keys block and network addresses when OnNetwork (gatewayJSON.onNetwork replaces open); the "Open to the network" strings are removed. - Docs: docker.html (en/zh/ja) and remote.html (en/zh/ja) say callers outside the container need a gateway key, how to make one, that sharing isn't needed for it, and what an upgrade means for agents that sent `magpie`; docs/reference.md and providers-accounts.md likewise. A MAGPIE_ADDR pinned to one interface's address now asks a key of this computer's agents calling that address too (documented). Tests - New TestNetworkGatewayGuardsEveryRoute: from 172.17.0.1 and 192.168.1.9 every registered route (model APIs, count_tokens, systemone, the Responses websocket GET, MCP, Codex backend and catalog, /v1/magpie/*, images, embeddings, rerank, videos, Gemini) gets 401 without a valid key; with one, models and quotas answer 200. - TestNetworkGatewayNeedsAKey (was TestExplicitOpenGatewayAccepts...), TestLANGuard, TestProxiedLoopbackNeedsTheKey, TestQuotasOverLAN, TestMCPProxyStaysHere, session route and remote prices tests, TestOnNetwork, serve_cli and gui public_url tests updated. With the old lanGuard restored, TestNetworkGatewayNeedsAKey, TestNetworkGatewayGuardsEveryRoute, TestLANGuard and TestProxiedLoopbackNeedsTheKey fail. Run - go test -tags nogui -count=1 ./internal/gateway ./internal/gui ./internal/provider . under a temp HOME: ok. - gofmt -l clean; GOOS=linux and GOOS=windows go vet -tags nogui on those packages: clean. - GUI: public-url and gateway-caller-keys tests, Chromium and WebKit: 22/22. - Locally built image (OrbStack), -p 127.0.0.1:3481:3425: from the host, /v1/models with no key, `magpie` and another made-up key -> 401; count_tokens and /mcp/x -> 401; a key from `docker exec ... magpie gateway-key add Host` -> models 200, quotas 200; inside the container wget /v1/models -> 200; healthcheck exits 0. `web --addr 0.0.0.0:3430`: /api/state 401 without the web key, 200 with its cookie. |
||
|
|
c4476d5658 |
library: skills from GitHub are checked as the skills page opens, and one changed here is marked and asked about before an update replaces it (#1449)
YangZhiqiang98 asked (#1449) that the Library find which skills have updates and update them. magpie already checked skills from GitHub (a market install among them) by hand and could update them, but an update deleted the library's folder, so a skill the user had changed (by hand, through an agent's link, or in an agent's copy not yet taken in) was lost without a word, and nothing checked unless the user pressed Check. - A skill is "changed here" when its library folder no longer hashes to what was fetched (Finder's .DS_Store aside), or an agent's copy was edited since its mark and not yet taken in (editState, skill_changed.go). The view says so (SkillView.edited) and the row shows "Changed here". - UpdateSkill refuses such a skill with *EditedError unless told to replace it. The row's Update asks in the page first (Keep mine / Update; Update sends replace). Update N, the group's Update N and the CLI's update-all leave it as it is and say so. The CLI takes `magpie library skill update <name> --replace-edits`. - The version an update replaces is moved to the backups when it was changed, or when it can't be told (a skill with no hash from before). - View.checkDue: no check since magpie started, or none for 12 hours. The page then runs the check once by itself as the skills open, and says only what there is to update or add. Not checked, by design: skills the skills CLI manages (`npx skills update` updates them; magpie only groups them), linked local folders, skills imported from an agent with no source, and From-only skills have no remote; CC Switch-installed skills can still be updated by hand. Verified: - go test -tags nogui ./... (sandbox HOME); new TestUpdateKeepsASkillChangedHere, TestUpdateKeepsACopyEditedInAnAgent, TestUpdateKeepsWhatItCantTell, TestCheckDue, TestCheckSkillsMarksItDone, all from httptest. With editState forced to "not changed" the first three fail. - gofmt -l, go vet, GOOS=linux and GOOS=windows go vet -tags nogui on ./internal/library ./internal/gui ./internal/tui . - GUI: new skill-update-edited.test.cjs (Chromium and WebKit, 860 and 400px, en and zh; no scroll on click; fails with the row's ask removed), plus the 22 other library skill tests, gui-ja, gui-de, gui-zh-tw, i18n-keys and click-scroll: 262 pass. - Real use in a sandbox HOME with github.com/anthropics/skills: installed pdf and docx, the real check said both current; an edit through Claude Code's link was refused by `skill update pdf`, skipped by `skill update` (docx updated), replaced by `--replace-edits` with the edit in backups/<time>/library/skills/pdf; docx edited later shows edited, a .DS_Store doesn't. |
||
|
|
d86a4e0568 |
site: the docs have Docker and Remote magpie pages in English, Chinese and Japanese, listed in every docs page's contents and footer and linked from the READMEs (HuanCheng Bai on Discord)
Until now, both were only in docs/reference.md. The new pages are at
/docs/docker, /docs/remote and their zh/ and ja/ versions.
- docker: the image, its tags, ports, the /config volume and the nonroot
user. How to run it with the browser UI (`web --addr 0.0.0.0:3430
--no-open`, MAGPIE_WEB_KEY, the ?k= cookie). Signing in through the
Callback URL paste or `docker exec ... magpie accounts add`. What
/config keeps, the env vars, sharing with gateway keys, and a Compose
file for a NAS. Healthcheck and updates.
- remote: sharing the main magpie, then adding it as a Remote magpie
from the app, the TUI (a/w/m) or `magpie provider add remote-magpie
<key> url=... id=office`. What comes over (models, groups, APIs,
images, System One, embeddings, prices, quotas) and the errors magpie
shows, quoted from remote_quotas.go and lan.go.
The facts were checked against the code, not reference.md:
- the Dockerfile ENV, VOLUME, HEALTHCHECK and EXPOSE;
- docker.yml's tags;
- web_cli.go;
- the lanGuard in internal/gateway/lan.go;
- the remote-magpie preset.
The Docker page's warning says a Docker gateway answers anyone until
sharing is on, because MAGPIE_ADDR is 0.0.0.0 there.
The new links in the top nav bar carry a toc-only class:
- docs.js still lists them in the contents;
- the bar keeps today's width, since nine links overflowed it at
1024-1280px.
`.steps li` gets min-width: 0, so a long inline code no longer widens
the grid past a phone's width, where `overflow: hidden` clipped the
text.
Run:
- Real containers from ghcr.io/yetone/magpie:latest (v0.1.1150):
- the documented `docker run ... web` came up healthy, and its log
output is what the page shows;
- with "lan": true, `Bearer magpie` got 401 and a gateway key got 200;
- a second container ran `provider add remote-magpie <key>
url=http://<a>:3425 id=office` and listed office/test-relay/... models.
- `node --test site/*.test.mjs`: 23 pass.
- Playwright, in Chromium and WebKit, at 1280, 1024 and 390, light and
dark:
- no horizontal scroll and no nav overflow on all 20 docs pages;
- every internal link and #anchor resolves. /zh/ and /ja/ are routed
by the worker and return 200 live.
|
||
|
|
c65d9fb473 |
codex: magpie-models.json keeps the keys the user added by hand to a model, and says supports_reasoning_summaries itself, so Codex before 0.145 sends the effort and shows thinking (heng91, #1450)
What was wrong: every write of ~/.codex/magpie-models.json rendered the catalog from magpie's models alone, so a key the user had put into an entry was gone after the next model switch, catalog sync (Refresh, a provider added, a gateway start) or a switch to Codex's own default, which also deleted the file outright. The key heng91 added, supports_reasoning_summaries, is one Codex before 0.145 needs: its client.rs build_reasoning sends no `reasoning` (no effort, no summary, so no thinking shown) for a model whose entry doesn't say true, until openai/codex#32206 (first in 0.145). 0.144 also refuses to load a catalog an entry of which lacks the key at all. What changed: - codexcat: every entry says supports_reasoning_summaries, true for a model with effort levels, false otherwise. Later Codex ignore it. - codexcat.Keep: before a write, the keys of an entry on disk magpie doesn't write are carried over to the entry of the same slug, and a true of supports_reasoning_summaries stays over magpie's false. A key magpie writes keeps magpie's value; a model magpie no longer serves drops out; an unreadable file keeps nothing. - agent/codex.go: the write when magpie becomes the provider and the catalog sync go through Keep; the three places that removed the file remove it only when nothing of the user's is in it. Run: - New tests TestCodexCatalogKeepsWhatTheUserAdded, TestCodexCatalogRemovedWhenNothingAdded (agent, from the file magpie writes, edited by hand) and TestCodexCatalogReasoningSummaries, TestKeepUserKeys (codexcat). Each fails on origin/main's code. - Real Codex in a sandbox CODEX_HOME against a capture server: 0.144.0 fails to load the old catalog ("missing field supports_reasoning_summaries"), loads the new one and sends reasoning {effort: high, summary: auto}; with the user's keys kept it sends summary "detailed". 0.162.0 loads both and sends the same. - gofmt -l clean; go vet, GOOS=linux and GOOS=windows go vet of ./internal/agent ./internal/codexcat; linux and windows builds; go test -tags nogui ./... under a temp HOME, all ok. |
||
|
|
229ce19acc |
gateway: the loop guard's end-to-end tests bound how soon the loop was cut by what reached the agent, not by how far the fake vendor wrote ahead of a slowed gateway (#1443)
Cause: loopCut failed when the fake vendor had sent more than 2000 lines
by the time it saw its request let go. That count isn't the guard's: the
cut is made in holdWriter.Write, on the write that trips it, and cancels
the vendor's request there. But the vendor's writes succeed as long as the
sockets' and transports' buffers take them, so it runs ahead of a gateway
that reads slowly. Under load (CI macOS -race, a full run at load ~35) it
got ~2,800-3,200 lines ahead of a cut the agent saw at the 259th line.
Fix (test only, the guard was already deterministic): loopingVendor sends
at most 2000 lines of its loop, then holds its stream open until its
request is let go (20s, else it says it wasn't). Being let go therefore
means the guard found the loop within those 2000 lines, however the test
is scheduled. How soon it cut is now checked on what reached the agent:
the loop lines in the stream are at most the guard's window (256) plus one
32K read of the vendor's stream, which the cut's write sends whole.
Run:
- Reproduced by injecting a 300us sleep into holdWriter.Write (not
committed): the old test failed 140 of 180 subtests (-race -count=5
-cpu 1,2,4, 16 busy loops beside it), every route and protocol but chat,
"let go only after 2754..3082 lines", the vendor up to 8322. The agent
got ~259 loop lines in each.
- The new test with the same sleep and load: -race -count=50 -cpu 1,2,4,
1800/1800 subtests pass (built-in and plugin, chat/anthropic/responses/
gemini, and TestLoopEndsTheReplyOnEveryProtocol). At most 470 loop lines
reached the agent (the bound is 583; 256 + 32768/141 is ~490).
- Without the sleep, under load: -race -count=20 -cpu 1,2,4 of these plus
TestLoopGuardOff and TestLongReplyThatDoesNotLoopGoesThrough, all pass.
- The test goes red when the guarantee breaks: with cutLoop's h.stop()
disabled ("the vendor's request was not let go", built-in anthropic,
responses, gemini; chat is still let go when the failed write ends its
copy), and with the guard's trips dropped for its first 800 writes ("the
loop was cut only after 1038 of its lines reached the agent").
- gofmt -l, go vet (darwin, GOOS=linux, GOOS=windows) ./internal/gateway,
go test -race ./internal/gateway under a temp HOME.
|