# CHAT-03 brief: live adapters and mediated terminal (#1507, row 5) Author: Dewey, 2026-09-26. R1, for review. This is the brief only. It includes no source, no contract edits and no seat changes. Jason chartered CHAT-03 on 2026-09-26 (`docs/plans/2026-09-26_lead-decisions.md` item 22). Plan row: `docs/plans/2026-09-13_webui-session-chat.md` line 215. It depends on CHAT-01 (`28d4e98a`) and implements part of the CHAT-01C companion (`b023841c`). The layout follows `docs/plans/BRIEF-TEMPLATE.md`, and the depth follows the CHAT-02 brief (`636b0fac`). The six carry-forwards Sage set are mapped to their evidence under "Carry-forwards". Line numbers and hashes below are at `40a02d2b`. ## CHAT-03: live adapters and mediated terminal ### Problem CHAT-02 made every repository Pi conversation readable. Nothing can drive a conversation yet except the old paths: the Pi TUI in a tmux pane, and board Reply or agent-send, which paste into that pane. Those paths have no controller, no generation fence and no stop proof. One has already misfired. At 2026-09-26T20:10:57Z a leftover `/` in Pi's composer landed in front of a board reply, because `tools/tmux/send-message.sh` (lines 45–54) pastes onto whatever the composer holds and then presses Enter (`docs/plans/DEFERRED.md`, "Board send can turn into a Pi slash command"). CHAT-01 defines the records and commands for one controller per conversation. It defers the writer-claim record (`chat-01/README.md:342`), and CHAT-01C defers R3-1 (`chat-01c/README.md:217`). Lead decisions item 8 moves both here. The Claude catalogue still refuses `unsupported-harness` (`packages/conversation/src/reader.mjs:24`). Jason ruled that CHAT-03 owns it after B1. Claude on this host reports 2.1.283, but the plan's evidence named 2.1.269 (plan line 159). The binary changes under any adapter that does not pin it. ### Owner and reviewer - **Source author: ``.** The plan has Darkwing name the backend author (line 215; lead decisions item 22). Darkwing is on queue A1/A2, so the author is named after this brief is approved. The brief does not pick one. - Brief author: Dewey. - Filbert reviews this brief, then the exact candidate of each increment. - Rocko does an adversarial pass on §5 (control races) and §6 (stop and recovery), in this brief and later in the code of each increment. Rocko also reviews the B1 packet (§8), because it is evidence that a false pass would turn into a wrong adapter. - No one reviews their own work. If Darkwing names Filbert or Rocko as author, Sage names a replacement for that review. - Tracking: #1507. ### Files owned The source author may create or change only these paths. | Path | Contents | |---|---| | `packages/conversation/src/**` | New modules for the controller, claim, supervisor, Pi adapter, events, transport and mediated terminal. The Claude parser and adapter come in 3D. `reader.mjs` changes only to lift the Claude refusal for the pinned version. | | `packages/conversation/tests/**` | Tests, fake engines, fixture extensions and redacted recordings | | `packages/conversation/README.md`, `packages/conversation/package.json` | Documentation and the new refusal names. No new dependencies. | | `agents//work/chat-03/**` | Review packets, evidence and the B1 packet | Module names inside `src/` are the author's call (plan lines 150–151). Any path outside this table needs a brief amendment. Excluded, and why: - **`tools/tmux/**`.** Plan line 244 excludes it, and §4 shows the mediated path needs no change there. - **`packages/control-board/**`.** No board edit is needed (§4, S6). Tests import `replyToRow` read-only by relative path, the same way the queue-as-data plan imports `scan.mjs`. - **`packages/seat/**`, `scripts/agent-host-dev.sh`, `agents/*/launch.sh`.** No seat migrates, so no seat or launcher changes. The launcher handoff belongs to CHAT-07. - **`packages/webui/**`.** The chat UI is CHAT-05. - **`docs/plans/chat-0*/**`.** Contracts change only through the C items below. - **`roles/**`, `contracts/**`, root files, auth and `~/.mosaic`.** No overlap with queue-as-data or the ledger: | Owner | Paths | Overlap | |---|---|---| | A1, Darkwing (`agents/darkwing/work/queue-a1/build-manifest.sha256`) | `packages/queue/**`, `scripts/queue-commit.sh`, `scripts/test-queue.sh`, `scripts/git-hooks/pre-commit`, `docs/plans/BRIEF-TEMPLATE.md` | none | | A2, Darkwing (Filbert's plan `282fabbb…` §8.1; lead decisions item 20) | `scripts/mosaic` dispatch, `docs/plans/queue.json`, `docs/plans/QUEUE.md`, `scripts/test-{darkwing,rocko}-launch.mjs` | none. The mediated terminal runs as `node packages/conversation/src/.mjs`. It is not a `scripts/mosaic` verb. | | Piece B, after A | `AGENTS.md`, `agents/*/CONTEXT.md` | none | | Ledger | `packages/ledger/**` | none | The author may import the process-identity helpers in `packages/discord/src/journal.mjs` read-only, as A1 does. That changes neither package. ### Contracts implemented These are unchanged since the commits named. The sha256 values are at `40a02d2b`, and `git status` shows no local edits. | Path | sha256 | Commit | |---|---|---| | `docs/plans/chat-00/README.md` | `991663e607404c2f022716bd45b40d5b3092d53c3f9f61bbafd455c0659b832f` | `370823b3` | | `docs/plans/chat-00/sources.json` | `1a07ae88de45fd3219eca10acae13637ca75416cb1c598aa9080825bd7598af9` | `370823b3` | | `docs/plans/chat-00/fixtures.json` | `ad9d4fe94131b2c4bb30691ad846698c5b0818f0935d20a38de73fa3f267bd80` | `370823b3` | | `docs/plans/chat-00/check.mjs` | `568f004f5a0cfce64d65ff202c420ab491e716919a93989377a7bb9e7b8be622` | `370823b3` | | `docs/plans/chat-01/README.md` | `61aba7d60f380ff8a135a04a2e851f11c250795ead11cca2e594566849483163` | `28d4e98a` | | `docs/plans/chat-01/contracts.schema.json` | `38382e08c97635f8864e6953b6cf44ec324040397a1aa8fc3f86e279abe36db1` | `28d4e98a` | | `docs/plans/chat-01/fixtures.json` | `403c8ae91963a379de17805380bc1425bc4e96c1ef34094385b703d17cf5ce7d` | `28d4e98a` | | `docs/plans/chat-01/check.mjs` | `2e164e4bfa61963bb5dd8639e26e67407e64d19278cc56ed8a4f77e430f1dee5` | `28d4e98a` | | `docs/plans/chat-01c/README.md` | `63d9f9edffc2aa1aa3bc99364b864e8c6f234eedc8ad2f9da231531970d54250` | `b023841c` | | `docs/plans/chat-01c/contracts.schema.json` | `da132f0a02f29281344cc5350396af53c893d7895308bc430f9bf3ca3b3785b9` | `b023841c` | | `docs/plans/chat-01c/fixtures.json` | `00639a219b00b67c5e0ab904163a6d945481f6b0df45a058b32e00f1f1326611` | `b023841c` | | `docs/plans/chat-01c/check.mjs` | `5971ed5f11a32f0dff5720db03d8d4bc4c007a5d2654ba50726bb35ba753eba6` | `b023841c` | Design inputs, which CHAT-03 does not implement in full: `docs/plans/foundation-v1-candidate/RUNTIME.md` §§3–5 (`b1a2b4d0…`) and the plan itself (`48142829…`). What CHAT-03 takes from each contract: - **CHAT-01.** The records `binding`, `connection`, `clientRequest`, `request`, `receipt`, `event`, `stop`, `cohortProof`, `effectReport`, `turnProof`, `nativeDecision`, `approval` and `confirmation`. The commands `observe`, `prompt`, `takeover`, `acquire-recovery-control`, `approval`, `interrupt`, `force-stop`, `recover`, `issue-confirmation` and `answer-confirmation`. The draft, upload and queue-edit commands belong to CHAT-04. - **CHAT-01C.** Only the confirmation restoration rules (lines 92–104). The private-state pages, upload ranges and `recover-refused-draft` need CHAT-04's durable store. The CHAT-01 text at line 342 stays as published. Lead decisions item 8 records the move. **Contract changes, each a separate reviewed item.** Each one lands and is approved before the code that needs it. Sage assigns the author; Filbert and Dewey review, as they did for CHAT-01C. - **C-1: a state for dispatched input that native evidence proves unconsumed (R3-1).** It also decides whether that input may be recovered into a draft. Q20 says already-dispatched work keeps its actual or uncertain status, so Sage decides whether C-1 goes to Jason. §3 gives the interim rule until C-1 lands. - **C-2: the CHAT-03D native dialog companion.** It covers Pi `confirm` and `select` with exact labels; `input` and `editor` stay unsupported. CHAT-01 lines 239–242 and 292 block non-permission forms until it exists. Increment 3B waits for it. - **C-3: Claude record fields.** Needed only if B1 shows that CHAT-01 lacks a field, for example the capability-negotiation result on `binding`. Otherwise C-3 is void. Not contract changes: - The writer-claim storage record (§2) is internal and never crosses the wire. It stores the CHAT-01 `binding` fields it needs. If one is missing, that becomes a C item, not an edit. - New refusal names (`busy`, `receipt-unknown`, `engine-pin-mismatch`, `foreign-writer`, `not-controller`, `stale-generation`) are bounded draft IDs, which CHAT-01 line 363 allows without a registry. The package README lists them. ### What ships Four increments, in order. Each is its own candidate and review round, as with the queue-as-data A1/A2 split. No increment starts before the one above it is approved. | Increment | Content | Needs first | |---|---|---| | 3A | Controller, transport, writer claim, Pi adapter on a fake Pi engine, incremental events, mediated terminal, interrupt, force stop and recovery. Fixtures in §2–§6, §9 and §10. | Brief approved; author named | | 3B | Pi native dialogs (§7) | 3A approved; C-2 approved | | 3C | The B1 evidence packet for Claude (§8) | 3A approved. Jason's go for any run that calls a model or reads Claude auth. | | 3D | Claude adapter, catalogue and history (§7, §8) | 3C approved, so B1 passed; C-3 if it exists | If B1 does not pass, CHAT-03 closes after 3B. Claude keeps refusing `unsupported-harness`, and CHAT-06's both-harness gate stays blocked with CHAT-03 as its owner. That outcome is recorded, not a silent narrowing. #### 1. Controller and transport - One controller process per execution owns the engine's stdin. Pi runs in `--mode rpc` with no TUI, so a mediated engine has no native composer or terminal left to fence. That is how CHAT-03 meets plan lines 133–134: a losing native TUI can't stay writable because none exists. - Engine output is split on LF only (`rpc.md` lines 30–38). Node `readline` is not used, because it also splits on U+2028 and U+2029. The ledger refused a live line for that class of bug (lead decisions item 18). Fixture E2 covers it. - Clients connect over a Unix socket in a 0700 directory. Every connection starts as an observer. The browser does not reach the socket in CHAT-03; the WebUI side is CHAT-05. - **Actor.** There is one actor, `local-operator`, as in CHAT-02. The socket directory's mode is the only boundary, and every process with the same uid passes it. That includes agent seats, which run as the same user. So CHAT-03 cannot stop a same-uid agent from taking control of another conversation. See Limit 1. This blocks any live use until a local owner-channel design passes review (plan lines 181–185; B4). - **No broker queue.** CHAT-04 owns durable queues (plan line 216). In CHAT-03, a prompt sent while the engine is busy is refused with `busy`, and the text stays in the client. The controller never sends `streamingBehavior`, `steer` or `follow_up`, so mediated input never enters Pi's native queues (`rpc.md` lines 56–65). Tradeoff: there are no queued follow-ups (Q6) until CHAT-04, but 3A has no native-queue ambiguity of its own making. - Admission and dispatch both recheck, under one dispatch lock: binding state, open admission, the controlling connection and its generation, the execution incarnation and the text policy (CHAT-01 lines 153–160). - **Dedup key.** Actor, conversation and client request ID (CHAT-01 line 144). The index lives as long as one controller incarnation. A retry that arrives after a controller restart is refused with `receipt-unknown`; it is never redispatched. Durable receipts are CHAT-04. - **Text only.** Images and files are CHAT-04. #### 2. Writer-claim record (D1) CHAT-01 lines 336–343 allow at most one non-stopped binding per conversation or native session identity, and they defer the record. RUNTIME.md §3 item 3 adds one claim per (agent, project, workspace). The record: - **Keys.** Each claim has two keys, and both must be held: the seat tuple (seat, project, workspace) and the native session identity (the Pi header ID or the Claude session UUID). They are always taken in that order, so two controllers can't deadlock. - **Revisions.** Each key is a directory under a claim root. Each revision is a write-once file created exclusively and fsynced along with its directory. The current state is the highest revision. No file is ever rewritten, the same rule as run records. - **Fields.** A revision stores: - the key and the binding ID; - the harness, conversation, and branch and leaf at launch; - the config pins: engine version, binary sha256 and launch argv digest; - the execution incarnation: boot ID, pid, process start time and cgroup; - the controller generation; - the state: `reserved`, `active`, `stopping`, `uncertain` or `stopped`; - the proof reference that allowed `stopped`. - **Location.** The claim root is a constructor argument. CHAT-03 sets no production default and writes nothing under the data root or `.pi/state`. Fixtures use temporary roots. The live location is chosen at cutover (CHAT-07) and reviewed as a data-map change then. Tradeoff: nothing live can use 3A until that decision, which is intended. - **What never releases a claim.** Disconnect never changes a claim. `agent_settled`, EOF, SIGTERM, idle and an abort acknowledgement never release one (CHAT-01 line 325; CHAT-00 line 65). - **Restart.** A controller that starts and finds an `active` claim for its key does not launch. It classifies the recorded incarnation: - **Different boot ID.** Every process of that boot is gone. That is proof for the whole cohort, so the claim moves to `stopped` with a boot proof. Tool calls with no recorded end become `uncertain` effects. - **Same boot, engine alive.** This is an orphan the controller cannot attach to, because the stdin pipe is gone. The state becomes `uncertain`. Only a confirmed force stop of that cohort (§6) moves it on. - **Same boot, engine gone.** The state is `uncertain` until the cohort observation in §6 shows no member left. - **Foreign writer.** The controller watches the bound session file. An appended entry whose ID never appeared on the controller's own stream means another writer, for example someone running `pi --session` on the same file. Admission closes, the binding becomes `uncertain`, and views get a reconcile marker (plan lines 179–180). - **No writes to sessions.** The controller never writes a session file and never uses `SessionManager.open` (CHAT-00 line 67). | # | Fixture | Expected | |---|---|---| | W1 | Two processes acquire the same key at once | Exactly one claim. The other refuses `already-active`. | | W2 | Acquire while a claim is `active` or `reserved` | `already-active` | | W3 | Acquire while `stopping`, `uncertain`, or `stopped` without proof | `unsafe-replacement` | | W4 | Same session with a different seat tuple, and the reverse | Both refuse. No partial claim is left. | | W5 | Controller killed with SIGKILL at each step of acquire, transition and release | After restart: never two non-stopped claims and never a lost claim. The recorded state is the old one or the new one. | | W6 | Controller killed mid-turn and restarted while the engine is alive | `uncertain`. No launch, and prompts refuse. | | W7 | Recorded boot ID differs from the current one | `stopped` with a boot proof. Open tool calls become `uncertain`. | | W8 | Resume after a proven stop with the same pins | New revision, generation +1, same conversation, branch and leaf | | W9 | Resume with a changed binary, argv digest, branch or leaf | Refused. The claim is unchanged. | | W10 | Foreign append to the bound session file | Admission closed, `foreign-writer`, `uncertain`, reconcile marker. No engine write. | | W11 | Controller writes to session files | None. The CHAT-02 F17 check runs over the fixture session directory. The fake engine's own appends are recorded separately and excluded. | #### 3. Pi adapter, incremental events and R3-1 Pinned inputs for Pi 0.85.1, under `node_modules/@earendil-works/pi-coding-agent/`: - `docs/rpc.md` (`15fcd26bee72777b373fd5f2edd77091a01cadd4de95e48b08422ced0552a28d`) - `dist/modes/rpc/rpc-mode.js` (`e7e4724aa55c5aac73cf36793653b26736200e5c59d58373990fc31028f86477`) - `dist/modes/rpc/rpc-types.d.ts` (`e968e5be01dc7ad9615f938ae867ef136fa495f13dcf169942e9f781a299d9eb`) The plan requires reading the Pi docs completely before Pi implementation (lines 163–164). The author does that before any 3A code. - **The fake engine.** A fixture process that speaks the pinned protocol: - prompt acceptance and refusal; - message and tool events; - `agent_end` arriving before `agent_settled`, and retries; - `clear_queue` and `abort`; - dialogs; - scripted pause points, so a test can land a race at an exact step. Its behavior is checked against the pinned files, not guessed. - **Real-binary smoke.** The pinned `pi` starts in `--mode rpc` in a scratch directory, with no auth file and no model call. It answers `get_state`, `get_commands`, and `clear_queue` and `abort` while idle. The recorded exchange shows that the fake's framing matches the binary. If the binary won't start without auth, the author records that, and the packet says plainly that 3A rests on the fake alone. - **Events.** Native events map onto CHAT-01 `event` records: message-start, text and thinking deltas, tool-start, tool-update and tool-end, message-end with `updateMode: replace`, and run-settled. - Every event carries the execution incarnation and a sequence number for that execution. - An unknown native event becomes an `unavailable` event. It is never dropped silently and never passed through raw. - `agent_settled` maps to run-settled, never to cohort termination. - **Joining history and the stream.** CHAT-01 lines 105–112 require an atomic cut between a history page and the stream; otherwise streaming is advertised as unavailable. The author shows the cut from the pinned source: when a message_end's entry is on disk relative to the event. If that can't be shown, 3A advertises replay as unavailable. A client gets the page, then live events from the moment it subscribes, with a reconcile marker at the seam. Fixture E4 covers both cases. **R3-1: input dispatched but not consumed when a fence lands.** Because the controller never sends `streamingBehavior`, Pi queues mediated input in one case only. Pi accepts a prompt at preflight, and queued prompts count as success (`rpc-mode.js` lines 298–318), at a moment the controller didn't see as busy, such as between `agent_end` and `agent_settled` during a retry. Anything else in the native queues came from outside Mosaic, for example an extension's follow-up message. The rule: 1. Interrupt and force stop close admission first, then send `clear_queue`, then `abort`, in that order. `abort` runs any queued messages still in the session (`rpc.md` line 158). 2. At most one dispatched item can lack a started user message. If `clear_queue` returns exactly that item's text, exactly once, the item is proven unconsumed. 3. Until C-1 lands, a proven-unconsumed item keeps its actual receipt state (`acknowledged`). The turnProof records that `clear_queue` returned it. The item is never relabelled unsent and never resent. The client shows the text so the actor can send it again as a new request. 4. Returned text that matches no Mosaic item is external input. It goes into the turnProof and is shown read-only as held by the engine, not sent by Mosaic, and discarded. It is never resent. 5. If `clear_queue` fails or times out, the turnProof records `nativeQueue: unknown`, the stop stays `uncertain`, and admission stays closed. | # | Fixture | Expected | |---|---|---| | N1 | Prompt accepted during the retry window, then Interrupt | `clear_queue` goes before `abort`. The returned text matches the one item. The receipt stays `acknowledged`, the turnProof lists the item, and nothing is resent. | | N2 | N1 with `abort` sent first (mutant) | The fake runs the queued item, so the test fails. This is the ordering guard. | | N3 | `clear_queue` returns an extension's follow-up | Recorded as external and shown read-only. Attributed to no request. | | N4 | `clear_queue` returns the item's text twice, or a near match | No proof. The receipt becomes `delivery-unknown`, and the stop stays `uncertain`. | | N5 | `clear_queue` times out | `nativeQueue: unknown`, stop `uncertain`, admission closed | | N6 | Pi refuses a prompt because it is streaming (no `streamingBehavior`) | Receipt `failed` with native evidence. The frozen text is kept for an explicit resend. | #### 4. The slash path (carry-forward 3) There are two separate hazards: text left in a composer gets concatenated with a new message, and Pi interprets certain prefixes. - **Concatenation.** `send-message.sh` pastes onto whatever the composer holds. The mediated path has no engine-side composer. Each prompt is one JSONL record, and its `message` field holds the whole text. Nothing left over can prefix it. The mediated terminal's own composer is a local buffer. It is empty at start, cleared after each submit and on control transfer, and it submits only while its connection is the controller. - **Interpretation.** RPC still acts on a leading `/`. Extension commands run immediately, even while streaming, and skill and template commands expand (`rpc.md` lines 67–69). - Repository seats load one extension command, `/goal` (`.pi/extensions/goal/index.ts:230`), and disable skills and templates (`scripts/agent-host-dev.sh:135–136`). - Admission refuses `text-policy` when the first non-whitespace character is `/` (CHAT-01 lines 181–183). Dispatch checks again. - CHAT-01 lines 368–370 leave `!`, `@` and slashes on later lines open under B3. 3A settles them from the pinned source. The author lists every prefix that Pi 0.85.1 RPC `prompt` interprets, and the list becomes a fixture. Admission refuses each listed prefix, and any prefix the author can't classify. - **Board and agent-send for a mediated seat.** A mediated engine has no tmux pane. The board already refuses a reply to a registration without a tmux session before the tool runs (`packages/control-board/src/serve.mjs:84`; test `serve.test.mjs:810`). CHAT-03 relies on that and changes neither the board nor `tools/tmux`. No real seat registers as mediated in CHAT-03, so the refusal first applies to a real seat at cutover. - **Seats still on tmux keep the hazard.** The DEFERRED entry stays open. It closes when a seat moves to the mediated path (CHAT-07), or when a CHAT-03I charter covers `tools/tmux` (CHAT-01 lines 232–238). Neither happens in CHAT-03. | # | Fixture | Expected | |---|---|---| | S1 | `/goal x`, and the same with leading spaces or a tab | `text-policy` at admission. Zero bytes reach the fake engine. | | S2 | Each prefix on the author's list | Refused. The test reads the same list the code uses. | | S3 | `/goal` on the second line | The fixture pins the result from the pinned source: admitted if Pi doesn't interpret later lines, refused otherwise | | S4 | The terminal composer holds a `/` from an abandoned edit, then control transfers and returns | Composer cleared. The next submit sends only the new text, and the fake engine records the exact bytes. | | S5 | An observer terminal gets a paste and then Enter, as `send-message.sh` does | Not admitted: `not-controller`. Zero engine bytes. | | S6 | A mediated-shaped registration (no tmux) passed to `replyToRow` with a recording `exec` | 409, "no tmux session". `exec` is never called. | | S7 | A prompt containing ESC, bracketed-paste markers or U+2028 | Sent as one JSON string. The fake engine receives the exact text, and no record splits. | #### 5. Control races (Rocko) Each race uses the fake engine's pause points to land at an exact step. Every race asserts the engine bytes, the receipts and the events. | # | Race | Expected | |---|---|---| | H1 | Two takeovers with the same expected generation | One wins, and the generation goes up by 1. The other refuses `stale-generation`. | | H2 | The old controller's prompt arrives after a takeover commits | Refused. Zero engine bytes. | | H3 | A takeover commits while a prompt holds the dispatch lock | Either the write finished before the commit, and the receipt keeps the old actor, or the prompt is refused. Never a partial write, never both. | | H4 | Self-takeover | Refused (CHAT-01 line 269) | | H5 | The old controller answers an approval after a takeover | Refused. The decision is answered once, through the new projection. | | H6 | Duplicate approval answers with the same content | One native response | | H7 | Conflicting approval answers with the same request ID | The second is refused | | H8 | An approval answer after a native timeout or cancel | Refused. The state comes from native evidence. | | H9 | Interrupt racing a prompt's dispatch | Before the write: `dispatch-refused`. After it: the R3-1 rules in §3. | | H10 | Interrupt and force stop at the same time | One stop chain. Force stop supersedes (CHAT-01 lines 319–322). | | H11 | The controller disconnects mid-turn | Work continues and the claim is unchanged. Control stays with the disconnected connection until an observer takes over explicitly. Nothing happens automatically. | | H12 | Exact retry of a prompt after reconnecting | The same receipt. No second dispatch. | | H13 | Retry with the same request ID and different text | Refused | | H14 | Late stdout from the old engine after a replacement | Dropped by execution incarnation and counted in the evidence. Never rendered. | | H15 | A revoked connection sends a command | Refused, with the revocation fence of CHAT-01 lines 271–280 | | H16 | A second controller process for the same session | Refused as in W1 and W2. The first controller is untouched. | | H17 | A confirmation reused, answered from another connection, or answered after the stop context changed | Refused. Confirmations are single-use (CHAT-01 lines 264–268; CHAT-01C lines 92–100). | #### 6. Stop, cohort proof and recovery (Rocko) - **Cohort.** The controller launches each engine in its own transient systemd user scope (this host runs systemd 261), so the cohort is that cgroup. A child that calls `setsid` stays in it. If a scope can't be created, the cohort falls back to the process group. A stop there can reach `uncertain` but never `stopped`, because a `setsid` child can escape. Tradeoff: this needs `systemd-run --user`, and the fallback is honest rather than equivalent. - **Proof.** `stopped` needs a cohortProof (CHAT-01 lines 327–331): - complete membership; - boot ID, and pid and start time for each member; - a death time for each member; - the observation time; - an empty cgroup. SIGTERM, EOF, an abort acknowledgement, `agent_settled` and idle never promote a stop to `stopped` (line 325). - **Effects.** A tool-start with no tool-end at stop time is an `uncertain` effect. Killing never counts as rollback (line 334). - **Interrupt (Q20).** Close admission, `clear_queue`, `abort`, wait for settle, build the turnProof, record `reconciled`, and reopen admission under current control (lines 307–313). If this hangs, force stop stays available. - **Force stop (Q8).** Needs an answered confirmation. It closes admission, sends SIGTERM to the cgroup, then SIGKILL after a bounded grace period, then builds the proof. It acts only on the selected execution's cgroup, and other executions survive. - **Recover (Q15).** Recover reports eligibility only. It needs: - a stopped proof; - effects reconciled or explicitly uncertain; - an exact confirmation; - the same pins (lines 336–343). Launching is a separate library call made by a launcher, and it needs that eligibility record. No client command launches. A browser or controller crash can't start an engine. - **Pi resume.** Resume reuses the same session file and leaf. The author confirms from the pinned docs how Pi selects the leaf. Before admission opens, the adapter checks `get_state` and `get_tree` and refuses if the engine loaded any other leaf (RUNTIME.md §3 item 4). | # | Fixture | Expected | |---|---|---| | K1 | Force stop an engine whose tool child calls `setsid`, with a scope | The child is killed. `stopped`, with proof. | | K2 | K1 on the process-group fallback | `uncertain`, never `stopped` | | K3 | SIGTERM acknowledged while a member is still alive | Stays `stopping` or `uncertain` | | K4 | Two fake engines; force stop one | The other keeps running and still answers | | K5 | Stop during a tool call | The effect is `uncertain` and shown | | K6 | Recover without proof, without confirmation, or with changed pins | Refused | | K7 | Recover after proof, then launch | New incarnation, generation +1, same leaf. The cancelled prompt is not replayed. | | K8 | The engine loads a different leaf on resume | Refused before admission opens | | K9 | An interrupt that never settles | Stays `uncertain`. Force stop stays available, and ordinary takeover is refused while fenced. | | K10 | Controller crash during a force stop | The restart finds `stopping` and resumes observing. No relaunch. | #### 7. Native approval dialogs (3B after C-2; Claude in 3D) - Pi has no built-in permission system. Approvals come from extensions through `extension_ui_request` (`rpc.md` lines 1186–1210). Repository seats load no extension that raises dialogs today, so 3B tests with a fixture extension. - CHAT-01 lines 239–242 and 292 block non-permission forms until CHAT-03D exists, and C-2 is that companion. After C-2: - `confirm` and `select` render the exact native labels and answer with the exact request ID; - `input` and `editor` stay unsupported and show as disabled, with a reason; - nothing is answered on the user's behalf, and a generic "yes" is not accepted. - Cancel is enabled only where C-2 proves it can't mean allow. A Pi select cancel does not inherit confirm's meaning (lines 293–295). - Pending dialogs don't survive a Pi restart (CHAT-00 line 69). After a takeover, the controller reprojects each dialog: a new projection ID for the same decision (lines 282–287). - **Claude, in 3D after B1.** `can_use_tool` maps to allow-once and deny. Choices that widen permissions are disabled, and there is no invented cancel (lines 294–295). Pending permission requests are re-read from initialize on reconnect, but only if B1 shows that the pinned version supports it. | # | Fixture | Expected | |---|---|---| | P1 | `confirm` dialog answered by the controller | One `extension_ui_response` with the exact ID and value | | P2 | `select` with labels containing markup and bidi controls | Exact labels, rendered inert | | P3 | `input` or `editor` dialog | Disabled, with a reason. No response is sent. | | P4 | A native timeout before the answer | `uncertain`, then resolved from native evidence. A late answer is refused (H8). | | P5 | Takeover while a dialog is pending | New projection ID and the old one superseded. One native answer. | | P6 | Interrupt while a dialog is pending | The approval becomes `uncertain`, not denied (lines 297–299) | | P7 | A Claude permission request (3D, recorded fixture) | Allow-once and deny only. Widening choices disabled. | #### 8. Claude: B1 and the catalogue (carry-forward 2) **What B1 is.** `docs/plans/chat-00/README.md` lines 193–195: prove the exact Claude CLI and protocol combination, capability negotiation, transcript branch selection and the image and file input shapes. SDK and source examples narrow the uncertainty, but they don't certify the installed binary. CHAT-00 line 66 names the gap for history: Claude's persisted branch format and leaf selection. **What proves it (3C).** A B1 packet under `agents//work/chat-03/b1/`, approved by Filbert and Rocko, with five parts: 1. **Pin.** The exact version string and the sha256 of the binary the adapter will run. Today `claude --version` reports 2.1.283; the plan saw 2.1.269. The adapter checks both at launch and otherwise refuses with `engine-pin-mismatch`. It runs the engine with auto-update off, and the author cites the setting from the pinned docs. 2. **Sources.** The official protocol sources for that exact version, by hash. These are the C-* entries in `chat-00/sources.json`, re-pinned to that version. 3. **Recordings.** Made from that binary, in a scratch config directory with no repository credentials: - initialize and its capability answer, including whether `interrupt_cancel_queued_v1` and `pending_permission_requests` exist; - a text prompt and an image prompt; - a `can_use_tool` allow and a deny; - an interrupt. They are redacted, hash-pinned and used as the 3D fixtures. The plan rules out model calls for discovery (line 166). Any recording that calls a model or reads Claude auth therefore needs Jason's go first, because it spends money and touches credentials. Without that go, 3C stops after parts 1 and 2, and B1 stays open. 4. **Branch selection.** Two persisted transcripts from that binary, one of them with a fork (an edit or a rewind). The packet states the leaf rule and shows that `--resume` continues the leaf the parser picks. 5. **Negative.** The parser refuses a transcript from another version with `unsupported-harness`. **The catalogue after B1 (3D).** - Rocko's launcher runs Claude with the default config directory (`agents/rocko/launch.sh:86`, no `CLAUDE_CONFIG_DIR`). So its transcripts sit in the same `~/.claude/projects//` directory as every other Claude session started in this checkout, including T3 threads and Jason's own sessions. - The catalogue therefore never lists that directory. It opens only session IDs the repository launcher recorded for that seat (`.pi/state/rocko/session-id` and `launches/*/session-id`). Each ID must resolve to exactly one file under the approved root, opened with the CHAT-02 safe-open rules. - A launcher receipt is a hint, like a registration (CHAT-01 lines 62–64). The access mapping stays under B4 review. - The CHAT-02 guarantees still apply: read-only, no writes (F17), cursors, byte caps, continuation parts and inert rendering. | # | Fixture | Expected | |---|---|---| | L1 | A Claude transcript at the pinned version | Default leaf shown and other branches readable, as in F12 | | L2 | A transcript from another version | `unsupported-harness` | | L3 | A decoy session file in the same directory, named in no receipt | Never listed, never opened | | L4 | A receipt naming a file outside the root, a symlink or another project | Refused, as in F7–F9 | | L5 | Sidechain or subagent entries | Kept separate, not merged into the main branch | | L6 | No-write check | F17 over the Claude directory | | L7 | The binary's sha256 or version differs at launch | `engine-pin-mismatch`. No launch. | The 3D adapter maps Claude stream-json onto CHAT-01 events. The §5 and §9 fixtures then run again against the recordings. #### 9. Return flow and events | # | Fixture | Expected | |---|---|---| | E1 | The plan §6 return flow on the fake engine: send, acknowledge, user, toolCall, toolResult, new final answer | Shown once in the same conversation, with no refresh, no duplication, and the draft and reading position kept | | E2 | U+2028 and U+2029 inside JSON strings, and CRLF | Each parsed as one record | | E3 | Multipart final, two blocks, null request correlation, duplicate delivery | The CHAT-01 cases at lines 110–112 | | E4 | A page, then a subscription with overlap, and again with a gap or a new epoch | Overlap deduplicated. A gap reconciles. | | E5 | An unknown native event | An `unavailable` event. Nothing is dropped. | | E6 | A tool result delayed across a pause and a reconnect | Reconciled without a manual refresh | | E7 | The mediated terminal as observer, then as controller | Renders the same stream as the library client, and submits only as controller | The browser leg of the return flow is CHAT-05. 3A proves it at the library and the terminal. #### 10. Checks and mutation pass - **Suites.** These must pass: - `node --test packages/conversation/tests/`; - the control-board, webui and seat package tests; - `node docs/plans/chat-00/check.mjs`, `chat-01/check.mjs` and `chat-01c/check.mjs`; - the eight `scripts/test-*.sh` suites. - **Nested runners.** A test that starts `node --test` itself clears `NODE_TEST_CONTEXT` for that child, as in the DEFERRED entry on nested runners (N13), or it can hide failures. - **Isolation.** Every fixture runs in temporary directories. No fixture touches live registrations, `.pi/state`, `~/.claude` or a live seat (plan line 272). - **Mutation pass.** Run in a scratch copy, as for the CHAT-02 Console. Each of these mutants must fail at least one test: 1. dispatch skips the generation recheck; 2. `abort` is sent before `clear_queue`; 3. a claim file is created without exclusive create; 4. the late-event filter ignores the incarnation; 5. the text policy checks only the first character, missing leading whitespace; 6. an acknowledged SIGTERM promotes a stop to `stopped`; 7. the process-group fallback can reach `stopped`; 8. a confirmation is not consumed on use; 9. disconnect releases control; 10. the composer isn't cleared on transfer; 11. engine output is read with `readline`. 3D adds two more: the pin check is skipped, and the decoy file is listed. #### 11. Choices in this brief, for review | Choice | Tradeoff | |---|---| | No broker queue; a busy engine refuses `busy` | No queued follow-ups until CHAT-04, and no native queueing of mediated input | | No `streamingBehavior`, `steer` or `follow_up` | Same as above; this is what keeps R3-1 down to one case | | No default claim root | Nothing live can use 3A before the CHAT-07 data-map decision | | Cohort is a systemd user scope; the process-group fallback can't reach `stopped` | Depends on `systemd-run --user`, and the fallback leaves stops uncertain | | One actor, `local-operator`, guarded only by socket-directory mode | Same-uid agents are not kept out (Limit 1) | | Dedup lasts one controller incarnation | A retry across a restart refuses instead of returning the receipt | | Four increments, with 3C/3D gated on B1 | CHAT-03 can close Pi-only, with that outcome recorded | | Claude catalogue from launcher receipts only | A Claude session started outside the launcher never appears | ### Carry-forwards | # | Sage's item | Where | Acceptance evidence | |---|---|---|---| | 1 | D1 writer-claim record and R3-1 (lead decisions item 8) | §2, §3 | W1–W11 and N1–N6 pass. Mutants 2 and 3 fail tests. C-1 settles the final state. | | 2 | The Claude catalogue after B1 | §8; increments 3C and 3D | The B1 packet (parts 1–5), approved by Filbert and Rocko. L1–L7 pass. | | 3 | The board send that can become a Pi slash command | §4 | S1–S7 pass. Mutants 5 and 10 fail tests. The DEFERRED entry stays open for seats still on tmux. | | 4 | The CHAT-01/01C contracts, by path and hash | "Contracts implemented" | The hash table matches `sha256sum` at approval. C-1 to C-3 are separate reviewed items. | | 5 | Path authority | "Files owned" | The overlap table. Every changed path in each candidate is inside "Files owned". | | 6 | The source author | "Owner and reviewer" | ``, named by Darkwing after this brief is approved | ### Limits 1. **Same-uid control.** Any process running as the same user can connect to the socket and take control. Agent seats run as that user, so an agent could take over another conversation. CHAT-03 is fixture-only, so no live conversation is exposed. Live use waits for a reviewed local owner-channel design (B4). 2. **Fake engines.** They model the pinned docs. The real-binary smoke covers framing only. 3A makes no model call, so real-engine behavior is unproven until CHAT-06. 3. **B2 untouched.** Tool isolation and trust and settings parity (CHAT-00 line 195) are untouched. Any real engine for a real seat waits on B2. 4. **Containment.** The cgroup covers local processes. Network and remote effects stay `uncertain`. 5. **Takeover has nothing to recover.** With no broker queue, a takeover has no queued input to turn into drafts. Q13 is CHAT-04's. ### Out of scope - Durable queues, drafts, uploads, queue edit and cancel, takeover-to-draft (CHAT-04). - Remote control (CHAT-04R) and the chat UI (CHAT-05). - The candidate, cutover and any real seat migration (CHAT-06, CHAT-07). - Changes to `tools/tmux`, the board, the seat package, launchers, `roles/`, `contracts/` and the `docs/plans/chat-0*` contracts. - The CHAT-03I fleet-communications charter (CHAT-01 lines 232–238). - Authenticated multi-actor control and the local owner-channel design (B4). - The Claude adapter and history, unless B1 passes. - Filbert's idsDigest note, which stays in `agents/dewey/work/chat-02/FOLLOWUPS.md`. ### Gate - **Brief.** - Filbert approves the exact `BRIEF.md` hash. - Rocko's pass on §5 and §6 leaves no blocking finding open. - Sage commits and pins the brief. Darkwing then names the author. - **Each increment.** - Filbert approves the exact candidate hashes. Rocko passes the §5 and §6 code in 3A and the B1 packet in 3C. - Every §10 suite is green on an index export, and every mutant is killed. - Sage commits. A push needs Jason's word. - **3C.** Jason's go comes before any model call. - **CHAT-03 done.** 3A and 3B are approved and committed, and either 3D is approved, or B1 is recorded as not passed, with Claude still refusing and CHAT-06 blocked. - **No live check.** No real seat migrates, so there is none. The live proof belongs to CHAT-07.