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:
@@ -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.
|
||||
Reference in New Issue
Block a user