701 lines
42 KiB
Markdown
701 lines
42 KiB
Markdown
# 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.
|