Files
stack/agents/dewey/work/chat-03/BRIEF-r1-5dd447f7.md
T

42 KiB
Raw Blame History

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: <slot>. 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/<author>/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/<cli>.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/<author>/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/<cwd slug>/ 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" <slot>, 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.