Conversation
The wire first, then the leg. svkozak/pi-acp 0.0.33 speaks claude's ACP shape over `pi --mode rpc`; the spike settled what olai does not wait for and what it does not draw: - no usage previews and no raw content re-announced afterwards, so it reads like claude's own adapter and the doors that could double-dip stay closed; - session/set_mode unset, _session/steering unimplemented — both declared null rather than lied about, and queueing is the adapter's own (announce, answer in order); - mcpServers are accepted and wired to nothing — no permission draw and no olai tools; the conversation keeps them listed; - the editor prologue doubles as one ordinary agent chunk equal to the startupInfo its own session/new answer carries; the transcript drops it, armed at the open, consumed on the match, never pattern-matched; - session/list answers the exact directory, paginated at 50 — the chats list's deferral is named in the leg. The row is both fixtures from chat.md's gates: the adapter is the shipped pin (nix/acp-agent.nix builds pi-acp beside the claude one from the same lockfile at svkozak/pi-acp#d1cffc0069968bca00487b5726ba0b2bc6cf7217, never npm's latest), and the agent is the found one — `pi` on the agent search path, handed to the adapter at spawn as PI_ACP_PI_COMMAND. The picker offers pi only when both halves hold, draws its own mark (pi), and the resume-less badge publishes "stored by pi-acp locally".
The leg named svkozak/pi-acp @ d1cffc0; a reader of the pin (npm's pi-acp@0.0.33) is left to work out whether that is the same code. It is: d1cffc0 is two commits past the v0.0.33 tag and the delta is one README line, which is now said where the claim is made, with the full SHA so the check never has to be redone.
GrokRead the diff at Verdict: the pin is the acp-agent.nix pattern — one lockfile, SHOULD
NIT
|
Review — Claude CodeVerdict: changes requested. The leg itself is the good part: every MUST1. Every pi conversation is told it has olai's MCP servers, and the shipped pin proves it has none. —
The leg is the file allowed to know this. SHOULD2. The prologue arm is never disarmed, and the load's disarm lands after the replay it claims to protect. — The comment says arming at the load is "really disarming: whatever a previous conversation of this process announced, nothing of it may consume a chunk of THIS session's replay." It cannot: forty lines above, On 0.0.33 nothing reaches it: 3. chat.md does not carry the prologue posture the PR body says it does. — The body lists "chat.md (pi row in the list, plus its rules — not-wired tools, queue interlude, π fair play, prologue posture)". What landed is the pi row, the two variable sentences, and three "does not offer" bullets. There is nothing in chat.md about the banner, and nothing pi-specific about queueing. Olai silently drops an NIT4. Both halves of the "paginates at 50 with a 5. The derivation now builds two adapters and is still labelled as one. —
The fake is the real thing, not a happy-path subset: the |
`leaving()` clears every session-keyed value, and the doubled-prologue arm was the one it did not: an arm whose banner never landed (a reordered adapter) would wait the life of the process for the first chunk of ANY later conversation equal to it — and the load path's replay lands before the answer that rearmed it, with `fromElsewhere` passing the frames. Disarm with everything else; the load's post-answer assignment is restated as what it is: a rearm.
…handed Two claims the section exists to hold (the rule: an expected absence is said rather than discovered): pi's roster rows name a server that never reached the agent from the open, and pi's adapter-published startup banner is left out of the transcript rather than answered for — matched on the open's own text, with the silent-turn collision named.
…labelled for what it builds The paginated-at-50 passage read as one wire observation of the spike's; it is a PAGE_SIZE from the pin's source, and olai follows no cursor — said so, and the open half filed as pi-list-pagination where the tail is lost (agent.ts's storedFor, the leg's list passage). The steering deferral's id sits where the refusal is declared. The empty-roster line a greppable reader lands on names the pi row's two halves too, and the acp-agent derivation's name/version now name both pins, so a bump of either is a new store path and a new name — never a silent rebuild under the old one.
The scripted pi-acp reported a prompt arriving mid-turn at position 0 — its standing count was stood down at turn START, so the announcement only ever fired between two queued items. Kept the running turn and the count of waiters apart, the real adapter's two facts: a fresh mid-turn prompt answers 'Queued message (position 1).' with the queue corner's session_info_update, and the mid-turn scenario now reads the frame rather than trusting the composer's word for it. The banner grows the real buildStartupInfo's shape (sections and trailing blank, one chunk), and the prologue fixture follows it byte for byte.
…d owns Ruling, after review: the chats list showing pi's newest fifty (the adapter's page, not an index) and the mid-turn message QUEUEING rather than steering are things the user doc states, not items filed. docs/chat.md says both plainly; the code comments that pointed at the filings now say the posture instead.
getcwd(2) answers the physical path — /var/folders and /private/var/folders are one tree, two spellings — so the fake's exact string scope answered `[]` for a client started before a symlink: both @agent-stored pi scenarios died 15s and 30s deep on darwin's e2e, the only lane whose cwd was ever spelled that way. Scope by either spelling and answer the row in the request's; the store's own spelling of a directory pi never visited is not something a fake can answer for.
The PI lane was a branch where one lane got special-cased into the kept verse — a second vocabulary beside AGENT= for the same resource. The dance's all there is: one passthrough for every knob that reaches the answer, empty is off, and the call carries both halves of the pi row; the recipe decides nothing.
Why
Sprint pi-acp: pi becomes a selectable third agent in the chat panel. The method was binding: the wire decided the wiring, so the work started with a spike of the real third-party adapter and only then wrote the leg from what the spike saw.
What
packages/chat/src/agents/pi.ts— the new leg, one that reads like claude's own (same SDK handshake, no doors opened that make claude's special dispensation general), with capabilities set to exact spike facts, and the spiked frames documented in the file.roster.ts— pi is the fixture that satisfies both ofdocs/chat.md's launch gates at once: the adapter is the shipped pin (nix/acp-agent.nixnow builds pi-acp@0.0.33 — which is the revision this was spiked against: svkozak/pi-acp@d1cffc047ab37a096ee70ca39cfc1de463db8d12sits two commits past the v0.0.33 tag with aREADME.mdone-liner as the whole delta from the same committed lockfile as the claude adapter — npm's blessing, never npm's latest), and the agent is the found one (pion the agent search path, handed to the adapter's spawn asPI_ACP_PI_COMMAND). No pi on a machine: no pi row, nothing handwritten.agent.ts— one transcript-level rule, not a pi-shaped one: a leg may declare a prologue (Leg.prologueIn), the text its open will double as one ordinary agent chunk; armed at the open from the adapter's own answer, consumed on first equal chunk, never pattern-matched. pi'sstartupInfobanner (pi v0.84.$n) is thus left out — it would otherwise mask the silence arm: a transcript that has it can no longer tell the difference between the agent saying nothing and the adapter saying nothing yet (spike, §client-side queue). Arm: response-before-notification — the banner is emitted by pi-acp from a scheduled continuation behind its ownsession/newreturn, and on the response's microtask the request's continuation (arming) runs before the next notification can be pulled; an adapter that reorders ships the banner to the transcript, which is the safe direction.NoAgentknows pi's where-from.chat.md(pi row in the list, plus its rules — not-wired tools, queue interlude, π fair play, prologue posture),architecture.md(roster + pins),running.md(what olai looks for).Wire facts the spike settled (why the leg says what it says)
<tool>:<n>—bash:0,edit:1), not opencode'scall_*; bash frames arrive without_meta.toolNamebut with terminal content and theterminal_exit/terminal_outputcorners; edits carry structured diffs and locations.usage_update, no per-message_piMessageInfo), and raw content is not re-announced afterwards — so the two doors claude's adapter opens (usageIn,contentIn) stay closed here.session/set_modeanswers invalid params (-32602) and_session/steeringmethod not found (-32601) — neither exists, so both are declarednullrather than lied about. Queueing is the adapter's own: a mid-turn prompt is announced (session_info_updatewith_meta.piAcp.queueDepth) and answered in order — verified over a held 12s turn.mcpServersare accepted without complaint and wired to nothing: no permission is ever asked for pi's own tools, and olai's tools never reach pi. The roster's rows answer it from the open: each stands did-not-attach with the wire's own sentence under it ("…wires them to nothing…") — a row that stoodhandedfor the life of the conversation would be the panel repeating the model's wrong answer in the place a person has decided to trust.Evidence
Saatchi against real pi (kimi-k3, on this machine's PATH;
JUSPAY_API_KEYin env) — the picker offers π, the header namespi litellm/kimi-k3, a real turn wroteevidence.mdwith its structured diff drawn, and the banner is nowhere:One take: choose pi from the chooser, one real write streaming its own tool row, one message sent mid-turn answered by the adapter's own Queued message (position 1)., a second chat, and a reload that reopens the remembered conversation with its answer replayed — real pi, real store:
record.mp4
Runs
just typecheck— cleanjust test— 4271 + 87 pass;just fmt-checkcleanjust fmt-check— cleanjust nix— packages; the built wrapper's self-check answers both baked adapters (packaged pi adapter: …/bin/pi-acp)choosing_an_agent.feature— 35/35, incl. the six@piscenarios (picker offers pi; banner held out; silent-turn arm not poisoned; the mid-turn interlude — composer claim AND the adapter's ownQueued message (position 1).chunk; roster rows at did-not-attach with the sentence; grouped list without badges; stored reopening with the fixed name)Deferrals
No deferrals — both earlier filings were dissolved by ruling: the chats list's newest-fifty page and the mid-turn QUEUE-not-steer are the permanent shape, stated plainly in
docs/chat.md. The fold section above names the filings as they stood when the fold answered them.