feat(conversation): CHAT-03 I1, mediated control of a sealed headless Pi (#1507)

Controller, claim store, live-session guard, engine link and seal,
turn tracker, cohort force stop and recovery, client library,
transcript and mediated terminal, with the fake engine and tests.
Fixtures only; no live cutover.

Dewey built it. Darkwing (comment 26690) and Filbert (comment 26694)
approved round 2. Manifest I1-r2-manifest.sha256 (2b48e333, 27 files).
Suites on an export: conversation 152/152, control-board 124, webui 14,
seat 19, chat-00/01/01c checks, and all nine scripts/test-*.sh green.
Follow-ups for I3 are in DEFERRED. Gate E stays with Jason.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-04 15:47:53 -05:00
co-authored by Claude Opus 5.5
parent ddd9cf3635
commit 243e153c8b
35 changed files with 10317 additions and 11 deletions
@@ -0,0 +1,140 @@
# CHAT-03 I1, row 5, round 2 review (Filbert)
Issue #1507. Candidate: manifest `agents/dewey/work/chat-03/I1-r2-manifest.sha256`,
sha256 `2b48e333a0f09185364359ae6f8277cc88c0b9ff39058de45cc2c1f0ec9d5c4a`,
27 files on base `1c724958`. Packet: `agents/dewey/work/chat-03/BUILD-I1-r2.md`.
Round 1 review: `agents/filbert/work/chat-03-i1-review-r1-2026-10-04.md` (comment 26683).
Verdict: **approve.** All six of my round 1 blockers are fixed. Three
follow-ups below; none of them blocks the commit.
## Method
- Scratch clone at `~/filbert-scratch/f5r2/repo` (push URL `DISABLED`),
checked out at `1c724958`, plus the 27 candidate files copied from the
canonical checkout. `sha256sum -c` on the manifest: 27 OK.
`node_modules` is a symlink to the canonical checkout's, as in round 1.
- Round 1 to round 2 diff (`r1-r2.diff`, against my round 1 export, which
matches the round 1 manifest): 11 files changed. `cohort.mjs` and
`shim.mjs` are unchanged since round 1.
- Mutants run in a separate copy (`~/filbert-scratch/f5r2/mut`) with the
same 27 files. Baseline there: 152/152.
## Suites (one run each, in the clone)
| Suite | Result |
|---|---|
| `node --test packages/conversation/tests/` | 152 pass, 0 fail (49.7 s) |
| control-board | 124 pass, 0 fail |
| webui | 14 pass, 0 fail |
| seat | 19 pass, 0 fail |
| chat-00 / chat-01 / chat-01c checks | 48 checks / R3 PASS / PASS |
| test-auth, conductor, config | 15, 17, 24 passed, 0 failed |
| test-discord, extension-package, foundation | 64, 18, 44 passed, 0 failed |
| test-queue | 27 passed, 0 failed (node 148/148) |
| test-release, test-task | 14, 90 passed, 0 failed |
test-queue's 27 against Dewey's 29: two checks (`queue verify` and
`render --check`) skip outside the canonical root. The node part matches.
My first conversation run had no `node_modules` and refused
`engine-pin-mismatch` everywhere; that was my setup. The log is kept as
`conv-0-no-node_modules.txt`.
## Round 1 blockers
| # | Finding | Check | Result |
|---|---|---|---|
| B1 | Text after Enter joins the message | Round 1 probe: `key("first\rsecond\r")` | `["first","second"]` (round 1: `["firstsecond"]`). Mutant TB1 (send reads the composer when it runs): killed by the new flows test |
| B2 | Paste-start split after its ESC | Round 1 probe: `key("\x1b")`, then `key("[200~a\rb\x1b[201~")` | Nothing sent; stays a paste (round 1: `["[200~ab"]`). Mutant TB2 (round 1's `>= 2` carry rule): killed |
| B3 | A second force stop runs a parallel escalation | Read `controller.mjs` 680–690 and 1451–1460; new H10 test | Fixed. The new H10 test asserts one stop record, the confirmation not consumed, `launcher.stops` 1, every claim revision on the first stop and no engine bytes. Mutant (drop `\|\| this.escalating`): H10 and H17 fail |
| B4 | Decision 34 untested | Round 1 mutant: `turns.mjs:163` disabled | Killed: both new N9 tests fail (150/152) |
| B5 | K12 doesn't prove the freeze | Round 1 mutant C3: no freeze write, answers `frozen 1` | Killed by K12's `cgroup.freeze` read. The code waits for `frozen 1` before enumerating (below). The remaining test gap is follow-up F1 |
| B6 | K15 has no missing-path case | Round 1 mutant D: ENOENT in `events()` answers `populated 0` | Killed by the new third K15 block (17/18) |
### The freeze path (Sage's ruling)
I read the code myself:
- `shim.mjs` `freeze` op: it writes `engine/cgroup.freeze`, then
`waitFor(e => e.frozen === 1 || e.populated === 0, timeoutMs)`. That
polls `engine/cgroup.events` every 10 ms. An unreadable file returns
`ok: false`, and the timeout returns `timedOut: true`.
- `cohort.mjs` 181–185: `freeze` is sent with `timeoutMs: 3000` and a
6000 ms client limit. `!frozen.ok` or `frozen.timedOut` returns
`unavailable` at phase `kill` before `members` is called. `members` runs
only after a `frozen 1` (or `populated 0`) was read.
So the controller does wait for the frozen state before it lists the pids.
Under the ruling, the missing test is a follow-up and doesn't block.
## Darkwing's blockers (touched files only; the verdicts are Darkwing's)
- B1 seal. `checkSeal` is an allow-list. A direct probe of
`buildPiArgs` + `checkSeal` with 24 extra-argument lists accepted only
the three that use `--model`, `--provider` and `--thinking` with plain
values. It refused `--session /outside`, `--mode json`, `--no-session`, a
repeat, a `-x` or `@f` value, a missing or empty value, a bare word,
`@file`, `--model=m`, `--approve`, `--fork`, `--export`, `--print`, `-p`,
`-e`, `--extension`, `--continue`, `--api-key`, `--system-prompt` and
`--tools`. A relative session path refuses too. Note for Darkwing: a
caller can keep the default `engine.command` and pass its own `preArgs`,
for example the Pi `cli.js` path plus `--no-session`. Only `--extension`
in `preArgs` is checked, so that argv reaches real Pi unsealed. The README
documents a non-default `preArgs` as a test hook, and I1 has no
production caller. I list it as a follow-up (F3), not a blocker.
- B2 session key. The key is the header ID, read at construction. Every
later read refuses `target` if it changed, and a missing file now refuses
`configuration` instead of throwing. `claim.mjs` `sessionKey` is
unchanged and takes any value. The three new W4 tests pass.
## Round 1 notes
n1, n2 and n3 are code changes. I read them: `stopLink` walks the
supersede chain for an abort written, the re-check after `before-abort`
covers `overlapped`, `gap` and `poisoned`, and `#stopUncertain` sets
`nativeQueue` to `unknown`. Dewey's r2-n1, r2-n2 and r2-n3 mutants are in
the table; I didn't rerun them. Dewey's dispositions of n4–n20 are
acceptable as stated.
## Follow-ups (none blocks the commit)
- **F1. No test proves enumeration happens under the freeze.** The packet
says both K12 checks "don't depend on timing". That's true of the
`cgroup.freeze` read, but the read only proves the file was written at
some point. I added mutant C4: no write at the `freeze` op, an answer of
`frozen 1`, and `cgroup.freeze` written just before `cgroup.kill`. It
survives the cohort suite 3 out of 3 runs (18/18 each). The pid-log check
catches a missing freeze only if the 5 ms fork loop forks inside the gap
between `members` and `kill`, which is about one socket round trip.
Dewey's r2-B5b (written, not waited on) also survives here (18/18).
Proposal: a test-only hold after `members` and before `kill` would let
the loop fork into the gap, which makes C4 fail every time. r2-B5b still
needs the FUSE or privileged fixture already proposed.
- **F2. Takeover and Enter in one input chunk.** `key("\x14hi\r")` from an
observer: round 1 took over and sent `hi`. Round 2 takes the composer
when it parses the Enter, which happens before the queued takeover runs.
The Enter is refused as observer, so `hi` stays in the composer and is
sent on the next Enter. Nothing is sent that shouldn't be; it costs an
extra keypress. Fix: either document it or check control when the send
runs (TB1 already holds the text).
- **F3. Overriding `preArgs` with the default command** (see Darkwing B1
above). Refuse a non-default `engine.command` or `preArgs` unless an
explicit test option is set, so a production caller can't use the test
hook by accident.
## Mutants run in this round
| Mutant | Target | Result |
|---|---|---|
| B3-no-escalating-fence | `controller.mjs` force-stop guard | killed (H10, H17) |
| D34-line163 | `turns.mjs:163` | killed (two N9 tests) |
| TB1-send-at-run | `terminal.mjs` Enter | killed (flows) |
| TB2-lone-esc | `terminal.mjs` ESC carry | killed (flows) |
| C3-no-freeze-answer-frozen | `shim.mjs` freeze | killed (K12) |
| C4-freeze-at-kill ×3 | `shim.mjs` freeze moved to the kill | survived 3/3 (F1) |
| C2-no-wait-frozen | `shim.mjs` freeze not awaited (= r2-B5b) | survived (F1) |
| D-enoent-populated0 | `shim.mjs` `events()` | killed (K15) |
Logs: `~/filbert-scratch/f5r2/logs/` (`mut-*.txt`, `mut-summary.txt`,
`conv-1.txt`, `summary.txt` and one log per suite). Probes:
`b1probe.mjs`, `sealprobe.mjs` and `tb-edge.mjs` in `~/filbert-scratch/f5r2/`.