docs(records): row 5 round 1 review files, record-issue gap in DEFERRED

Both reviewers requested changes (26681, 26683). Darkwing's verdict
landed on #1508; pointer 26685 on #1507.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-04 14:01:37 -05:00
co-authored by Claude Opus 5.5
parent 14562ac227
commit 8a3e672f4a
4 changed files with 436 additions and 0 deletions
@@ -0,0 +1,265 @@
# Queue row 5, CHAT-03 increment 1, round 1 review
Filbert, 2026-10-04. Request: #1507 comment 26671 (Dewey). Brief:
`agents/dewey/work/chat-03/BRIEF.md`, sha256 1ef15ac0…, with lead decisions
30–34 and 36. Candidate: `agents/dewey/work/chat-03/I1-manifest.sha256`,
digest `1404341eaeaf7d1e684c9f27e76718061ed08c25a52da5f190425feb1274ba69`,
27 files, uncommitted.
My part is the whole brief except the binding and the extension-load
refusal, which Darkwing reviews (`pi-pin.mjs`, `claim.mjs`, `guard.mjs`,
`SEAL_BASIS`, the K8 load check, takeover and generation; N24, K6/K8 pin
side, W1–W20, G1–G3, H1–H4, mutants 37/37b).
Verdict: **changes requested.** Six blocking findings, B1–B6. Two are
defects in the terminal, one is a defect in the force-stop chain, and three
are missing tests that leave a brief or lead-decision property unpinned.
The rest are notes. None of my findings repeats Darkwing's B1 or B2.
## How I checked
- Frozen copy in a scratch clone of 1c724958 (`/tmp/f5r1/repo`, push URL
`DISABLED`, candidate files read-only). The manifest verifies there and
in the canonical working tree. Every changed path is under
`packages/conversation/` (Files owned). `git diff 1c724958 -- docs
contracts roles` is empty and the brief's 12 contract hashes match.
- Export: `git archive` of 1c724958 plus the 27 files, manifest OK. Every
§10 suite was run on it, each with its own log:
- conversation 141/141, 0 skipped; control-board 124; webui 14; seat 19;
- CHAT-00 48 checks; CHAT-01 R3 PASS; CHAT-01c PASS;
- auth 15, config 24, discord 64, extension-package 18, foundation 44,
release 14, task 90;
- queue 27/0 with node 148/148. The two live-queue checks skip outside
the canonical root and say so, which accounts for Dewey's 29;
- conductor fails on the export only because the export isn't a git
repository (`fatal: not a git repository`). In the scratch clone
holding the candidate it passes 17/17.
- Three scoped deep reads, each in its own scratch copy: turns (§3, O1–O6,
N1–N25, decision 34); stop and recovery (§6, K1–K18, H10, H17, H20–H23);
transport, slash, terminal and seam (§1, §4, §9, H11–H19, E1–E7). I
reran every blocking result myself against the frozen candidate before
listing it.
## Blocking
### B1. Text typed after Enter in the same input chunk joins the submitted message
`terminal.mjs` 125–133 and 157–158. `key()` appends characters to the
composer at once but queues the Enter action until the loop ends, so
`submit()` reads the composer after the rest of the chunk has gone in.
Rerun on the frozen candidate (`/tmp/f5r1/b1probe.mjs`, a recording fake
client): `key("first\rsecond\r")` sends one prompt, `["firstsecond"]`; the
second Enter sends nothing. `key("abc\rdef")` sends `["abcdef"]`.
This is the concatenation hazard §4 exists to remove ("cleared after each
submit"; README "It clears after each submit"). One chunk holding Enter and
more text is ordinary: tmux `send-keys "x" Enter "y"`, typeahead while the
event loop is busy, or a terminal without bracketed paste. Fix: snapshot
the composer at the Enter (submit that text, clear, then keep reading the
chunk), and add a test with text after Enter in one chunk.
### B2. A paste-start marker split right after its ESC loses paste mode, and an Enter inside the paste submits
`terminal.mjs` 114. The carry needs `s.length - i >= 2`, so a lone trailing
`\x1b` is consumed as an Escape key. Rerun: `key("\x1b")` then
`key("[200~a\rb\x1b[201~")` sends `["[200~ab"]`. Splits at other offsets
inside the marker are handled.
README: "A bracketed paste is inserted literally, newlines included, and
never submits by itself." The read boundary has to fall exactly after the
ESC, so this is rare, but the fix is cheap: a lone Escape has no action in
this terminal, so carrying a trailing lone `\x1b` costs nothing. Add the
split case to the paste test.
### B3. A second force stop runs a parallel escalation and the first one's phase is written onto the second stop's record
`controller.mjs` 657–665 admits `force-stop` while `b.state` is
`stopping`. `#forceStop` for the second stop writes
`stop: { id: <second>, phaseStarted: null }` into the claim, while the
first escalation is still running. The first one's `onPhase` then writes
`{ ...r.stop, phaseStarted: name }`, which is the second stop's record. The
superseded check runs only after `stopCohort` returns (1454), so both
escalations signal, freeze and kill at once.
The stop-and-recovery read proved it with pause hooks (log kept at
`/tmp/f5r1-cohort-logs/XFS2-1.txt`, three runs): the claim shows the second
stop's ID with `phaseStarted: "kill"` while the first is held at the kill
phase and the second hasn't run TERM. I read the code path and it matches.
All three runs ended `stopped` with the child dead, so no false proof was
seen. But §6 makes the stopping revision the record of which stop reached
which phase before any signal, and here it is wrong. Two concurrent
freeze/enumerate/kill passes on one cgroup are also outside what the proof
reasoning covers.
Fix: one force-stop escalation at a time. Either refuse a second
`force-stop` while one is running (it can be retried once the first ends
`uncertain`), or mark the first superseded and wait for it before starting
the second. Add the race to `races.test.mjs` with engine bytes, the claim
revisions and events asserted.
### B4. Nothing tests lead decision 34
Decision 34: "If the code ever sees `aborted` with no stop in progress, it
treats it as an overlap signal. … This goes into the build and its tests."
The check is at `turns.mjs` 163. I replaced that line with a comment in a
copy of the export and ran the whole conversation suite: 141/141 pass
(`/tmp/f5r1/logs/mutant-d34.log`). No test mentions `aborted-without-stop`.
The check itself works: the turns read drove an `aborted` with no stop and
got `["aborted-without-stop"]`, binding `uncertain`, receipt outcome
unknown. Add that as a fixture and add the mutant to the table.
### B5. K12 doesn't prove the freeze
Brief line 1117: "The freeze stops forking. The enumeration is complete."
I made the shim skip the `cgroup.freeze` write and answer `frozen 1`
anyway. The whole cohort suite passes, 18/18, 0 skipped
(`/tmp/f5r1/logs/mutant-C3-cohort.log`). The stop-and-recovery read got the
same result for a freeze that is written but not waited on. K12 asserts
only the end state, and `cgroup.kill` alone reaches that state.
Fix: assert `cgroup.frozen` is 1 before enumeration, and that no member
outside the proof's list existed after the freeze (for example from the
fork loop's own pid log). Add the no-freeze mutant to the table.
### B6. K15 has no "engine path missing" case
Brief line 1120 lists "`engine` cgroup path missing or unreadable, or the
shim gone"; §6 says "an absent `engine` cgroup is an absent observation,
never an empty one." The test covers the shim gone and `chmod 000` on
`cgroup.events` (EACCES), not a missing path. I made the shim's `events()`
answer `populated 0` on ENOENT only. The cohort suite passes 18/18
(`/tmp/f5r1/logs/mutant-D-cohort.log`). The mutation table's 21 is a
different, broader variant that the EACCES case does kill, so I'm not
disputing the table, only the coverage.
The code does fail closed here today, through the TERM-phase enumeration
and the freeze write (the stop read removed the `engine` cgroup and got
`uncertain`, "engine enumeration failed (ENOENT)"). Pin it with a missing
`engine` fixture.
## Notes (not blocking)
Turns:
- **n1.** An `aborted` that arrives after the fence but before the first
abort is written is linked to the stop: receipt `failed`, reason
`interrupted`, no overlap signal, binding stays `active` (stop outcome
Unknown, so nothing reconciles). Decision 34's wording ("no stop in
progress") allows this, but under the seal an `aborted` before the
controller's abort isn't the controller's. Recommend linking only after
an abort was written (`ctx.abortIdxs` non-empty).
- **n2.** An O5 that arrives in the same chunk as the empty clear response
is recorded before the abort, yet the abort is still sent and the queued
item runs (`controller.mjs` 1278–1284; the overlap check is only at the
top of each round). N6 accepts this outcome. Rechecking `tr.overlapped`
after the `before-abort` pause costs one line and fits rule 2's reason.
- **n3.** On a non-empty clear or an abort failure, `stop.nativeQueue`
keeps its initial value instead of `unknown`. No turnProof is built on
any uncertain path, so "the turnProof records `nativeQueue: unknown`"
(rule 2) lives only on the stop record.
- **n4.** After a K9 no-settle the stop is `uncertain`, so a later
`aborted` from the controller's own abort raises `aborted-without-stop`.
After a force stop supersedes an Interrupt, an Interrupt-caused `aborted`
links to the force-stop ID. Labels only.
- **n5.** Test gaps: `rounds-exhausted` has no test; N25 asserts only that
O5 is present; no test for O5 from a `get_state` pending count, or O2
"before the slot's ack" and "a second settle for one ack".
- **n6.** A user `message_start` with no current run and an `agent_end`
with no open start are counted but raise no signal. Not in O1–O6, so not
a brief violation.
Stop and recovery:
- **n7.** Resuming from TERM after a restart isn't asserted: skipping TERM
when `phaseStarted` is `kill` passes K10 and K11. K3 doesn't assert TERM
was delivered.
- **n8.** K13: the README says the refusal is "not a controller check", but
the cgroup namespace is the shim's `unshare --cgroup` (removing it fails
K13). Nothing checks at runtime that cgroup2 is mounted `nsdelegate`; on
a host without it, stops would still record `stopped`. The brief only
asks that K13 shows the refusal on this host, so this is a portability
note for the cutover increment.
- **n9.** A member can ask the user systemd manager for a new unit outside
the scope (`systemd-run --user` from inside a user and cgroup namespace
lands in `app.slice`). Same-uid escape, Limits 3/4; worth a line in the
packet's limits.
- **n10.** The shim channel is not authenticated: the engine sees the
scope's `INVOCATION_ID` and can reach the shim socket, so a same-uid
engine that kills the shim could answer as it. Limit 1; the claim could
record the shim's pid and start time to narrow it.
- **n11.** An orphan's tool map is taken at restart, not at stop time.
Low impact.
- **n12.** H17 tests a confirmation used after the stop changed, not one
answered after it changed; answering doesn't check the stop, using it
refuses, so the outcome matches. A freeze or kill timeout leaves the
cgroup frozen (fails closed).
Transport, terminal, seam:
- **n13.** `visible()` passes U+061C (a bidi control the README says is
made visible), U+200B and other zero-width characters, U+FEFF, tag
characters U+E0000–E007F and LF in the status and head lines. None can
drive the terminal; they can hide or spoof text.
- **n14.** `LineSplitter`'s limit isn't checked for a line whose LF arrives
in the same chunk: with `maxBytes` 100 a 590-byte line is accepted. The
overshoot is at most one chunk.
- **n15.** For I3: the 5 s ack timeout can be short for real Pi, whose
`prompt()` may refresh OAuth or run auto-compaction before it acks. That
fails closed (poison, force stop) but the fake doesn't model it.
- **n16.** Compaction summaries are rendered on pages but not streamed, and
a quiet cut doesn't re-read after `run-settled`, so a summary appears
only after a reconnect or Ctrl-O (E4 "exactly once").
- **n17.** Two interrupts with one request ID in flight: the second gets
`fenced` instead of the first's outcome. The client has no
`onOverflow`, and the controller's socket writes have no backpressure.
- **n18.** Test gaps: no test that a socket directory that isn't 0700 or is
a symlink refuses; none for an overlong or split multibyte engine line;
the escaping test covers only ESC, BEL and U+202E; H12 and H13 don't
assert events (brief line 962: "Every race asserts the engine bytes, the
receipts and the events").
README:
- **n19.** "The scope launcher stops the cgroup with SIGKILL, because the
shim ignores SIGTERM by design" contradicts the code, which runs the
brief's TERM phase on `engine` members and then freeze and `cgroup.kill`.
The shim ignoring SIGTERM only keeps the scope's anchor alive.
- **n20.** The pieces table says the shim "answers `proof`"; its ops are
hello, events, members, term, freeze, kill and release.
## Checked and found correct
- Rule 4 receipt table, rule 5 stop outcomes, rule 6 reopening only on
Interrupted with every reconcile condition required; O1–O6 present; clear
failure sends no abort; `no-turn` lifts only its own fence; N1–N25 each
have a test.
- Force stop: the stopping revision before any signal; the invocation ID
checked through systemd and the shim; TERM phase then freeze, enumerate,
`cgroup.kill`, `populated 0`; `membershipComplete` only after all of
them; unavailable evidence ends `uncertain`; the pgroup fallback never
reaches `stopped`; restart re-runs from TERM.
- Recover: verified proof, unchanged pins and leaf, consumed confirmation,
single-use eligibility bound to the incarnation, launch rechecks it
(K6, K7, K17, K18). Foreign host refused, boot proof verified.
- Framing splits bytes at LF only and survives multibyte splits; the socket
directory is 0700, lstat-checked, owned, before listen; connections start
as observers; `stale-incarnation` first; the client never resends;
dedup by actor, conversation and request ID with `conflicting-request`;
`busy` decided under the lock; three write outcomes with poisoning; no
`streamingBehavior`, steer or follow-up is ever sent.
- Slash policy matches pinned Pi (`rpc-mode.js` 298–318,
`agent-session.js` 821–948): only a leading `/`, `/skill:` and templates
are interpreted, Pi doesn't trim, so refusing after whitespace is
conservative; `!`, `!!`, `@` and later-line `/` are admitted correctly.
- The quiet-cut rule holds for runs: Pi persists `message_end` before
`agent_settled`, the page read is under the lock.
- `ControlRefusal`, the four new package exports and the cross-package
`journal.mjs` import (same pattern as `packages/queue`).
## Round 2
I'll review the touched files and rerun the mutants above (decision 34,
no-freeze, ENOENT-as-empty) plus the B1–B3 probes against the round 2
candidate.