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:
@@ -0,0 +1,305 @@
|
||||
# CHAT-03 I1 build, review request round 2 (#1507, row 5)
|
||||
|
||||
From Dewey, 2026-10-04. Both reviewers asked for changes in round 1. Darkwing's
|
||||
verdict is comment 26681 (queue rev 53) and Filbert's is comment 26683 (queue
|
||||
rev 54). This is the last round (item 27): if blocking findings remain, Sage
|
||||
cuts scope. The round-1 packet `BUILD-I1.md` still holds except where this
|
||||
file says otherwise.
|
||||
|
||||
Darkwing's review and Sage's message cite #1508 for comment 26681. The row's
|
||||
issue is #1507, and Filbert's verdict is on #1507. I'm flagging the mismatch,
|
||||
not resolving it.
|
||||
|
||||
## Candidate
|
||||
|
||||
- Manifest: `agents/dewey/work/chat-03/I1-r2-manifest.sha256`, sha256
|
||||
`2b48e333a0f09185364359ae6f8277cc88c0b9ff39058de45cc2c1f0ec9d5c4a`, the same
|
||||
27 files as round 1.
|
||||
- Base `1c724958`, as in round 1. `git diff 1c724958 HEAD` shows no change
|
||||
under `packages/conversation`, `docs/plans/chat-0*`, `contracts/` or
|
||||
`roles/`, so the round-1 export method still works: `git archive
|
||||
1c724958` plus the 27 files.
|
||||
- Nothing is staged or committed.
|
||||
|
||||
## Darkwing's findings
|
||||
|
||||
- **B1, the seal is a deny-list.** `checkSeal` (`src/pi-pin.mjs`) is now an
|
||||
allow-list. The argv must start with exactly `--mode rpc`, the three
|
||||
`--no-*` seal flags and `--session <absolute path>`. After that only
|
||||
`--model`, `--provider` and `--thinking` (`ENGINE_OPTIONS`) may follow,
|
||||
each at most once with one value. A value may not be empty or start with
|
||||
`-` or `@`. Anything else refuses `unsealed-engine` before spawn. That
|
||||
covers a second `--mode` or `--session`, every session or output flag you
|
||||
listed, `--approve`, `--no-extensions` repeated, a bare word (a prompt) and
|
||||
`@file`. A non-list `engine.preArgs` or `engine.extraArgs` refuses too. A
|
||||
non-default `engine.command` or `preArgs` is documented as a test hook:
|
||||
the pin and the seal don't bind under it, and `argvDigest` records the
|
||||
whole command line (README "Engine command"). N24 now has cases for
|
||||
`--session`, `--mode json|text|rpc` and `--no-session` in extraArgs, plus
|
||||
the rest of the list. Your repro `/tmp/r5/seal-escape.mjs session|json`
|
||||
now refuses at construction; no Pi process starts.
|
||||
- **B2, the session key is the conversation ID.** The session key is the
|
||||
Pi header ID, read at construction (`controller.mjs`, constructor). A
|
||||
later read refuses `target` if the header ID changed. New controller-level
|
||||
W4 tests: a hard link and a copy of one session under another seat. The
|
||||
second controller refuses `already-active` and launches nothing. On a copy:
|
||||
the brief keys on the native session identity, and a copy carries the same
|
||||
header ID, so it counts as the same session. Your
|
||||
`/tmp/r5/hardlink.mjs` gives B `already-active` with 0 launches.
|
||||
- **n1.** The startup-append finding now states Pi's rule
|
||||
(`messages.length > 0`) and names both cases (README "Findings against
|
||||
pinned Pi", corrected below).
|
||||
- **n2.** README "Live roots" says the guard follows `$HOME`.
|
||||
|
||||
## Filbert's findings
|
||||
|
||||
- **B1, text after Enter joins the message.** The composer is taken at the
|
||||
Enter key, so text after it in the same chunk starts the next message
|
||||
(`terminal.mjs` `key`, `#take`). New flows test: `first\rsecond\r` sends
|
||||
two prompts; `abc\rdef` sends `abc` and leaves `def` in the composer.
|
||||
- **B2, a paste-start split after its ESC.** A lone trailing ESC is carried
|
||||
to the next chunk; a lone Escape has no action, so holding it costs
|
||||
nothing. New flows test: the marker split at every cut 1–5 is still a
|
||||
paste.
|
||||
- **B3, a second force stop runs a parallel escalation.** One escalation
|
||||
runs at a time (`controller.escalating`). A second force stop while one
|
||||
runs refuses `fenced` and doesn't consume its confirmation. Once an
|
||||
escalation ends `uncertain`, a fresh confirmation can retry. New H10 test:
|
||||
hold at `phase-term`, a second force stop is fenced, `launcher.stops` is 1,
|
||||
and every claim revision carries only the first stop's phases. Its second
|
||||
block retries after an `uncertain` end. H17's reuse-during-the-stop case
|
||||
now expects `fenced`.
|
||||
- **B4, decision 34 untested.** New N9 test: a run that ends `aborted` with
|
||||
no stop in progress raises `aborted-without-stop`, the binding goes
|
||||
`uncertain` with admission closed, and the receipt is outcome unknown
|
||||
(`run-overlap`), never `failed`. Mutant r2-D34 (your line 163) is in the
|
||||
table.
|
||||
- **B5, K12 doesn't prove the freeze.** K12 now checks two things that don't
|
||||
depend on timing. First, `engine/cgroup.freeze` still reads 1 after the
|
||||
stop; the shim holds the scope, so a freeze never written reads 0. Second,
|
||||
the fork loop logs each child's pid and its own SIGTERM. Every child forked
|
||||
after that TERM, which nothing ends before the kill, must be in the
|
||||
proof's member list, and there must be at least one. Mutant r2-B5 (no
|
||||
freeze write, `frozen 1` answered) is killed. Mutant r2-B5b (freeze
|
||||
written, not waited on) survives, and `frozen 1` is not asserted before
|
||||
enumeration. Both are explained under "Mutation pass".
|
||||
- **B6, no missing-path case in K15.** A third K15 block moves the engine's
|
||||
processes to a sibling cgroup and removes `engine`. `events` and
|
||||
`members` both answer unavailable with ENOENT, the force stop ends
|
||||
`uncertain` with no proof, and the engine is still alive. Mutants r2-B6
|
||||
(your ENOENT-as-`populated 0`) and r2-B6b (ENOENT as empty for both
|
||||
`events` and `members`) are in the table.
|
||||
- **n1.** An `aborted` links to the stop in progress only once an abort was
|
||||
written in that stop's chain. New N9 test: an `aborted` after the fence
|
||||
but before any abort is the overlap. Mutant r2-n1. The fix hid round-1
|
||||
mutant 31 from N14, so a second N14 test now has the run complete after
|
||||
the abort is written; the receipt stays `finished`.
|
||||
- **n2.** The interrupt checks `tr.overlapped`, the gap and the poisoned
|
||||
link again after the `before-abort` pause. New H10 test: an O5 read during
|
||||
the pause means no abort and the queued item never runs. Mutant r2-n2.
|
||||
- **n3.** A stop that ends `uncertain` with `nativeQueue` still `pending`
|
||||
records `unknown`. N1 (non-empty clear) asserts it. Mutant r2-n3.
|
||||
- **n8.** README "Escape hatches" now says K13's refusal comes from the
|
||||
shim's `unshare --cgroup` on an `nsdelegate` host, and that nothing checks
|
||||
`nsdelegate` at runtime.
|
||||
- **n13.** `visible()` now also shows U+061C, U+200B, U+2060–U+2064, U+FEFF
|
||||
and tag characters U+E0000–U+E007F. ZWJ and ZWNJ pass, because emoji
|
||||
sequences and joining scripts need them. The header, status, notices and
|
||||
dialogs show LF as `^J`. New flows test; mutants r2-n13 and r2-n13b.
|
||||
- **n19, n20.** README force-stop text now follows the code: a TERM phase,
|
||||
then freeze, enumerate, `cgroup.kill`, `populated 0`. The shim ignores
|
||||
SIGTERM only to keep the scope's anchor. The pieces table lists the shim's
|
||||
ops.
|
||||
|
||||
## Notes not taken
|
||||
|
||||
Recorded here as limits or bounded follow-ups. None widens I1.
|
||||
|
||||
- n4 (labels after K9 and after supersession): labels only, as you say.
|
||||
- n5, n18 (test gaps): `rounds-exhausted`, O5 from a `get_state` count, the
|
||||
two O2 variants, the socket-directory refusals, an overlong or split
|
||||
multibyte engine line, events in H12 and H13. Follow-up. n18's escaping
|
||||
gap is partly closed by the new `visible()` test.
|
||||
- n6 (counted, no signal): outside O1–O6.
|
||||
- n7 (TERM resume after restart not asserted; K3 doesn't assert TERM
|
||||
delivery): follow-up test.
|
||||
- n11 (orphan's tool map taken at restart), n12 (answer vs use after a stop
|
||||
change): low impact, outcome matches.
|
||||
- n14 (`LineSplitter` limit overshoot of at most one chunk): follow-up.
|
||||
- n15 (5 s ack timeout for real Pi): for I3. It fails closed.
|
||||
- n16 (compaction summaries appear only after a re-read): follow-up for the
|
||||
seam rules.
|
||||
- n17 (two interrupts with one request ID; no `onOverflow` or backpressure):
|
||||
follow-up.
|
||||
|
||||
## Suites
|
||||
|
||||
Run on the candidate bytes in the canonical checkout, one run each, no
|
||||
reruns for failures. The conversation suite ran twice, both 152/152: the
|
||||
second run came after a one-character fix to N15's title (a space my N14
|
||||
insertion dropped), so the frozen bytes have their own run. Logs are in `~/dewey-scratch/suites/` this session; `/tmp` was full
|
||||
(other seats' files, 17 of 19 GB), so my scratch moved out of it.
|
||||
|
||||
| Suite | Result |
|
||||
|---|---|
|
||||
| `node --test packages/conversation/tests/` | 152 pass, 0 fail, 0 skipped |
|
||||
| `packages/control-board` tests | 124 pass, 0 fail |
|
||||
| `packages/webui` tests | 14 pass, 0 fail |
|
||||
| `packages/seat` tests | 19 pass, 0 fail |
|
||||
| `docs/plans/chat-00/check.mjs` | 48 checks passed |
|
||||
| `docs/plans/chat-01/check.mjs` | R3 PASS |
|
||||
| `docs/plans/chat-01c/check.mjs` | PASS |
|
||||
| `scripts/test-auth.sh` | 15 passed, 0 failed |
|
||||
| `scripts/test-conductor.sh` | 17 passed, 0 failed |
|
||||
| `scripts/test-config.sh` | 24 passed, 0 failed |
|
||||
| `scripts/test-discord.sh` | 64 passed, 0 failed |
|
||||
| `scripts/test-extension-package.sh` | 18 passed, 0 failed |
|
||||
| `scripts/test-foundation.sh` | 44 passed, 0 failed |
|
||||
| `scripts/test-queue.sh` | 29 passed, 0 failed (node 148/148) |
|
||||
| `scripts/test-release.sh` | 14 passed, 0 failed |
|
||||
| `scripts/test-task.sh` | 90 passed, 0 failed |
|
||||
|
||||
Conversation tests went from 141 in round 1 to 152. The new and extended
|
||||
tests are named under each finding above.
|
||||
|
||||
## Contracts
|
||||
|
||||
The twelve contract files in the brief's table (`docs/plans/chat-00`,
|
||||
`chat-01`, `chat-01c`) hash to the brief's values (`sha256sum -c`, 12 OK).
|
||||
The pinned brief hashes to `1ef15ac0ed31…cbc1`. `git diff 1c724958 HEAD`
|
||||
and `git status` show no change under `docs/plans/chat-0*`, `contracts/`,
|
||||
`roles/` or `packages/conversation` outside the 27 candidate files.
|
||||
|
||||
## Mutation pass
|
||||
|
||||
Run in scratch copies under `~/dewey-scratch`, never the served tree. Each
|
||||
mutant is applied alone, then the whole conversation package runs. The
|
||||
round-1 table's 41 mutants were rerun against this candidate, plus 20 new
|
||||
ones that guard the round-1 fixes. All 61 anchors are unique
|
||||
(`mutants.py check`). Timeout is 900 s per run; "killed (timeout after
|
||||
failures)" means tests had already failed when the timer ran out, as with
|
||||
13 and 14 in round 1.
|
||||
|
||||
The pass ran on the candidate minus one test: the N14 abort-pause test
|
||||
below, added after the pass. A test added can only kill more, so every
|
||||
other row stands.
|
||||
|
||||
Two survived the pass:
|
||||
|
||||
- **31**, a run that ended `stop` relabelled `failed interrupted` when it
|
||||
links to a stop. Round 1's N14 killed it. The n1 fix made N14 blind to it:
|
||||
N14's run completes before any abort is written, so `stopLink()` now
|
||||
returns null there and the mutant changes nothing. New N14 test: the run
|
||||
completes after the abort is written but before Pi applies it, and the
|
||||
receipt stays `finished`. It passes on the candidate and fails under
|
||||
mutant 31 (whole package under mutant 31: 151 pass, 1 fail, the new test).
|
||||
- **r2-B5b**, the shim writes `cgroup.freeze` but answers `frozen 1`
|
||||
without waiting. It still survives. On a real cgroup the kernel finishes
|
||||
the freeze within microseconds, so nothing the suite can observe changes.
|
||||
Stopped, traced and vfork-waiting tasks count as frozen, so an
|
||||
unprivileged fixture can't stall a freeze with those. I tried a member
|
||||
spinning under `cpu.max 1000 1000000`. The freeze stalled in 5 of 7 runs
|
||||
and completed in 2, too flaky for the suite, so that test is not in the
|
||||
candidate. r2-B5, Filbert's no-freeze mutant, is killed by K12's
|
||||
`cgroup.freeze` check. Filbert's fix text asked for `frozen 1` asserted
|
||||
*before enumeration*, and the candidate doesn't do that literally. The
|
||||
only seams are the claim's phase names (a new phase changes the claim) and
|
||||
the shim socket path (the controller takes it from the launcher). The wait
|
||||
guards forks still in flight when the freeze is written. Follow-up: a FUSE
|
||||
or privileged fixture that holds a member in uninterruptible sleep, so the
|
||||
freeze can't finish.
|
||||
|
||||
Result: 60 of 61 killed, one survivor (r2-B5b).
|
||||
|
||||
| # | Mutant | Result | Failing tests (first 60 chars each) |
|
||||
|---|---|---|---|
|
||||
| 1 | dispatch skips the generation recheck | killed | H3: a takeover while a prompt holds the dispatch lock: writt |
|
||||
| 2 | abort is sent before clear_queue | killed | H10: an overlap during the pause before the abort: no abort,; H10: Interrupt and force stop together: one stop chain, forc; N14: the run completes while clear_queue is in flight: finis … |
|
||||
| 3 | a revision is published by rename, so an existing revision can be replaced | killed | W1: two processes acquire the same pair at once; exactly one; W1: two writers publish the same revision at once: one wins, |
|
||||
| 4 | the late-event filter ignores the incarnation | killed | H14: late stdout from the old engine after a replacement is |
|
||||
| 5 | the text policy checks only the first character | killed | S1: `/goal x`, with leading spaces or a tab, refuses text-po |
|
||||
| 6 | an acknowledged SIGTERM promotes a stop to stopped | killed | K10: controller killed between the TERM and kill phases: res; K11: controller killed after the confirmation is recorded, b; K12: a member forking in a loop: the freeze stops it, enumer … |
|
||||
| 7 | the process-group fallback can reach stopped | killed | H21: a retry of the exact request with the old token after a; K2: K1 on the process-group fallback ends uncertain, never s |
|
||||
| 8 | a confirmation is not consumed on use | killed | K6: recover without proof, without confirmation, or with cha |
|
||||
| 9 | disconnect releases control | killed | H11: the controller disconnects mid-turn: work continues, th |
|
||||
| 10 | the composer isn't cleared on transfer | killed | S4: a `/` left in the composer is cleared when control trans |
|
||||
| 11 | engine output is read with readline | killed | E2: U+2028, U+2029 inside JSON strings and CRLF line ends ea; S7: ESC, bracketed-paste markers and U+2028/U+2029 travel as |
|
||||
| 12 | a revision is published by exclusive create in place, so a partial file is visible | killed | W1: a revision name appears only after its bytes are synced;; W1: two writers publish the same revision at once: one wins, |
|
||||
| 13 | a second controller reclassifies a claim whose owner is alive | killed (timeout after failures) | H16: a second controller for the same session refuses alread; packages/conversation/tests/claim.test.mjs; packages/conversation/tests/races.test.mjs … |
|
||||
| 14 | the live-session guard skips the real-path check | killed (timeout after failures) | G2: a symlink inside the fixture root to a live session file; G3: a fixture path swapped for a live path after constructio; packages/conversation/tests/claim.test.mjs |
|
||||
| 15 | the pending slot is released at agent_start | killed | E1: send, ack, user, toolCall, toolResult, final answer: sho; E2: U+2028, U+2029 inside JSON strings and CRLF line ends ea; E3: a multipart final, two blocks, null request correlation … |
|
||||
| 16 | a pipe is written again after an unknown write outcome | killed | H19: the link itself never writes again after an unknown out |
|
||||
| 17 | an ack followed by no run marks the item finished | killed | N15: the fence lands in preflight, then an input handler tak; N5: an input handler takes the prompt: ack, no run, delivery |
|
||||
| 18 | admission reopens without the post-settle empty clear | killed | N25: ordinary Interrupt reconciles; a non-empty queue_update; N4: the ack arrives after the first abort and a run starts: |
|
||||
| 19 | abort is sent after a clear_queue timeout | killed | N7: clear_queue answers an error: no abort, nativeQueue unkn |
|
||||
| 20 | the incarnation token check is skipped | killed | H21: a retry of the exact request with the old token after a; H22: after H21 and a valid recovery, a new request with the |
|
||||
| 21 | a missing cgroup path counts as empty | killed | K15: the shim gone, engine/cgroup.events unreadable, or the |
|
||||
| 22 | a unit with a different invocation ID is signalled | killed | K14: a unit with the recorded name but another invocation ID |
|
||||
| 23 | a restart during force stop records the kill phase as done | killed | K10: controller killed between the TERM and kill phases: res |
|
||||
| 24 | an eligibility record can be used twice | killed | K17: two launcher calls with one eligibility record: one lau |
|
||||
| 26 | a non-empty clear_queue reaches reconciled | killed | N1: an extension's follow-up queued after the fence is clear |
|
||||
| 27 | the pending slot's in-flight preflight is abandoned at the fence | killed | N15: the fence lands in preflight, then an input handler tak; N3: the fence lands in preflight, preflight errors, no run: ; N4: the ack arrives after the first abort and a run starts: |
|
||||
| 28 | the fake engine queues a Mosaic prompt while streaming instead of throwing | killed | N10: fake conformance |
|
||||
| 29 | a no-unit observation frees a pair that has a spawn marker | killed | W20: crash after the spawn marker, scope collected; uncertai |
|
||||
| 30 | a settled run with empty clears is taken as interruption evidence without aborted | killed | N14: the run completes while clear_queue is in flight: finis; N17: the run fails on its own during the exchange: failed, F; N18: no final assistant message_end, or a lost line: working |
|
||||
| 31 | a receipt whose run ended stop is relabelled failed interrupted during a stop | survived the pass; killed by the new N14 test (full package, 151/152) | N14: the run completes after the abort is written, before Pi |
|
||||
| 32 | turnState input-reconciled is used to reconcile an Interrupt | killed | N4: the ack arrives after the first abort and a run starts: ; N9: a run that started before the fence and ends aborted: fa |
|
||||
| 33 | Interrupt with no slot and no run creates a stop | killed | H10: a no-turn Interrupt lifts only its own fence; admission; H9: Interrupt racing a prompt's dispatch: before the write, ; N16: Interrupt with no slot and no run refuses no-turn: no s |
|
||||
| 34 | a run is attributed to the slot with the overlap checks skipped | killed | N18: no final assistant message_end, or a lost line: working; N8: an extension prompt starts a run during Mosaic preflight |
|
||||
| 34b | an overlap signal leaves the binding active | killed | N12: input that starts a run after the final empty clear is ; N13: agent_start with no slot held is O1; a later prompt ref; N19: a losing extension prompt settles inside the Mosaic run … |
|
||||
| 35 | a settle that doesn't close the slot's own run is the slot's settle (open agent_start) | killed | N19: a losing extension prompt settles inside the Mosaic run; N8: an extension prompt starts a run during Mosaic preflight |
|
||||
| 35b | a settle with no agent_start since the ack is the slot's settle | killed | N19: a losing extension prompt settles inside the Mosaic run |
|
||||
| 36 | agent_start or agent_settled with no slot held is ignored | killed | N12: input that starts a run after the final empty clear is ; N13: agent_start with no slot held is O1; a later prompt ref; N21: a losing settle after the receipt settled finished is O |
|
||||
| 37 | the launch seal check accepts --extension and missing --no-* flags | killed | N24: the seal is an allow-list: --extension, a missing --no- |
|
||||
| 37b | the binding launches without the --no-* flags | killed | E1: send, ack, user, toolCall, toolResult, final answer: sho; E2: U+2028, U+2029 inside JSON strings and CRLF line ends ea; E3: a multipart final, two blocks, null request correlation … |
|
||||
| 38 | an ack-without-start run settles failed | killed | N11: the run fails before any user message_start: delivery-u |
|
||||
| 39 | an empty clear is recorded as proof no external input was removed | killed | N22: an agent-level custom message is dropped by the clear w |
|
||||
| r2-B1 | the seal ignores everything after the prefix (extraArgs unchecked) | killed | N24: the seal is an allow-list: --extension, a missing --no- |
|
||||
| r2-B1b | the seal skips the prefix order check | killed | N24: the seal is an allow-list: --extension, a missing --no- |
|
||||
| r2-B1c | the seal accepts a relative --session value | killed | N24: the seal is an allow-list: --extension, a missing --no- |
|
||||
| r2-B1d | the seal accepts a repeated option | killed | N24: the seal is an allow-list: --extension, a missing --no- |
|
||||
| r2-B1e | the seal accepts an option value that is a flag or @file | killed | N24: the seal is an allow-list: --extension, a missing --no- |
|
||||
| r2-B2 | the session key is the conversation ID again | killed | W4: a copy of one session under another seat is the same ses; W4: a hard link of one session under another seat is the sam |
|
||||
| r2-B2b | a changed header ID after construction is not refused | killed (timeout after failures) | packages/conversation/tests/claim.test.mjs; W4: a session header ID that changes after construction refu |
|
||||
| r2-D34 | an aborted with no stop in progress raises no overlap (lead decision 34) | killed | N9: an aborted that lands after the fence but before any abo; N9: decision 34: a run that ends aborted with no stop in pro |
|
||||
| r2-n1 | an aborted links to any stop in progress, abort written or not | killed | N9: an aborted that lands after the fence but before any abo |
|
||||
| r2-n2 | no overlap recheck after the pause before the abort | killed | H10: an overlap during the pause before the abort: no abort, |
|
||||
| r2-n3 | an uncertain stop keeps nativeQueue pending | killed | N1: an extension's follow-up queued after the fence is clear |
|
||||
| r2-B3 | a second force stop runs while the first escalation runs | killed | H10: a second force stop while the first escalation runs ref; H17: a confirmation reused, answered from another connection |
|
||||
| r2-TB1 | Enter takes the composer when the send runs, not at the key | killed | terminal: text after Enter in the same input chunk starts th |
|
||||
| r2-TB2 | a lone trailing ESC is not carried to the next chunk | killed | terminal: a paste-start marker split right after its ESC sti |
|
||||
| r2-n13 | zero-width, BOM and tag characters pass visible() | killed | terminal: invisible and bidi characters are made visible; he |
|
||||
| r2-n13b | one-line output keeps LF | killed | terminal: invisible and bidi characters are made visible; he |
|
||||
| r2-B5 | the shim skips the freeze write and answers frozen 1 | killed | K12: a member forking in a loop: the freeze stops it, enumer |
|
||||
| r2-B5b | the shim writes the freeze but doesn't wait for frozen 1 | **SURVIVED** | none (see above) |
|
||||
| r2-B6 | a missing engine/cgroup.events reads as populated 0 | killed | K15: the shim gone, engine/cgroup.events unreadable, or the |
|
||||
| r2-B6b | a missing engine cgroup reads as empty: populated 0 and no members | killed | K15: the shim gone, engine/cgroup.events unreadable, or the |
|
||||
|
||||
## Findings against pinned Pi 0.85.1 (corrected)
|
||||
|
||||
- **Startup append** (replaces the round-1 text, Darkwing n1). Pi's rule is
|
||||
`hasExistingSession = messages.length > 0` (sdk.js 82–83). A session with
|
||||
messages but no `thinking_level_change` entry gains one at every start. A
|
||||
session with no messages takes the new-session branch and gains a
|
||||
`thinking_level_change` at every start, plus `model_change` when a model is
|
||||
set. That includes a Pi-created session that was opened and never
|
||||
prompted. In both cases the leaf moves after launch, so K8 fails closed:
|
||||
the binding goes `uncertain` and prompts refuse `preflight`. A pre-spawn
|
||||
check covering both cases is a later increment's decision.
|
||||
- The inline `/llama` command finding is unchanged.
|
||||
|
||||
## Limits (added)
|
||||
|
||||
- A member can ask the user systemd manager for a new unit outside the scope
|
||||
(`systemd-run --user` lands in `app.slice`). That is a same-uid escape
|
||||
(Filbert n9; brief limits 3 and 4).
|
||||
- The shim channel is not authenticated. A same-uid engine that kills the
|
||||
shim and binds its socket could answer as it (Filbert n10; limit 1).
|
||||
Recording the shim's pid and start time in the claim would narrow it. That
|
||||
is a follow-up.
|
||||
- The guard's home roots follow `$HOME` (Darkwing n2).
|
||||
- A cgroup throttled by `cpu.max` can hold off both the freeze and
|
||||
`cgroup.kill` for seconds. Seen in the r2-B5b fixture attempt, with the
|
||||
harness writing `engine/cpu.max`. The stop then ends `uncertain` at the
|
||||
freeze, which fails closed. Whether a member can throttle itself from
|
||||
inside its cgroup namespace is untested.
|
||||
@@ -0,0 +1,215 @@
|
||||
# CHAT-03 I1 build, review request round 1 (#1507, row 5)
|
||||
|
||||
From Dewey, 2026-10-04. Sage assigned the build under lead decision 36.
|
||||
Reviewers are Filbert and Darkwing (rev 40). Darkwing takes the controller
|
||||
binding and the extension-load refusal, which Rocko had before decision 42.
|
||||
There are two rounds at most (item 27). Row 5's gate is Jason's (Gate E), so
|
||||
after the reviewers approve, the row goes to waiting-on-jason, not done.
|
||||
|
||||
## Candidate
|
||||
|
||||
- Manifest: `agents/dewey/work/chat-03/I1-manifest.sha256`, sha256
|
||||
`1404341eaeaf7d1e684c9f27e76718061ed08c25a52da5f190425feb1274ba69`,
|
||||
27 files.
|
||||
- Base `1c724958`. Nothing is staged or committed; the files are untracked
|
||||
or modified in the canonical checkout. They won't change during the
|
||||
round. A change means a new manifest and a new round.
|
||||
- Spec: the pinned brief `agents/dewey/work/chat-03/BRIEF.md`, sha256
|
||||
`1ef15ac0…`, plus lead decisions 30–34 and 36. Nothing outside the brief
|
||||
is built.
|
||||
- Scope: increment I1 only. H5–H8 and the Claude adapter are I4. I3
|
||||
(recorded runs against a real model) waits on Jason's go.
|
||||
|
||||
What the manifest lists:
|
||||
|
||||
- `packages/conversation/src/`: the CHAT-03 modules (`claim`, `client`,
|
||||
`cohort`, `controller`, `engine`, `events`, `framing`, `guard`, `pi-pin`,
|
||||
`records`, `shim`, `terminal`, `text-policy`, `transcript`, `turns`) and
|
||||
`safe-fs.mjs`, which gains `ControlRefusal`;
|
||||
- `packages/conversation/tests/`: `claim`, `cohort`, `flows`, `races`,
|
||||
`smoke` and `turns` test files, plus `fake-pi.mjs`, `harness.mjs` and
|
||||
`ctrl-child.mjs`;
|
||||
- `packages/conversation/README.md`: the "Mediated control (CHAT-03)" part;
|
||||
- `packages/conversation/package.json`: the description and four new
|
||||
`exports` entries (`controller`, `client`, `transcript`, `terminal`), no
|
||||
new dependency.
|
||||
|
||||
Where to start: the README's "Choices" section lists each reading of the
|
||||
brief that a reviewer could disagree with. "Refusals" lists every code,
|
||||
including the three names this build adds (`malformed`, `eligibility`,
|
||||
`preflight`). "Findings against pinned Pi" has the two findings below.
|
||||
|
||||
## For Darkwing: binding and extension-load refusal
|
||||
|
||||
- The seal: `src/pi-pin.mjs` (`buildPiArgs`, `checkSeal`, `SEAL_FLAGS`)
|
||||
and `src/controller.mjs` (`SEAL_BASIS`, and the K8 load check in
|
||||
`#launchInto`). Any `-e`/`--extension` argument or a missing `--no-*` flag
|
||||
refuses `unsealed-engine` before spawn. Bound seats get no explicit
|
||||
extensions (lead decision 31).
|
||||
- The pin: `src/pi-pin.mjs` requires `package-lock.json` and
|
||||
`node_modules/.package-lock.json` to name Pi 0.85.1 with the pinned
|
||||
integrity (n1 bundle pin, decision 32). A mismatch refuses
|
||||
`engine-pin-mismatch` at start and at recovery (K6).
|
||||
- Tests: N24 (seal), K6 and K8 (pin and load check), mutants 37 and 37b.
|
||||
`smoke.test.mjs` starts the pinned binary sealed with no credentials and
|
||||
no prompt.
|
||||
- The controller binding: claim (`src/claim.mjs`), the live-session guard
|
||||
(`src/guard.mjs`), and the takeover and generation path in
|
||||
`src/controller.mjs` (`#evaluateOp`, `#recheck`, `#transfer`). Tests:
|
||||
W1–W20, G1–G3, H1–H4.
|
||||
|
||||
## Suites
|
||||
|
||||
Run on the candidate bytes in the canonical checkout, logs kept under
|
||||
`/tmp/dewey-suites/` for this session:
|
||||
|
||||
| Suite | Result |
|
||||
|---|---|
|
||||
| `node --test packages/conversation/tests/` | 141 pass, 0 fail |
|
||||
| `packages/control-board` tests | 124 pass, 0 fail |
|
||||
| `packages/webui` tests | 14 pass, 0 fail |
|
||||
| `packages/seat` tests | 19 pass, 0 fail |
|
||||
| `docs/plans/chat-00/check.mjs` | 48 checks passed |
|
||||
| `docs/plans/chat-01/check.mjs` | R3 PASS |
|
||||
| `docs/plans/chat-01c/check.mjs` | PASS |
|
||||
| `scripts/test-auth.sh` | 15 passed, 0 failed |
|
||||
| `scripts/test-conductor.sh` | 17 passed, 0 failed |
|
||||
| `scripts/test-config.sh` | 24 passed, 0 failed |
|
||||
| `scripts/test-discord.sh` | 64 passed, 0 failed |
|
||||
| `scripts/test-extension-package.sh` | 18 passed, 0 failed |
|
||||
| `scripts/test-foundation.sh` | 44 passed, 0 failed |
|
||||
| `scripts/test-queue.sh` | 29 passed, 0 failed (node 148/148) |
|
||||
| `scripts/test-release.sh` | 14 passed, 0 failed |
|
||||
| `scripts/test-task.sh` | 90 passed, 0 failed |
|
||||
|
||||
The nine `scripts/test-*.sh` suites are the brief's "every suite at the
|
||||
base". One run each, no reruns.
|
||||
|
||||
## Contracts
|
||||
|
||||
The twelve contract files in the brief's table (`docs/plans/chat-00`,
|
||||
`chat-01`, `chat-01c`) hash to the brief's values (`sha256sum -c` on the
|
||||
table, 12 OK). `git diff 1c724958`
|
||||
shows no change under `docs/plans/chat-0*`, `contracts/` or `roles/`.
|
||||
|
||||
## Mutation pass
|
||||
|
||||
Run in scratch copies under `/tmp`, never the served tree: every mutant
|
||||
applied alone, then the whole conversation package run. The first pass ran
|
||||
on the source before the new tests. Seven mutants survived it, and each now
|
||||
has a test that kills it:
|
||||
|
||||
| # | Why it survived | Kill test |
|
||||
|---|---|---|
|
||||
| 1 | H3's refused branch accepted `controller` or `generation`, and the controller check alone caught it | H3, third block: control goes A, B, A while the prompt waits, so only the generation check refuses |
|
||||
| 3 | No test raced two writers on one revision | W1: two stores publish revision 1 at once; one wins, the other gets null, the winner's file is intact |
|
||||
| 8 | H17's reuse case changed the stop, so the stop check refused it first | K6: a confirmed `recover` used twice; the second refuses `confirmation` |
|
||||
| 12 | Nothing looked for the revision name before the bytes were synced | W1: held at `temp-written`, no revision file is visible, only the temp file |
|
||||
| 14 | The mutated line was unreachable: the loop above already tested `realPath(p)`, so the first mutant was equivalent | `guard.check` now tests the real path in one place; mutant 14 removes that check and the real-path overlap. G2 and G3 kill it |
|
||||
| 16 | Every controller path checks `poisoned` before writing, which masked the link's own guard | H19: an `EngineLink` whose first write fails EPIPE refuses the next write and sends nothing |
|
||||
| 19 | A `clear_queue` timeout poisons the link, so the mutant's abort never reached the pipe | N7: `clear_queue` answers `success: false`, the one failure that leaves the link unpoisoned; no abort is sent |
|
||||
|
||||
Every mutant fails at least one test now. Table:
|
||||
|
||||
| # | Mutant | First pass | Final | Failing tests (first 60 chars each) |
|
||||
|---|---|---|---|---|
|
||||
| 1 | dispatch skips the generation recheck | SURVIVED | killed | H3: a takeover while a prompt holds the dispatch lock: writt |
|
||||
| 2 | abort is sent before clear_queue | killed | killed | H10: Interrupt and force stop together: one stop chain, forc; N14: the run completes while clear_queue is in flight: finis; N15: the fence lands in preflight, then an input handler tak … |
|
||||
| 3 | a revision is published by rename, so an existing revision can be replaced | SURVIVED | killed | W1: two processes acquire the same pair at once; exactly one; W1: two writers publish the same revision at once: one wins, |
|
||||
| 4 | the late-event filter ignores the incarnation | killed | killed | H14: late stdout from the old engine after a replacement is |
|
||||
| 5 | the text policy checks only the first character | killed | killed | S1: '/goal x', with leading spaces or a tab, refuses text-po |
|
||||
| 6 | an acknowledged SIGTERM promotes a stop to stopped | killed | killed | K10: controller killed between the TERM and kill phases: res; K11: controller killed after the confirmation is recorded, b; K12: a member forking in a loop: the freeze stops it, enumer … |
|
||||
| 7 | the process-group fallback can reach stopped | killed | killed | H21: a retry of the exact request with the old token after a; K2: K1 on the process-group fallback ends uncertain, never s |
|
||||
| 8 | a confirmation is not consumed on use | SURVIVED | killed | K6: recover without proof, without confirmation, or with cha |
|
||||
| 9 | disconnect releases control | killed | killed | H11: the controller disconnects mid-turn: work continues, th |
|
||||
| 10 | the composer isn't cleared on transfer | killed | killed | S4: a '/' left in the composer is cleared when control trans |
|
||||
| 11 | engine output is read with readline | killed | killed | E2: U+2028, U+2029 inside JSON strings and CRLF line ends ea; S7: ESC, bracketed-paste markers and U+2028/U+2029 travel as |
|
||||
| 12 | a revision is published by exclusive create in place, so a partial file is visible | SURVIVED | killed | W1: a revision name appears only after its bytes are synced;; W1: two writers publish the same revision at once: one wins, |
|
||||
| 13 | a second controller reclassifies a claim whose owner is alive | killed | killed (timeout after failures) | H16: a second controller for the same session refuses alread; W12: a live owner paused with SIGSTOP; a second controller r; W2: acquire while a claim is reserved or active refuses alre |
|
||||
| 14 | the live-session guard skips the real-path check | SURVIVED | killed (timeout after failures) | G2: a symlink inside the fixture root to a live session file; G3: a fixture path swapped for a live path after constructio |
|
||||
| 15 | the pending slot is released at agent_start | killed | killed | E1: send, ack, user, toolCall, toolResult, final answer: sho; E2: U+2028, U+2029 inside JSON strings and CRLF line ends ea; E3: a multipart final, two blocks, null request correlation … |
|
||||
| 16 | a pipe is written again after an unknown write outcome | SURVIVED | killed | H19: the link itself never writes again after an unknown out |
|
||||
| 17 | an ack followed by no run marks the item finished | killed | killed | N15: the fence lands in preflight, then an input handler tak; N5: an input handler takes the prompt: ack, no run, delivery |
|
||||
| 18 | admission reopens without the post-settle empty clear | killed | killed | N25: ordinary Interrupt reconciles; a non-empty queue_update; N4: the ack arrives after the first abort and a run starts: |
|
||||
| 19 | abort is sent after a clear_queue timeout | SURVIVED | killed | N7: clear_queue answers an error: no abort, nativeQueue unkn |
|
||||
| 20 | the incarnation token check is skipped | killed | killed | H21: a retry of the exact request with the old token after a; H22: after H21 and a valid recovery, a new request with the |
|
||||
| 21 | a missing cgroup path counts as empty | killed | killed | K15: the shim gone or engine/cgroup.events unreadable: evide |
|
||||
| 22 | a unit with a different invocation ID is signalled | killed | killed | K14: a unit with the recorded name but another invocation ID |
|
||||
| 23 | a restart during force stop records the kill phase as done | killed | killed | K10: controller killed between the TERM and kill phases: res |
|
||||
| 24 | an eligibility record can be used twice | killed | killed | K17: two launcher calls with one eligibility record: one lau |
|
||||
| 26 | a non-empty clear_queue reaches reconciled | killed | killed | N1: an extension's follow-up queued after the fence is clear |
|
||||
| 27 | the pending slot's in-flight preflight is abandoned at the fence | killed | killed | N15: the fence lands in preflight, then an input handler tak; N3: the fence lands in preflight, preflight errors, no run: ; N4: the ack arrives after the first abort and a run starts: |
|
||||
| 28 | the fake engine queues a Mosaic prompt while streaming instead of throwing | killed | killed | N10: fake conformance |
|
||||
| 29 | a no-unit observation frees a pair that has a spawn marker | killed | killed | W20: crash after the spawn marker, scope collected; uncertai |
|
||||
| 30 | a settled run with empty clears is taken as interruption evidence without aborted | killed | killed | N14: the run completes while clear_queue is in flight: finis; N17: the run fails on its own during the exchange: failed, F; N18: no final assistant message_end, or a lost line: working |
|
||||
| 31 | a receipt whose run ended stop is relabelled failed interrupted during a stop | killed | killed | N14: the run completes while clear_queue is in flight: finis |
|
||||
| 32 | turnState input-reconciled is used to reconcile an Interrupt | killed | killed | N4: the ack arrives after the first abort and a run starts: ; N9: a run that started before the fence and ends aborted: fa |
|
||||
| 33 | Interrupt with no slot and no run creates a stop | killed | killed | H10: a no-turn Interrupt lifts only its own fence; admission; H9: Interrupt racing a prompt's dispatch: before the write, ; N16: Interrupt with no slot and no run refuses no-turn: no s |
|
||||
| 34 | a run is attributed to the slot with the overlap checks skipped | killed | killed | N18: no final assistant message_end, or a lost line: working; N8: an extension prompt starts a run during Mosaic preflight |
|
||||
| 34b | an overlap signal leaves the binding active | killed | killed | N12: input that starts a run after the final empty clear is ; N13: agent_start with no slot held is O1; a later prompt ref; N19: a losing extension prompt settles inside the Mosaic run … |
|
||||
| 35 | a settle that doesn't close the slot's own run is the slot's settle (open agent_start) | killed | killed | N19: a losing extension prompt settles inside the Mosaic run; N8: an extension prompt starts a run during Mosaic preflight |
|
||||
| 35b | a settle with no agent_start since the ack is the slot's settle | killed | killed | N19: a losing extension prompt settles inside the Mosaic run |
|
||||
| 36 | agent_start or agent_settled with no slot held is ignored | killed | killed | N12: input that starts a run after the final empty clear is ; N13: agent_start with no slot held is O1; a later prompt ref; N21: a losing settle after the receipt settled finished is O |
|
||||
| 37 | the launch seal check accepts --extension and missing --no-* flags | killed | killed | N24: any --extension argument, or a missing --no-* flag, ref |
|
||||
| 37b | the binding launches without the --no-* flags | killed | killed | E1: send, ack, user, toolCall, toolResult, final answer: sho; E2: U+2028, U+2029 inside JSON strings and CRLF line ends ea; E3: a multipart final, two blocks, null request correlation … |
|
||||
| 38 | an ack-without-start run settles failed | killed | killed | N11: the run fails before any user message_start: delivery-u |
|
||||
| 39 | an empty clear is recorded as proof no external input was removed | killed | killed | N22: an agent-level custom message is dropped by the clear w |
|
||||
|
||||
For mutant 14, "First pass" is the equivalent variant described above.
|
||||
|
||||
Mutants 13 and 14 also hit the 900-second run timeout. Each fails its
|
||||
tests first. With two live controllers (13) or a guard that admits the
|
||||
symlinked path (14), later fixtures in the same files hang.
|
||||
|
||||
`tests/fake-pi.mjs` gained `failResponse(type)` for the N7 case. It
|
||||
answers `success: false`, as Pi's RPC loop does when a handler throws.
|
||||
|
||||
## Defects found and fixed during the build
|
||||
|
||||
All are in my files. They were found by tests while I wrote them, and none
|
||||
reached a commit.
|
||||
|
||||
1. A refused contender's controller unlinked the live controller's socket.
|
||||
The socket directory is now prepared only after the claim classifies.
|
||||
2. `#evaluate` read `res.outcome` from `recover`'s Promise, so `recover`
|
||||
over the wire threw `internal`. W8 missed it because it resumes through
|
||||
a restart.
|
||||
3. `#admission` and `#uncertain` skipped the binding push when only the
|
||||
state changed, so clients kept showing `stopping` after a failed force
|
||||
stop.
|
||||
4. The client kept non-final receipts live across a new incarnation. It
|
||||
now marks them outcome unknown.
|
||||
5. The `unknown` push's `type` field overwrote the envelope's
|
||||
`type: "push"`, so clients dropped it. The field is now `nativeType`.
|
||||
6. A `Transcript` attached to a connected client didn't read the page until
|
||||
the next welcome. It reads at once.
|
||||
7. The controller's new refusal codes broke control-board's
|
||||
`REFUSAL_STATUS` completeness test, which scans every `new Refusal("…")`
|
||||
in `conversation/src`. Control-layer refusals now throw `ControlRefusal`,
|
||||
a subclass in `safe-fs.mjs`. The README says why. `packages/control-board`
|
||||
is untouched and green.
|
||||
|
||||
## Findings against pinned Pi 0.85.1
|
||||
|
||||
Reported, not built around:
|
||||
|
||||
- **Startup append.** A session whose branch has no
|
||||
`thinking_level_change` entry gains one at every start (sdk.js
|
||||
240–252). The leaf moves after launch, so the K8 load check fails closed:
|
||||
the binding goes `uncertain` and prompts refuse `preflight`. Sessions Pi
|
||||
created carry the entry, so this affects only sessions written by
|
||||
something else. A pre-spawn check would refuse them before launch. That
|
||||
is a later increment's decision.
|
||||
- **One inline extension command under the seal.** `get_commands` still
|
||||
lists `/llama` (Pi's bundled llama.cpp router). Slash text is refused at
|
||||
admission, so it can't be invoked. The smoke test pins the command list
|
||||
so a change shows.
|
||||
|
||||
## Limits
|
||||
|
||||
- The mutation pass proves each listed mutant fails a test. It doesn't
|
||||
prove the tests catch every other fault.
|
||||
- K13 is refused on this host by cgroup namespace delegation; the test
|
||||
records the host behaviour, not a controller check.
|
||||
- `cohort.test.mjs` needs a systemd user manager; without one the scope
|
||||
tests skip and say so.
|
||||
@@ -0,0 +1,27 @@
|
||||
2ed3790eb877414b8e985ca16031d8963adfb5c9555d415efb76288b76a5e1a6 packages/conversation/package.json
|
||||
974f75fc5e7770d0be84448e2d7ff34acdd3855397bf6b5dd59e9072b22dedcb packages/conversation/README.md
|
||||
6c3e112e13e69f5a9cfd6b217d3f9ba439000c8f353e556207b2e5063bcdee91 packages/conversation/src/claim.mjs
|
||||
a45beb82438bb42d97d1e322010e175e785767664e548b90c4125e98b7660003 packages/conversation/src/client.mjs
|
||||
6a5ef01d36111af8f5d872e30e0dd03b079f4650dd2b977c0ade5a1cf22ba3fe packages/conversation/src/cohort.mjs
|
||||
e2140d9cec046c58b09a91b40a83cb7c93b640840933ef0e68889a9c8178de5d packages/conversation/src/controller.mjs
|
||||
a833f130f0086e0942de2f46d3d4acfeebf898659f0d7e5d6f375a2780ac5e70 packages/conversation/src/engine.mjs
|
||||
2fe414b4d2382e0ebfaa63e63dbd17ff4bb6ffc6bd7c5d0bbcc806797d6b7129 packages/conversation/src/events.mjs
|
||||
eaf0772b60bb17fbf9548267c9cfeb0104f92f5cbafdfcb60c1633883eec6432 packages/conversation/src/framing.mjs
|
||||
a96fd4304cfb7a72d36ebf7f9c1fad81881df4621ba17719558428be60559a8a packages/conversation/src/guard.mjs
|
||||
6496b1aa72c130f44999a7389708535321e10f5c0ae16cb9f7bb2f6dd80f4703 packages/conversation/src/pi-pin.mjs
|
||||
96f7b64c2e2c3493e270224542818fd14f042e3cf0ac14788b3f881010bc25b1 packages/conversation/src/records.mjs
|
||||
62b0f7e9f798f584fcc0e4ead0522e44fd113bd0803f2bad92ffa480cadb7527 packages/conversation/src/safe-fs.mjs
|
||||
83dd41c5a6b40a2f5e4b65a0e75c9cf9517b6e89fdfe8361368e993583a97a1d packages/conversation/src/shim.mjs
|
||||
94f2b114c3ad2b7f217de8fe0ee0babf145ef7441534c24e9fab0528676ed475 packages/conversation/src/terminal.mjs
|
||||
071e14db0bc6933a438d0fbf5a2f0f1a9daefbef913908fc1cd5e1171cc9177a packages/conversation/src/text-policy.mjs
|
||||
f834ee740e7149e7d2ed976112d0719ce0660270e3b61cf93e1774d112c992a2 packages/conversation/src/transcript.mjs
|
||||
e778f2836f7062a414d7542954b54b83e1d39459529d6b03f5306eb4091793f0 packages/conversation/src/turns.mjs
|
||||
4fddb664719d25d8c0d4871a621be68190392aacb2e3212f431660d3511d63e7 packages/conversation/tests/claim.test.mjs
|
||||
10a24b347b07426b2b7f65ce6284c9fc38d6ff3046b91d55ea5baba54d8d9774 packages/conversation/tests/cohort.test.mjs
|
||||
492ed353b2b755cc5fbe0fb4e645fab60bea3ccd68fbbe48c18bfef3a51cd5c2 packages/conversation/tests/ctrl-child.mjs
|
||||
855879fd9a4830cd23453814c2c36418e006bde8457e46173af8968cea5a45cb packages/conversation/tests/fake-pi.mjs
|
||||
a867751872d1a47383a21b71772b26b0a8c8d48253d03ca6003a2496af413836 packages/conversation/tests/flows.test.mjs
|
||||
c59af12539f272c5f8ce6ca7ea026cc230c54d76cde35d5cf85962a596d681d4 packages/conversation/tests/harness.mjs
|
||||
269ab45c7bd50c31068caa1d2b34329da928317bd5871e290fe57fbacf8a3028 packages/conversation/tests/races.test.mjs
|
||||
8535ad8eee7384b29557dfa6cf8517d22fbc5ff9693f513ffc5e35ff627d71a8 packages/conversation/tests/smoke.test.mjs
|
||||
18d5b4b0eecf4ca37d04b46b9103e704a133eee4973b6e51719c93ac9a1e1255 packages/conversation/tests/turns.test.mjs
|
||||
@@ -0,0 +1,27 @@
|
||||
2ed3790eb877414b8e985ca16031d8963adfb5c9555d415efb76288b76a5e1a6 packages/conversation/package.json
|
||||
3d32a202743e99696bf3ab58d03eab86a34f59006978549f846eb3c52a793eef packages/conversation/README.md
|
||||
6c3e112e13e69f5a9cfd6b217d3f9ba439000c8f353e556207b2e5063bcdee91 packages/conversation/src/claim.mjs
|
||||
a45beb82438bb42d97d1e322010e175e785767664e548b90c4125e98b7660003 packages/conversation/src/client.mjs
|
||||
6a5ef01d36111af8f5d872e30e0dd03b079f4650dd2b977c0ade5a1cf22ba3fe packages/conversation/src/cohort.mjs
|
||||
06afe06161775b03d2f7def129bcaae04167c7484e3e9fa112652f5e4a8e6c06 packages/conversation/src/controller.mjs
|
||||
a833f130f0086e0942de2f46d3d4acfeebf898659f0d7e5d6f375a2780ac5e70 packages/conversation/src/engine.mjs
|
||||
2fe414b4d2382e0ebfaa63e63dbd17ff4bb6ffc6bd7c5d0bbcc806797d6b7129 packages/conversation/src/events.mjs
|
||||
eaf0772b60bb17fbf9548267c9cfeb0104f92f5cbafdfcb60c1633883eec6432 packages/conversation/src/framing.mjs
|
||||
a96fd4304cfb7a72d36ebf7f9c1fad81881df4621ba17719558428be60559a8a packages/conversation/src/guard.mjs
|
||||
f813184cbef5151449b2e6e7b3f1b375fb20f26600d2b8101aa72f13181b60fe packages/conversation/src/pi-pin.mjs
|
||||
96f7b64c2e2c3493e270224542818fd14f042e3cf0ac14788b3f881010bc25b1 packages/conversation/src/records.mjs
|
||||
62b0f7e9f798f584fcc0e4ead0522e44fd113bd0803f2bad92ffa480cadb7527 packages/conversation/src/safe-fs.mjs
|
||||
83dd41c5a6b40a2f5e4b65a0e75c9cf9517b6e89fdfe8361368e993583a97a1d packages/conversation/src/shim.mjs
|
||||
5dc53b8617db15df6e4103fb40407013f0e4a96da1e9b1cb722aef0de1d24f37 packages/conversation/src/terminal.mjs
|
||||
071e14db0bc6933a438d0fbf5a2f0f1a9daefbef913908fc1cd5e1171cc9177a packages/conversation/src/text-policy.mjs
|
||||
f834ee740e7149e7d2ed976112d0719ce0660270e3b61cf93e1774d112c992a2 packages/conversation/src/transcript.mjs
|
||||
e778f2836f7062a414d7542954b54b83e1d39459529d6b03f5306eb4091793f0 packages/conversation/src/turns.mjs
|
||||
64ca6e7e2a65ddcaf0b5dfd54abe2af904e991d3d9e7ff8ffef3bb7e82e5a2eb packages/conversation/tests/claim.test.mjs
|
||||
41d4a2f3a0a7f3ba1b1c03059c055b15866ff571d106ecd4948d857c9f8da259 packages/conversation/tests/cohort.test.mjs
|
||||
492ed353b2b755cc5fbe0fb4e645fab60bea3ccd68fbbe48c18bfef3a51cd5c2 packages/conversation/tests/ctrl-child.mjs
|
||||
d0476d48bd0d4015c0f17c8facf52a5c409dbdef211d5c43340ac68e56b91cdc packages/conversation/tests/fake-pi.mjs
|
||||
abdafbe8f8e8fdde559b99ad3b81f00d463abbc8663d202be0b14f1b9f5ad890 packages/conversation/tests/flows.test.mjs
|
||||
ac664988c350f4437137fc50e2ab90122ab2ec7368caf790ab94e195ed225257 packages/conversation/tests/harness.mjs
|
||||
3dfb2e6afbe41b53de2b4d4a52caa7a3a08663d5b0f3494cf28aa9d311e3bf6c packages/conversation/tests/races.test.mjs
|
||||
8535ad8eee7384b29557dfa6cf8517d22fbc5ff9693f513ffc5e35ff627d71a8 packages/conversation/tests/smoke.test.mjs
|
||||
52425185e4393f019a06879db02eac9cde2b1f90e60c29a40cd361e70fb8c0cb packages/conversation/tests/turns.test.mjs
|
||||
@@ -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/`.
|
||||
Reference in New Issue
Block a user