42 KiB
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 importreplyToRowread-only by relative path, the same way the queue-as-data plan importsscan.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,approvalandconfirmation. The commandsobserve,prompt,takeover,acquire-recovery-control,approval,interrupt,force-stop,recover,issue-confirmationandanswer-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-draftneed 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
confirmandselectwith exact labels;inputandeditorstay 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
bindingfields 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 rpcwith 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.mdlines 30–38). Nodereadlineis 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 sendsstreamingBehavior,steerorfollow_up, so mediated input never enters Pi's native queues (rpc.mdlines 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,uncertainorstopped; - 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
activeclaim 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
stoppedwith a boot proof. Tool calls with no recorded end becomeuncertaineffects. - 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
uncertainuntil the cohort observation in §6 shows no member left.
- Different boot ID. Every process of that boot is gone. That is
proof for the whole cohort, so the claim moves to
- 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 --sessionon the same file. Admission closes, the binding becomesuncertain, 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_endarriving beforeagent_settled, and retries;clear_queueandabort;- 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
pistarts in--mode rpcin a scratch directory, with no auth file and no model call. It answersget_state,get_commands, andclear_queueandabortwhile 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
eventrecords: message-start, text and thinking deltas, tool-start, tool-update and tool-end, message-end withupdateMode: replace, and run-settled.- Every event carries the execution incarnation and a sequence number for that execution.
- An unknown native event becomes an
unavailableevent. It is never dropped silently and never passed through raw. agent_settledmaps 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:
- Interrupt and force stop close admission first, then send
clear_queue, thenabort, in that order.abortruns any queued messages still in the session (rpc.mdline 158). - At most one dispatched item can lack a started user message. If
clear_queuereturns exactly that item's text, exactly once, the item is proven unconsumed. - Until C-1 lands, a proven-unconsumed item keeps its actual receipt state
(
acknowledged). The turnProof records thatclear_queuereturned 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. - 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.
- If
clear_queuefails or times out, the turnProof recordsnativeQueue: unknown, the stop staysuncertain, 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.shpastes onto whatever the composer holds. The mediated path has no engine-side composer. Each prompt is one JSONL record, and itsmessagefield 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.mdlines 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-policywhen 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 RPCpromptinterprets, and the list becomes a fixture. Admission refuses each listed prefix, and any prefix the author can't classify.
- Repository seats load one extension command,
- 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; testserve.test.mjs:810). CHAT-03 relies on that and changes neither the board nortools/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
setsidstays in it. If a scope can't be created, the cohort falls back to the process group. A stop there can reachuncertainbut neverstopped, because asetsidchild can escape. Tradeoff: this needssystemd-run --user, and the fallback is honest rather than equivalent. -
Proof.
stoppedneeds 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_settledand idle never promote a stop tostopped(line 325). -
Effects. A tool-start with no tool-end at stop time is an
uncertaineffect. Killing never counts as rollback (line 334). -
Interrupt (Q20). Close admission,
clear_queue,abort, wait for settle, build the turnProof, recordreconciled, 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_stateandget_treeand 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.mdlines 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:
confirmandselectrender the exact native labels and answer with the exact request ID;inputandeditorstay 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_toolmaps 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:
-
Pin. The exact version string and the sha256 of the binary the adapter will run. Today
claude --versionreports 2.1.283; the plan saw 2.1.269. The adapter checks both at launch and otherwise refuses withengine-pin-mismatch. It runs the engine with auto-update off, and the author cites the setting from the pinned docs. -
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. -
Recordings. Made from that binary, in a scratch config directory with no repository credentials:
- initialize and its capability answer, including whether
interrupt_cancel_queued_v1andpending_permission_requestsexist; - a text prompt and an image prompt;
- a
can_use_toolallow 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.
- initialize and its capability answer, including whether
-
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
--resumecontinues the leaf the parser picks. -
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, noCLAUDE_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-idandlaunches/*/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.mjsandchat-01c/check.mjs;- the eight
scripts/test-*.shsuites.
-
Nested runners. A test that starts
node --testitself clearsNODE_TEST_CONTEXTfor 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,~/.claudeor 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:
- dispatch skips the generation recheck;
abortis sent beforeclear_queue;- a claim file is created without exclusive create;
- the late-event filter ignores the incarnation;
- the text policy checks only the first character, missing leading whitespace;
- an acknowledged SIGTERM promotes a stop to
stopped; - the process-group fallback can reach
stopped; - a confirmation is not consumed on use;
- disconnect releases control;
- the composer isn't cleared on transfer;
- 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
- 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).
- 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.
- 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.
- Containment. The cgroup covers local processes. Network and remote
effects stay
uncertain. - 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 thedocs/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.mdhash. - Rocko's pass on §5 and §6 leaves no blocking finding open.
- Sage commits and pins the brief. Darkwing then names the author.
- Filbert approves the exact
- 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.