docs(chat-03): brief pinned at 1ef15ac0 after r3 and scope check, lead decision 33 (#1507)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-09-26 19:45:04 -05:00
co-authored by Claude Opus 5.5
parent 93ee5054f4
commit 8a465e891a
18 changed files with 6822 additions and 1 deletions
@@ -0,0 +1,700 @@
# CHAT-03 brief: live adapters and mediated terminal (#1507, row 5)
Author: Dewey, 2026-09-26. R1, for review. This is the brief only. It
includes no source, no contract edits and no seat changes.
Jason chartered CHAT-03 on 2026-09-26 (`docs/plans/2026-09-26_lead-decisions.md`
item 22). Plan row: `docs/plans/2026-09-13_webui-session-chat.md` line 215.
It depends on CHAT-01 (`28d4e98a`) and implements part of the CHAT-01C
companion (`b023841c`). The layout follows `docs/plans/BRIEF-TEMPLATE.md`,
and the depth follows the CHAT-02 brief (`636b0fac`). The six carry-forwards
Sage set are mapped to their evidence under "Carry-forwards".
Line numbers and hashes below are at `40a02d2b`.
## CHAT-03: live adapters and mediated terminal
### Problem
CHAT-02 made every repository Pi conversation readable. Nothing can drive a
conversation yet except the old paths: the Pi TUI in a tmux pane, and board
Reply or agent-send, which paste into that pane. Those paths have no
controller, no generation fence and no stop proof. One has already misfired.
At 2026-09-26T20:10:57Z a leftover `/` in Pi's composer landed in front of a
board reply, because `tools/tmux/send-message.sh` (lines 45–54) pastes onto
whatever the composer holds and then presses Enter (`docs/plans/DEFERRED.md`,
"Board send can turn into a Pi slash command").
CHAT-01 defines the records and commands for one controller per
conversation. It defers the writer-claim record (`chat-01/README.md:342`),
and CHAT-01C defers R3-1 (`chat-01c/README.md:217`). Lead decisions item 8
moves both here. The Claude catalogue still refuses `unsupported-harness`
(`packages/conversation/src/reader.mjs:24`). Jason ruled that CHAT-03 owns it
after B1. Claude on this host reports 2.1.283, but the plan's evidence named
2.1.269 (plan line 159). The binary changes under any adapter that does not
pin it.
### Owner and reviewer
- **Source author: `<slot>`.** The plan has Darkwing name the backend author
(line 215; lead decisions item 22). Darkwing is on queue A1/A2, so the
author is named after this brief is approved. The brief does not pick one.
- Brief author: Dewey.
- Filbert reviews this brief, then the exact candidate of each increment.
- Rocko does an adversarial pass on §5 (control races) and §6 (stop and
recovery), in this brief and later in the code of each increment. Rocko
also reviews the B1 packet (§8), because it is evidence that a false pass
would turn into a wrong adapter.
- No one reviews their own work. If Darkwing names Filbert or Rocko as
author, Sage names a replacement for that review.
- Tracking: #1507.
### Files owned
The source author may create or change only these paths.
| Path | Contents |
|---|---|
| `packages/conversation/src/**` | New modules for the controller, claim, supervisor, Pi adapter, events, transport and mediated terminal. The Claude parser and adapter come in 3D. `reader.mjs` changes only to lift the Claude refusal for the pinned version. |
| `packages/conversation/tests/**` | Tests, fake engines, fixture extensions and redacted recordings |
| `packages/conversation/README.md`, `packages/conversation/package.json` | Documentation and the new refusal names. No new dependencies. |
| `agents/<author>/work/chat-03/**` | Review packets, evidence and the B1 packet |
Module names inside `src/` are the author's call (plan lines 150–151). Any
path outside this table needs a brief amendment.
Excluded, and why:
- **`tools/tmux/**`.** Plan line 244 excludes it, and §4 shows the mediated
path needs no change there.
- **`packages/control-board/**`.** No board edit is needed (§4, S6). Tests
import `replyToRow` read-only by relative path, the same way the
queue-as-data plan imports `scan.mjs`.
- **`packages/seat/**`, `scripts/agent-host-dev.sh`, `agents/*/launch.sh`.**
No seat migrates, so no seat or launcher changes. The launcher handoff
belongs to CHAT-07.
- **`packages/webui/**`.** The chat UI is CHAT-05.
- **`docs/plans/chat-0*/**`.** Contracts change only through the C items
below.
- **`roles/**`, `contracts/**`, root files, auth and `~/.mosaic`.**
No overlap with queue-as-data or the ledger:
| Owner | Paths | Overlap |
|---|---|---|
| A1, Darkwing (`agents/darkwing/work/queue-a1/build-manifest.sha256`) | `packages/queue/**`, `scripts/queue-commit.sh`, `scripts/test-queue.sh`, `scripts/git-hooks/pre-commit`, `docs/plans/BRIEF-TEMPLATE.md` | none |
| A2, Darkwing (Filbert's plan `282fabbb…` §8.1; lead decisions item 20) | `scripts/mosaic` dispatch, `docs/plans/queue.json`, `docs/plans/QUEUE.md`, `scripts/test-{darkwing,rocko}-launch.mjs` | none. The mediated terminal runs as `node packages/conversation/src/<cli>.mjs`. It is not a `scripts/mosaic` verb. |
| Piece B, after A | `AGENTS.md`, `agents/*/CONTEXT.md` | none |
| Ledger | `packages/ledger/**` | none |
The author may import the process-identity helpers in
`packages/discord/src/journal.mjs` read-only, as A1 does. That changes
neither package.
### Contracts implemented
These are unchanged since the commits named. The sha256 values are at
`40a02d2b`, and `git status` shows no local edits.
| Path | sha256 | Commit |
|---|---|---|
| `docs/plans/chat-00/README.md` | `991663e607404c2f022716bd45b40d5b3092d53c3f9f61bbafd455c0659b832f` | `370823b3` |
| `docs/plans/chat-00/sources.json` | `1a07ae88de45fd3219eca10acae13637ca75416cb1c598aa9080825bd7598af9` | `370823b3` |
| `docs/plans/chat-00/fixtures.json` | `ad9d4fe94131b2c4bb30691ad846698c5b0818f0935d20a38de73fa3f267bd80` | `370823b3` |
| `docs/plans/chat-00/check.mjs` | `568f004f5a0cfce64d65ff202c420ab491e716919a93989377a7bb9e7b8be622` | `370823b3` |
| `docs/plans/chat-01/README.md` | `61aba7d60f380ff8a135a04a2e851f11c250795ead11cca2e594566849483163` | `28d4e98a` |
| `docs/plans/chat-01/contracts.schema.json` | `38382e08c97635f8864e6953b6cf44ec324040397a1aa8fc3f86e279abe36db1` | `28d4e98a` |
| `docs/plans/chat-01/fixtures.json` | `403c8ae91963a379de17805380bc1425bc4e96c1ef34094385b703d17cf5ce7d` | `28d4e98a` |
| `docs/plans/chat-01/check.mjs` | `2e164e4bfa61963bb5dd8639e26e67407e64d19278cc56ed8a4f77e430f1dee5` | `28d4e98a` |
| `docs/plans/chat-01c/README.md` | `63d9f9edffc2aa1aa3bc99364b864e8c6f234eedc8ad2f9da231531970d54250` | `b023841c` |
| `docs/plans/chat-01c/contracts.schema.json` | `da132f0a02f29281344cc5350396af53c893d7895308bc430f9bf3ca3b3785b9` | `b023841c` |
| `docs/plans/chat-01c/fixtures.json` | `00639a219b00b67c5e0ab904163a6d945481f6b0df45a058b32e00f1f1326611` | `b023841c` |
| `docs/plans/chat-01c/check.mjs` | `5971ed5f11a32f0dff5720db03d8d4bc4c007a5d2654ba50726bb35ba753eba6` | `b023841c` |
Design inputs, which CHAT-03 does not implement in full:
`docs/plans/foundation-v1-candidate/RUNTIME.md` §§3–5 (`b1a2b4d0…`) and the
plan itself (`48142829…`).
What CHAT-03 takes from each contract:
- **CHAT-01.** The records `binding`, `connection`, `clientRequest`,
`request`, `receipt`, `event`, `stop`, `cohortProof`, `effectReport`,
`turnProof`, `nativeDecision`, `approval` and `confirmation`. The commands
`observe`, `prompt`, `takeover`, `acquire-recovery-control`, `approval`,
`interrupt`, `force-stop`, `recover`, `issue-confirmation` and
`answer-confirmation`. The draft, upload and queue-edit commands belong to
CHAT-04.
- **CHAT-01C.** Only the confirmation restoration rules (lines 92–104). The
private-state pages, upload ranges and `recover-refused-draft` need
CHAT-04's durable store.
The CHAT-01 text at line 342 stays as published. Lead decisions item 8
records the move.
**Contract changes, each a separate reviewed item.** Each one lands and is
approved before the code that needs it. Sage assigns the author; Filbert and
Dewey review, as they did for CHAT-01C.
- **C-1: a state for dispatched input that native evidence proves
unconsumed (R3-1).** It also decides whether that input may be recovered
into a draft. Q20 says already-dispatched work keeps its actual or
uncertain status, so Sage decides whether C-1 goes to Jason. §3 gives the
interim rule until C-1 lands.
- **C-2: the CHAT-03D native dialog companion.** It covers Pi `confirm` and
`select` with exact labels; `input` and `editor` stay unsupported. CHAT-01
lines 239–242 and 292 block non-permission forms until it exists.
Increment 3B waits for it.
- **C-3: Claude record fields.** Needed only if B1 shows that CHAT-01 lacks
a field, for example the capability-negotiation result on `binding`.
Otherwise C-3 is void.
Not contract changes:
- The writer-claim storage record (§2) is internal and never crosses the
wire. It stores the CHAT-01 `binding` fields it needs. If one is missing,
that becomes a C item, not an edit.
- New refusal names (`busy`, `receipt-unknown`, `engine-pin-mismatch`,
`foreign-writer`, `not-controller`, `stale-generation`) are bounded draft
IDs, which CHAT-01 line 363 allows without a registry. The package README
lists them.
### What ships
Four increments, in order. Each is its own candidate and review round, as
with the queue-as-data A1/A2 split. No increment starts before the one above
it is approved.
| Increment | Content | Needs first |
|---|---|---|
| 3A | Controller, transport, writer claim, Pi adapter on a fake Pi engine, incremental events, mediated terminal, interrupt, force stop and recovery. Fixtures in §2–§6, §9 and §10. | Brief approved; author named |
| 3B | Pi native dialogs (§7) | 3A approved; C-2 approved |
| 3C | The B1 evidence packet for Claude (§8) | 3A approved. Jason's go for any run that calls a model or reads Claude auth. |
| 3D | Claude adapter, catalogue and history (§7, §8) | 3C approved, so B1 passed; C-3 if it exists |
If B1 does not pass, CHAT-03 closes after 3B. Claude keeps refusing
`unsupported-harness`, and CHAT-06's both-harness gate stays blocked with
CHAT-03 as its owner. That outcome is recorded, not a silent narrowing.
#### 1. Controller and transport
- One controller process per execution owns the engine's stdin. Pi runs in
`--mode rpc` with no TUI, so a mediated engine has no native composer or
terminal left to fence. That is how CHAT-03 meets plan lines 133–134: a
losing native TUI can't stay writable because none exists.
- Engine output is split on LF only (`rpc.md` lines 30–38). Node `readline`
is not used, because it also splits on U+2028 and U+2029. The ledger
refused a live line for that class of bug (lead decisions item 18).
Fixture E2 covers it.
- Clients connect over a Unix socket in a 0700 directory. Every connection
starts as an observer. The browser does not reach the socket in CHAT-03;
the WebUI side is CHAT-05.
- **Actor.** There is one actor, `local-operator`, as in CHAT-02. The socket
directory's mode is the only boundary, and every process with the same uid
passes it. That includes agent seats, which run as the same user. So
CHAT-03 cannot stop a same-uid agent from taking control of another
conversation. See Limit 1. This blocks any live use until a local
owner-channel design passes review (plan lines 181–185; B4).
- **No broker queue.** CHAT-04 owns durable queues (plan line 216). In
CHAT-03, a prompt sent while the engine is busy is refused with `busy`,
and the text stays in the client. The controller never sends
`streamingBehavior`, `steer` or `follow_up`, so mediated input never
enters Pi's native queues (`rpc.md` lines 56–65). Tradeoff: there are no
queued follow-ups (Q6) until CHAT-04, but 3A has no native-queue ambiguity
of its own making.
- Admission and dispatch both recheck, under one dispatch lock: binding
state, open admission, the controlling connection and its generation, the
execution incarnation and the text policy (CHAT-01 lines 153–160).
- **Dedup key.** Actor, conversation and client request ID (CHAT-01 line
144). The index lives as long as one controller incarnation. A retry that
arrives after a controller restart is refused with `receipt-unknown`; it
is never redispatched. Durable receipts are CHAT-04.
- **Text only.** Images and files are CHAT-04.
#### 2. Writer-claim record (D1)
CHAT-01 lines 336–343 allow at most one non-stopped binding per conversation
or native session identity, and they defer the record. RUNTIME.md §3 item 3
adds one claim per (agent, project, workspace). The record:
- **Keys.** Each claim has two keys, and both must be held: the seat tuple
(seat, project, workspace) and the native session identity (the Pi header
ID or the Claude session UUID). They are always taken in that order, so
two controllers can't deadlock.
- **Revisions.** Each key is a directory under a claim root. Each revision
is a write-once file created exclusively and fsynced along with its
directory. The current state is the highest revision. No file is ever
rewritten, the same rule as run records.
- **Fields.** A revision stores:
- the key and the binding ID;
- the harness, conversation, and branch and leaf at launch;
- the config pins: engine version, binary sha256 and launch argv digest;
- the execution incarnation: boot ID, pid, process start time and cgroup;
- the controller generation;
- the state: `reserved`, `active`, `stopping`, `uncertain` or `stopped`;
- the proof reference that allowed `stopped`.
- **Location.** The claim root is a constructor argument. CHAT-03 sets no
production default and writes nothing under the data root or `.pi/state`.
Fixtures use temporary roots. The live location is chosen at cutover
(CHAT-07) and reviewed as a data-map change then. Tradeoff: nothing live
can use 3A until that decision, which is intended.
- **What never releases a claim.** Disconnect never changes a claim.
`agent_settled`, EOF, SIGTERM, idle and an abort acknowledgement never
release one (CHAT-01 line 325; CHAT-00 line 65).
- **Restart.** A controller that starts and finds an `active` claim for its
key does not launch. It classifies the recorded incarnation:
- **Different boot ID.** Every process of that boot is gone. That is
proof for the whole cohort, so the claim moves to `stopped` with a boot
proof. Tool calls with no recorded end become `uncertain` effects.
- **Same boot, engine alive.** This is an orphan the controller cannot
attach to, because the stdin pipe is gone. The state becomes
`uncertain`. Only a confirmed force stop of that cohort (§6) moves it
on.
- **Same boot, engine gone.** The state is `uncertain` until the cohort
observation in §6 shows no member left.
- **Foreign writer.** The controller watches the bound session file. An
appended entry whose ID never appeared on the controller's own stream
means another writer, for example someone running `pi --session` on the
same file. Admission closes, the binding becomes `uncertain`, and views
get a reconcile marker (plan lines 179–180).
- **No writes to sessions.** The controller never writes a session file and
never uses `SessionManager.open` (CHAT-00 line 67).
| # | Fixture | Expected |
|---|---|---|
| W1 | Two processes acquire the same key at once | Exactly one claim. The other refuses `already-active`. |
| W2 | Acquire while a claim is `active` or `reserved` | `already-active` |
| W3 | Acquire while `stopping`, `uncertain`, or `stopped` without proof | `unsafe-replacement` |
| W4 | Same session with a different seat tuple, and the reverse | Both refuse. No partial claim is left. |
| W5 | Controller killed with SIGKILL at each step of acquire, transition and release | After restart: never two non-stopped claims and never a lost claim. The recorded state is the old one or the new one. |
| W6 | Controller killed mid-turn and restarted while the engine is alive | `uncertain`. No launch, and prompts refuse. |
| W7 | Recorded boot ID differs from the current one | `stopped` with a boot proof. Open tool calls become `uncertain`. |
| W8 | Resume after a proven stop with the same pins | New revision, generation +1, same conversation, branch and leaf |
| W9 | Resume with a changed binary, argv digest, branch or leaf | Refused. The claim is unchanged. |
| W10 | Foreign append to the bound session file | Admission closed, `foreign-writer`, `uncertain`, reconcile marker. No engine write. |
| W11 | Controller writes to session files | None. The CHAT-02 F17 check runs over the fixture session directory. The fake engine's own appends are recorded separately and excluded. |
#### 3. Pi adapter, incremental events and R3-1
Pinned inputs for Pi 0.85.1, under
`node_modules/@earendil-works/pi-coding-agent/`:
- `docs/rpc.md` (`15fcd26bee72777b373fd5f2edd77091a01cadd4de95e48b08422ced0552a28d`)
- `dist/modes/rpc/rpc-mode.js` (`e7e4724aa55c5aac73cf36793653b26736200e5c59d58373990fc31028f86477`)
- `dist/modes/rpc/rpc-types.d.ts` (`e968e5be01dc7ad9615f938ae867ef136fa495f13dcf169942e9f781a299d9eb`)
The plan requires reading the Pi docs completely before Pi implementation
(lines 163–164). The author does that before any 3A code.
- **The fake engine.** A fixture process that speaks the pinned protocol:
- prompt acceptance and refusal;
- message and tool events;
- `agent_end` arriving before `agent_settled`, and retries;
- `clear_queue` and `abort`;
- dialogs;
- scripted pause points, so a test can land a race at an exact step.
Its behavior is checked against the pinned files, not guessed.
- **Real-binary smoke.** The pinned `pi` starts in `--mode rpc` in a scratch
directory, with no auth file and no model call. It answers `get_state`,
`get_commands`, and `clear_queue` and `abort` while idle. The recorded
exchange shows that the fake's framing matches the binary. If the binary
won't start without auth, the author records that, and the packet says
plainly that 3A rests on the fake alone.
- **Events.** Native events map onto CHAT-01 `event` records: message-start,
text and thinking deltas, tool-start, tool-update and tool-end,
message-end with `updateMode: replace`, and run-settled.
- Every event carries the execution incarnation and a sequence number
for that execution.
- An unknown native event becomes an `unavailable` event. It is never
dropped silently and never passed through raw.
- `agent_settled` maps to run-settled, never to cohort termination.
- **Joining history and the stream.** CHAT-01 lines 105–112 require an
atomic cut between a history page and the stream; otherwise streaming is
advertised as unavailable. The author shows the cut from the pinned
source: when a message_end's entry is on disk relative to the event. If
that can't be shown, 3A advertises replay as unavailable. A client gets
the page, then live events from the moment it subscribes, with a
reconcile marker at the seam. Fixture E4 covers both cases.
**R3-1: input dispatched but not consumed when a fence lands.** Because the
controller never sends `streamingBehavior`, Pi queues mediated input in one
case only. Pi accepts a prompt at preflight, and queued prompts count as
success (`rpc-mode.js` lines 298–318), at a moment the controller didn't
see as busy, such as between `agent_end` and `agent_settled` during a
retry. Anything else in the native queues came from outside Mosaic, for
example an extension's follow-up message. The rule:
1. Interrupt and force stop close admission first, then send `clear_queue`,
then `abort`, in that order. `abort` runs any queued messages still in
the session (`rpc.md` line 158).
2. At most one dispatched item can lack a started user message. If
`clear_queue` returns exactly that item's text, exactly once, the item is
proven unconsumed.
3. Until C-1 lands, a proven-unconsumed item keeps its actual receipt state
(`acknowledged`). The turnProof records that `clear_queue` returned it.
The item is never relabelled unsent and never resent. The client shows
the text so the actor can send it again as a new request.
4. Returned text that matches no Mosaic item is external input. It goes into
the turnProof and is shown read-only as held by the engine, not sent by
Mosaic, and discarded. It is never resent.
5. If `clear_queue` fails or times out, the turnProof records
`nativeQueue: unknown`, the stop stays `uncertain`, and admission stays
closed.
| # | Fixture | Expected |
|---|---|---|
| N1 | Prompt accepted during the retry window, then Interrupt | `clear_queue` goes before `abort`. The returned text matches the one item. The receipt stays `acknowledged`, the turnProof lists the item, and nothing is resent. |
| N2 | N1 with `abort` sent first (mutant) | The fake runs the queued item, so the test fails. This is the ordering guard. |
| N3 | `clear_queue` returns an extension's follow-up | Recorded as external and shown read-only. Attributed to no request. |
| N4 | `clear_queue` returns the item's text twice, or a near match | No proof. The receipt becomes `delivery-unknown`, and the stop stays `uncertain`. |
| N5 | `clear_queue` times out | `nativeQueue: unknown`, stop `uncertain`, admission closed |
| N6 | Pi refuses a prompt because it is streaming (no `streamingBehavior`) | Receipt `failed` with native evidence. The frozen text is kept for an explicit resend. |
#### 4. The slash path (carry-forward 3)
There are two separate hazards: text left in a composer gets concatenated
with a new message, and Pi interprets certain prefixes.
- **Concatenation.** `send-message.sh` pastes onto whatever the composer
holds. The mediated path has no engine-side composer. Each prompt is one
JSONL record, and its `message` field holds the whole text. Nothing left
over can prefix it. The mediated terminal's own composer is a local
buffer. It is empty at start, cleared after each submit and on control
transfer, and it submits only while its connection is the controller.
- **Interpretation.** RPC still acts on a leading `/`. Extension commands run
immediately, even while streaming, and skill and template commands
expand (`rpc.md` lines 67–69).
- Repository seats load one extension command, `/goal`
(`.pi/extensions/goal/index.ts:230`), and disable skills and templates
(`scripts/agent-host-dev.sh:135–136`).
- Admission refuses `text-policy` when the first non-whitespace character
is `/` (CHAT-01 lines 181–183). Dispatch checks again.
- CHAT-01 lines 368–370 leave `!`, `@` and slashes on later lines open
under B3. 3A settles them from the pinned source. The author lists every
prefix that Pi 0.85.1 RPC `prompt` interprets, and the list becomes a
fixture. Admission refuses each listed prefix, and any prefix the author
can't classify.
- **Board and agent-send for a mediated seat.** A mediated engine has no
tmux pane. The board already refuses a reply to a registration without a
tmux session before the tool runs (`packages/control-board/src/serve.mjs:84`;
test `serve.test.mjs:810`). CHAT-03 relies on that and changes neither the
board nor `tools/tmux`. No real seat registers as mediated in CHAT-03, so
the refusal first applies to a real seat at cutover.
- **Seats still on tmux keep the hazard.** The DEFERRED entry stays open. It
closes when a seat moves to the mediated path (CHAT-07), or when a CHAT-03I
charter covers `tools/tmux` (CHAT-01 lines 232–238). Neither happens in
CHAT-03.
| # | Fixture | Expected |
|---|---|---|
| S1 | `/goal x`, and the same with leading spaces or a tab | `text-policy` at admission. Zero bytes reach the fake engine. |
| S2 | Each prefix on the author's list | Refused. The test reads the same list the code uses. |
| S3 | `/goal` on the second line | The fixture pins the result from the pinned source: admitted if Pi doesn't interpret later lines, refused otherwise |
| S4 | The terminal composer holds a `/` from an abandoned edit, then control transfers and returns | Composer cleared. The next submit sends only the new text, and the fake engine records the exact bytes. |
| S5 | An observer terminal gets a paste and then Enter, as `send-message.sh` does | Not admitted: `not-controller`. Zero engine bytes. |
| S6 | A mediated-shaped registration (no tmux) passed to `replyToRow` with a recording `exec` | 409, "no tmux session". `exec` is never called. |
| S7 | A prompt containing ESC, bracketed-paste markers or U+2028 | Sent as one JSON string. The fake engine receives the exact text, and no record splits. |
#### 5. Control races (Rocko)
Each race uses the fake engine's pause points to land at an exact step.
Every race asserts the engine bytes, the receipts and the events.
| # | Race | Expected |
|---|---|---|
| H1 | Two takeovers with the same expected generation | One wins, and the generation goes up by 1. The other refuses `stale-generation`. |
| H2 | The old controller's prompt arrives after a takeover commits | Refused. Zero engine bytes. |
| H3 | A takeover commits while a prompt holds the dispatch lock | Either the write finished before the commit, and the receipt keeps the old actor, or the prompt is refused. Never a partial write, never both. |
| H4 | Self-takeover | Refused (CHAT-01 line 269) |
| H5 | The old controller answers an approval after a takeover | Refused. The decision is answered once, through the new projection. |
| H6 | Duplicate approval answers with the same content | One native response |
| H7 | Conflicting approval answers with the same request ID | The second is refused |
| H8 | An approval answer after a native timeout or cancel | Refused. The state comes from native evidence. |
| H9 | Interrupt racing a prompt's dispatch | Before the write: `dispatch-refused`. After it: the R3-1 rules in §3. |
| H10 | Interrupt and force stop at the same time | One stop chain. Force stop supersedes (CHAT-01 lines 319–322). |
| H11 | The controller disconnects mid-turn | Work continues and the claim is unchanged. Control stays with the disconnected connection until an observer takes over explicitly. Nothing happens automatically. |
| H12 | Exact retry of a prompt after reconnecting | The same receipt. No second dispatch. |
| H13 | Retry with the same request ID and different text | Refused |
| H14 | Late stdout from the old engine after a replacement | Dropped by execution incarnation and counted in the evidence. Never rendered. |
| H15 | A revoked connection sends a command | Refused, with the revocation fence of CHAT-01 lines 271–280 |
| H16 | A second controller process for the same session | Refused as in W1 and W2. The first controller is untouched. |
| H17 | A confirmation reused, answered from another connection, or answered after the stop context changed | Refused. Confirmations are single-use (CHAT-01 lines 264–268; CHAT-01C lines 92–100). |
#### 6. Stop, cohort proof and recovery (Rocko)
- **Cohort.** The controller launches each engine in its own transient
systemd user scope (this host runs systemd 261), so the cohort is that
cgroup. A child that calls `setsid` stays in it. If a scope can't be
created, the cohort falls back to the process group. A stop there can
reach `uncertain` but never `stopped`, because a `setsid` child can
escape. Tradeoff: this needs `systemd-run --user`, and the fallback is
honest rather than equivalent.
- **Proof.** `stopped` needs a cohortProof (CHAT-01 lines 327–331):
- complete membership;
- boot ID, and pid and start time for each member;
- a death time for each member;
- the observation time;
- an empty cgroup.
SIGTERM, EOF, an abort acknowledgement, `agent_settled` and idle never
promote a stop to `stopped` (line 325).
- **Effects.** A tool-start with no tool-end at stop time is an `uncertain`
effect. Killing never counts as rollback (line 334).
- **Interrupt (Q20).** Close admission, `clear_queue`, `abort`, wait for
settle, build the turnProof, record `reconciled`, and reopen admission
under current control (lines 307–313). If this hangs, force stop stays
available.
- **Force stop (Q8).** Needs an answered confirmation. It closes admission,
sends SIGTERM to the cgroup, then SIGKILL after a bounded grace period,
then builds the proof. It acts only on the selected execution's cgroup,
and other executions survive.
- **Recover (Q15).** Recover reports eligibility only. It needs:
- a stopped proof;
- effects reconciled or explicitly uncertain;
- an exact confirmation;
- the same pins (lines 336–343).
Launching is a separate library call made by a launcher, and it needs
that eligibility record. No client command launches. A browser or
controller crash can't start an engine.
- **Pi resume.** Resume reuses the same session file and leaf. The author
confirms from the pinned docs how Pi selects the leaf. Before admission
opens, the adapter checks `get_state` and `get_tree` and refuses if the
engine loaded any other leaf (RUNTIME.md §3 item 4).
| # | Fixture | Expected |
|---|---|---|
| K1 | Force stop an engine whose tool child calls `setsid`, with a scope | The child is killed. `stopped`, with proof. |
| K2 | K1 on the process-group fallback | `uncertain`, never `stopped` |
| K3 | SIGTERM acknowledged while a member is still alive | Stays `stopping` or `uncertain` |
| K4 | Two fake engines; force stop one | The other keeps running and still answers |
| K5 | Stop during a tool call | The effect is `uncertain` and shown |
| K6 | Recover without proof, without confirmation, or with changed pins | Refused |
| K7 | Recover after proof, then launch | New incarnation, generation +1, same leaf. The cancelled prompt is not replayed. |
| K8 | The engine loads a different leaf on resume | Refused before admission opens |
| K9 | An interrupt that never settles | Stays `uncertain`. Force stop stays available, and ordinary takeover is refused while fenced. |
| K10 | Controller crash during a force stop | The restart finds `stopping` and resumes observing. No relaunch. |
#### 7. Native approval dialogs (3B after C-2; Claude in 3D)
- Pi has no built-in permission system. Approvals come from extensions
through `extension_ui_request` (`rpc.md` lines 1186–1210). Repository seats
load no extension that raises dialogs today, so 3B tests with a fixture
extension.
- CHAT-01 lines 239–242 and 292 block non-permission forms until CHAT-03D
exists, and C-2 is that companion. After C-2:
- `confirm` and `select` render the exact native labels and answer with
the exact request ID;
- `input` and `editor` stay unsupported and show as disabled, with a
reason;
- nothing is answered on the user's behalf, and a generic "yes" is not
accepted.
- Cancel is enabled only where C-2 proves it can't mean allow. A Pi select
cancel does not inherit confirm's meaning (lines 293–295).
- Pending dialogs don't survive a Pi restart (CHAT-00 line 69). After a
takeover, the controller reprojects each dialog: a new projection ID for
the same decision (lines 282–287).
- **Claude, in 3D after B1.** `can_use_tool` maps to allow-once and deny.
Choices that widen permissions are disabled, and there is no invented
cancel (lines 294–295). Pending permission requests are re-read from
initialize on reconnect, but only if B1 shows that the pinned version
supports it.
| # | Fixture | Expected |
|---|---|---|
| P1 | `confirm` dialog answered by the controller | One `extension_ui_response` with the exact ID and value |
| P2 | `select` with labels containing markup and bidi controls | Exact labels, rendered inert |
| P3 | `input` or `editor` dialog | Disabled, with a reason. No response is sent. |
| P4 | A native timeout before the answer | `uncertain`, then resolved from native evidence. A late answer is refused (H8). |
| P5 | Takeover while a dialog is pending | New projection ID and the old one superseded. One native answer. |
| P6 | Interrupt while a dialog is pending | The approval becomes `uncertain`, not denied (lines 297–299) |
| P7 | A Claude permission request (3D, recorded fixture) | Allow-once and deny only. Widening choices disabled. |
#### 8. Claude: B1 and the catalogue (carry-forward 2)
**What B1 is.** `docs/plans/chat-00/README.md` lines 193–195: prove the
exact Claude CLI and protocol combination, capability negotiation,
transcript branch selection and the image and file input shapes. SDK and
source examples narrow the uncertainty, but they don't certify the
installed binary. CHAT-00 line 66 names the gap for history: Claude's
persisted branch format and leaf selection.
**What proves it (3C).** A B1 packet under `agents/<author>/work/chat-03/b1/`,
approved by Filbert and Rocko, with five parts:
1. **Pin.** The exact version string and the sha256 of the binary the
adapter will run. Today `claude --version` reports 2.1.283; the plan saw
2.1.269. The adapter checks both at launch and otherwise refuses with
`engine-pin-mismatch`. It runs the engine with auto-update off, and the
author cites the setting from the pinned docs.
2. **Sources.** The official protocol sources for that exact version, by
hash. These are the C-* entries in `chat-00/sources.json`, re-pinned to
that version.
3. **Recordings.** Made from that binary, in a scratch config directory with
no repository credentials:
- initialize and its capability answer, including whether
`interrupt_cancel_queued_v1` and `pending_permission_requests` exist;
- a text prompt and an image prompt;
- a `can_use_tool` allow and a deny;
- an interrupt.
They are redacted, hash-pinned and used as the 3D fixtures. The plan
rules out model calls for discovery (line 166). Any recording that calls
a model or reads Claude auth therefore needs Jason's go first, because it
spends money and touches credentials. Without that go, 3C stops after
parts 1 and 2, and B1 stays open.
4. **Branch selection.** Two persisted transcripts from that binary, one of
them with a fork (an edit or a rewind). The packet states the leaf rule
and shows that `--resume` continues the leaf the parser picks.
5. **Negative.** The parser refuses a transcript from another version with
`unsupported-harness`.
**The catalogue after B1 (3D).**
- Rocko's launcher runs Claude with the default config directory
(`agents/rocko/launch.sh:86`, no `CLAUDE_CONFIG_DIR`). So its transcripts
sit in the same `~/.claude/projects/<cwd slug>/` directory as every other
Claude session started in this checkout, including T3 threads and Jason's
own sessions.
- The catalogue therefore never lists that directory. It opens only session
IDs the repository launcher recorded for that seat
(`.pi/state/rocko/session-id` and `launches/*/session-id`). Each ID must
resolve to exactly one file under the approved root, opened with the
CHAT-02 safe-open rules.
- A launcher receipt is a hint, like a registration (CHAT-01 lines 62–64).
The access mapping stays under B4 review.
- The CHAT-02 guarantees still apply: read-only, no writes (F17), cursors,
byte caps, continuation parts and inert rendering.
| # | Fixture | Expected |
|---|---|---|
| L1 | A Claude transcript at the pinned version | Default leaf shown and other branches readable, as in F12 |
| L2 | A transcript from another version | `unsupported-harness` |
| L3 | A decoy session file in the same directory, named in no receipt | Never listed, never opened |
| L4 | A receipt naming a file outside the root, a symlink or another project | Refused, as in F7–F9 |
| L5 | Sidechain or subagent entries | Kept separate, not merged into the main branch |
| L6 | No-write check | F17 over the Claude directory |
| L7 | The binary's sha256 or version differs at launch | `engine-pin-mismatch`. No launch. |
The 3D adapter maps Claude stream-json onto CHAT-01 events. The §5 and §9
fixtures then run again against the recordings.
#### 9. Return flow and events
| # | Fixture | Expected |
|---|---|---|
| E1 | The plan §6 return flow on the fake engine: send, acknowledge, user, toolCall, toolResult, new final answer | Shown once in the same conversation, with no refresh, no duplication, and the draft and reading position kept |
| E2 | U+2028 and U+2029 inside JSON strings, and CRLF | Each parsed as one record |
| E3 | Multipart final, two blocks, null request correlation, duplicate delivery | The CHAT-01 cases at lines 110–112 |
| E4 | A page, then a subscription with overlap, and again with a gap or a new epoch | Overlap deduplicated. A gap reconciles. |
| E5 | An unknown native event | An `unavailable` event. Nothing is dropped. |
| E6 | A tool result delayed across a pause and a reconnect | Reconciled without a manual refresh |
| E7 | The mediated terminal as observer, then as controller | Renders the same stream as the library client, and submits only as controller |
The browser leg of the return flow is CHAT-05. 3A proves it at the library
and the terminal.
#### 10. Checks and mutation pass
- **Suites.** These must pass:
- `node --test packages/conversation/tests/`;
- the control-board, webui and seat package tests;
- `node docs/plans/chat-00/check.mjs`, `chat-01/check.mjs` and
`chat-01c/check.mjs`;
- the eight `scripts/test-*.sh` suites.
- **Nested runners.** A test that starts `node --test` itself clears
`NODE_TEST_CONTEXT` for that child, as in the DEFERRED entry on nested
runners (N13), or it can hide failures.
- **Isolation.** Every fixture runs in temporary directories. No fixture
touches live registrations, `.pi/state`, `~/.claude` or a live seat (plan
line 272).
- **Mutation pass.** Run in a scratch copy, as for the CHAT-02 Console. Each
of these mutants must fail at least one test:
1. dispatch skips the generation recheck;
2. `abort` is sent before `clear_queue`;
3. a claim file is created without exclusive create;
4. the late-event filter ignores the incarnation;
5. the text policy checks only the first character, missing leading
whitespace;
6. an acknowledged SIGTERM promotes a stop to `stopped`;
7. the process-group fallback can reach `stopped`;
8. a confirmation is not consumed on use;
9. disconnect releases control;
10. the composer isn't cleared on transfer;
11. engine output is read with `readline`.
3D adds two more: the pin check is skipped, and the decoy file is listed.
#### 11. Choices in this brief, for review
| Choice | Tradeoff |
|---|---|
| No broker queue; a busy engine refuses `busy` | No queued follow-ups until CHAT-04, and no native queueing of mediated input |
| No `streamingBehavior`, `steer` or `follow_up` | Same as above; this is what keeps R3-1 down to one case |
| No default claim root | Nothing live can use 3A before the CHAT-07 data-map decision |
| Cohort is a systemd user scope; the process-group fallback can't reach `stopped` | Depends on `systemd-run --user`, and the fallback leaves stops uncertain |
| One actor, `local-operator`, guarded only by socket-directory mode | Same-uid agents are not kept out (Limit 1) |
| Dedup lasts one controller incarnation | A retry across a restart refuses instead of returning the receipt |
| Four increments, with 3C/3D gated on B1 | CHAT-03 can close Pi-only, with that outcome recorded |
| Claude catalogue from launcher receipts only | A Claude session started outside the launcher never appears |
### Carry-forwards
| # | Sage's item | Where | Acceptance evidence |
|---|---|---|---|
| 1 | D1 writer-claim record and R3-1 (lead decisions item 8) | §2, §3 | W1–W11 and N1–N6 pass. Mutants 2 and 3 fail tests. C-1 settles the final state. |
| 2 | The Claude catalogue after B1 | §8; increments 3C and 3D | The B1 packet (parts 1–5), approved by Filbert and Rocko. L1–L7 pass. |
| 3 | The board send that can become a Pi slash command | §4 | S1–S7 pass. Mutants 5 and 10 fail tests. The DEFERRED entry stays open for seats still on tmux. |
| 4 | The CHAT-01/01C contracts, by path and hash | "Contracts implemented" | The hash table matches `sha256sum` at approval. C-1 to C-3 are separate reviewed items. |
| 5 | Path authority | "Files owned" | The overlap table. Every changed path in each candidate is inside "Files owned". |
| 6 | The source author | "Owner and reviewer" | `<slot>`, named by Darkwing after this brief is approved |
### Limits
1. **Same-uid control.** Any process running as the same user can connect
to the socket and take control. Agent seats run as that user, so an
agent could take over another conversation. CHAT-03 is fixture-only, so
no live conversation is exposed. Live use waits for a reviewed local
owner-channel design (B4).
2. **Fake engines.** They model the pinned docs. The real-binary smoke
covers framing only. 3A makes no model call, so real-engine behavior is
unproven until CHAT-06.
3. **B2 untouched.** Tool isolation and trust and settings parity (CHAT-00
line 195) are untouched. Any real engine for a real seat waits on B2.
4. **Containment.** The cgroup covers local processes. Network and remote
effects stay `uncertain`.
5. **Takeover has nothing to recover.** With no broker queue, a takeover
has no queued input to turn into drafts. Q13 is CHAT-04's.
### Out of scope
- Durable queues, drafts, uploads, queue edit and cancel, takeover-to-draft
(CHAT-04).
- Remote control (CHAT-04R) and the chat UI (CHAT-05).
- The candidate, cutover and any real seat migration (CHAT-06, CHAT-07).
- Changes to `tools/tmux`, the board, the seat package, launchers, `roles/`,
`contracts/` and the `docs/plans/chat-0*` contracts.
- The CHAT-03I fleet-communications charter (CHAT-01 lines 232–238).
- Authenticated multi-actor control and the local owner-channel design (B4).
- The Claude adapter and history, unless B1 passes.
- Filbert's idsDigest note, which stays in
`agents/dewey/work/chat-02/FOLLOWUPS.md`.
### Gate
- **Brief.**
- Filbert approves the exact `BRIEF.md` hash.
- Rocko's pass on §5 and §6 leaves no blocking finding open.
- Sage commits and pins the brief. Darkwing then names the author.
- **Each increment.**
- Filbert approves the exact candidate hashes. Rocko passes the §5 and §6
code in 3A and the B1 packet in 3C.
- Every §10 suite is green on an index export, and every mutant is
killed.
- Sage commits. A push needs Jason's word.
- **3C.** Jason's go comes before any model call.
- **CHAT-03 done.** 3A and 3B are approved and committed, and either 3D is
approved, or B1 is recorded as not passed, with Claude still refusing and
CHAT-06 blocked.
- **No live check.** No real seat migrates, so there is none. The live proof
belongs to CHAT-07.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,640 @@
--- BRIEF.md@2c5be6b4 (R3)
+++ BRIEF.md (final, lead decision 32)
@@ -1,7 +1,9 @@
# CHAT-03 brief: live adapters and mediated terminal (#1507, row 5)
-Author: Dewey, 2026-09-26. R3, for review. This is the brief only. It
-includes no source, no contract edits and no seat changes. R1
+Author: Dewey, 2026-09-26/27. Final text for pinning: R3 (`2c5be6b4`,
+frozen as `BRIEF-r3-2c5be6b4.md`) with the one edit lead decision 32
+orders. It is not a review round (lead decision 27). This is the brief
+only. It includes no source, no contract edits and no seat changes. R1
(`5dd447f7`) is frozen as `BRIEF-r1-5dd447f7.md`, and R2 (`5c5b45a2`) as
`BRIEF-r2-5c5b45a2.md`. §0 lists what changed.
@@ -19,13 +21,33 @@
lead decisions gained items 26 and 27, and the goals review
(`docs/plans/2026-09-27_goals-review.md`) was added.
-Lead decision 27 makes R3 the last review round. After it, no CHAT-03
-source work starts until Sage rescopes CHAT-03 against Gate E. So R3's
-answer to anything pinned Pi can't prove is to refuse or report unknown,
-never new machinery to prove it. `REVIEW-REQUEST.md` marks each section
-as needed for Gate E or not, as input for the rescope.
+Lead decision 27 made R3 the last review round, and its answer to
+anything pinned Pi can't prove is to refuse or report unknown, never new
+machinery to prove it. Sage rescoped CHAT-03 against Gate E from R3's
+section map (lead decision 30), ruled on Rocko's R3 finding (lead
+decision 31) and ordered this edit (lead decision 32).
+
+### 0. Changes since R3 (lead decision 32)
+
+Filbert approved R3 on the sections Gate E keeps, with no blocking
+finding (`agents/filbert/work/chat-03-brief-review-r3-2026-09-27.md`,
+`48447592`). Rocko closed his R2 finding and raised one blocking finding
+in the seal (`agents/rocko/work/chat-03-r3-adversarial-2026-09-27.md`,
+`19e3fcff`), which lead decision 31 resolves. This edit does the five
+things lead decision 32 lists, and nothing else.
-### 0. Changes since R2
+| Item | What changed |
+|---|---|
+| 1. Lead decision 30 cuts, removed and not reworded | Removed: C-1, C-2 and C-4; increment I2 and §7's Pi dialogs (P1, P2, P4–P6); increment I1b (C-5 moves to CHAT-04, and the interim rule stays); the §2 idle drift check, W10, W18, W19, mutant 25, the drift choice and the `session-drift` name. H5–H8 stay for Claude in I4. References to removed items were adjusted where they stood: What ships, E5, carry-forwards 1 and 4, §11, Limits 5 and 11, Out of scope and Gate. Two sentences were kept by moving them: the claim's reach, into the live-session guard bullet, and P3's disabled Pi dialog, which is now §7's only Pi rule. |
+| 2. Lead decision 31: no explicit extensions | The seal is `--no-extensions`, `--no-prompt-templates`, `--no-themes` and no `--extension`. The registry, the input-silent review and the tree hashes are removed, not replaced with pinning. N5 and N15 are fake-only. N24 and mutant 37 refuse any `--extension`. Limit 11 and §11 say explicit extensions return with goal in CHAT-06. |
+| 3. Filbert n1: the Pi pin | Pi is pinned by the `package-lock.json` integrity of `@earendil-works/pi-coding-agent` 0.85.1, checked against npm's installed record. The listed `dist/core` and `dist/extensions` files are now labelled as citation sources, since `pi` runs `dist/bundle`. The built-in `llama.cpp` is pinned with the package. |
+| 4. Filbert n2: the clear's own `queue_update` | O5 counts the empty `queue_update` Pi's clear emits before its response as the controller's own. N10's fake emits it, and N25 proves that an ordinary Interrupt reconciles. |
+| 5. Rocko's R3 note | A build note under What ships: the `no-turn` cleanup lifts only its own fence. H10 covers the force-stop, overlap and revocation branches. |
+
+Filbert's n3 (an `aborted` with no stop in progress) isn't in lead
+decision 32, so it isn't applied.
+
+### 0a. Changes from R2 to R3 (historical)
R3 answers Rocko's review of R2
(`agents/rocko/work/chat-03-r2-adversarial-2026-09-26.md`, `07b938fb`),
@@ -49,7 +71,7 @@
| Consistency passes | Two passes over the R3 draft found 22 defects of wording and cross-reference. A third pass over the finished draft found 17, including three where a fixture's expected result didn't follow from the rules (N6, N8, N20). All are fixed. |
| Base | Line numbers and hashes move to `f2b9e622`. No contract, source or pinned Pi file changed since `40a02d2b`. Queue A1 is now committed, so the overlap table names its commit. |
-### 0a. Changes from R1 to R2 (historical)
+### 0b. Changes from R1 to R2 (historical)
The rule numbers in this table are R2's. R2's rule 6 is R3's rule 7.
@@ -204,38 +226,9 @@
approved before the code that needs it. Sage assigns the author; Filbert and
Dewey review, as they did for CHAT-01C.
-- **C-1: withdrawn from CHAT-03 and restated for CHAT-06.** R2's C-1
- would have added a `turnProof` list of the external items that
- `clear_queue` removed, so that `reconciled` could follow a non-empty
- clear. Its premise was that the clear accounts for everything an
- Interrupt removes. Filbert's R2 review (F2) showed that it doesn't.
- Agent-level custom messages are dropped without being returned, and
- `nextTurn` messages survive (§3). A list built from the clear's return
- would claim completeness it doesn't have, so C-1 doesn't ship in
- CHAT-03. The check isn't weakened: CHAT-03 runs only sealed engines,
- where any non-empty clear is an overlap signal and leaves the stop
- `uncertain` (§3 rule 3). Restated for CHAT-06, the item that owns
- turn-starting extensions: a `turnProof` record of removed external
- input, adopted only when pinned Pi gives complete evidence of what an
- abort removed (every queue, including agent-level and `nextTurn`
- messages) and ties runs to prompts, for example with run IDs. That needs
- a Pi upgrade or upstream change, which CHAT-06 names and pins.
-- **C-2: the CHAT-03D native dialog companion.** It covers Pi `confirm` and
- `select` with exact labels; `input` and `editor` stay unsupported. CHAT-01
- lines 239–242 and 292 block non-permission forms until it exists.
- Increment I2 waits for it.
- **C-3: Claude record fields.** Needed only if B1 shows that CHAT-01 lacks
a field, for example the capability-negotiation result on `binding`.
Otherwise C-3 is void.
-- **C-4 (optional; Sage decides): an event type for an unrecognized native
- event.** `event.type` is a closed enum, and `unavailable` is only a
- `visibility` value. Until C-4, an unknown native event gets no client
- event. It is recorded in controller evidence (type name and byte count)
- and counted, and the terminal shows the count. So it is not silent, and
- no record gets a type it doesn't have. Mapping it to an existing type
- with `visibility: unavailable` was the other option Filbert offered. It
- isn't taken because each existing type has meaning a client acts on, for
- example tool-start opening an effect.
- **C-5: interrupt outcomes other than an interrupted turn (R3).**
`turnProof.turnState` allows `interrupted`, `unknown` and
@@ -246,8 +239,9 @@
`completed-before-interrupt`, `failed-before-interrupt`,
`no-run-at-interrupt`),
allowed by `reconcile-interrupt` without recording `turn-interrupted`.
- Until C-5 lands, those stops stay `uncertain` (§3 rule 5). Adoption goes
- through I1b.
+ Until C-5 lands, those stops stay `uncertain` (§3 rule 5). C-5 moves to
+ CHAT-04 with its own contract review (lead decision 30), so CHAT-03
+ ships that interim rule only.
**Deviation V-1, accepted with limits (lead decision 25).** CHAT-01 line 145 says an exact
retry after reconnect returns the existing receipt. CHAT-01C line 212 says
@@ -270,7 +264,7 @@
wire. It stores the CHAT-01 `binding` fields it needs. If one is missing,
that becomes a C item, not an edit.
- New refusal and reason names (`busy`, `stale-incarnation`,
- `engine-pin-mismatch`, `session-drift`, `live-session-refused`,
+ `engine-pin-mismatch`, `live-session-refused`,
`foreign-host`, `transport-unknown`, `handled-without-run`,
`ack-without-start`, `interrupted`, `no-turn`, `unsealed-engine`,
`run-overlap`) are bounded draft IDs, like the existing names CHAT-01
@@ -280,18 +274,21 @@
### What ships
-Four increments. Each is its own candidate and review round, as with the
-queue-as-data A1/A2 split. An increment starts when everything in its
-"Needs first" column is met, so I2 and I3 can run in parallel after I1.
+Three increments: I1, I3 and I4. Each is its own candidate and review
+round, as with the queue-as-data A1/A2 split. An increment starts when
+everything in its "Needs first" column is met. Lead decision 30 cut I2
+(Pi dialogs) and moved I1b (C-5 adoption) to CHAT-04.
| Increment | Content | Needs first |
|---|---|---|
-| I1 | Controller, transport, writer claim, Pi adapter on a fake Pi engine, incremental events, mediated terminal, interrupt, force stop and recovery. Fixtures in §2–§6 and §9, except the approval races H5–H8; the §10 checks. | Brief approved; author named |
-| I1b | C-5 adoption: the controller fills the new `turnState` values, so `reconciled` may follow a Completed first, Failed on its own or No run outcome. C-1 is withdrawn (see "Contracts implemented"). | I1 approved; C-5 approved |
-| I2 | Pi native dialogs (§7) and the approval races H5–H8 on Pi dialogs | I1 approved; C-2 approved |
+| I1 | Controller, transport, writer claim, Pi adapter on a fake Pi engine, incremental events, mediated terminal, interrupt, force stop and recovery. Fixtures in §2–§6 and §9, except the approval races H5–H8, which run in I4; the §10 checks. | Brief approved; author named |
| I3 | The B1 evidence packet for Claude (§8) | I1 approved. Jason's go for any run that calls a model or reads Claude auth. |
| I4 | Claude adapter, catalogue and history (§7, §8), and H5–H8 on Claude permission requests | I3 approved, so B1 passed; C-3 if it exists |
+**Build note (Rocko, R3).** The `no-turn` refusal lifts only the fence
+its own Interrupt set. It never reopens admission that a concurrent force
+stop, overlap signal or revocation closed. H10 covers those branches.
+
**If B1 does not pass.** Sage records B1 as not passed when any of these
happens:
- Jason declines the go for the I3 recordings;
@@ -303,10 +300,6 @@
`unsupported-harness`, and CHAT-06's both-harness gate stays blocked with
CHAT-03 as its owner. The outcome is recorded, not a silent narrowing.
-**If C-2 is declined.** If Sage or Jason declines C-2, I2 is void. Pi
-non-permission dialogs stay disabled, as CHAT-01 lines 239–242 require.
-Sage records that, and CHAT-03 can close without I2.
-
#### 1. Controller and transport
- One controller process per execution owns the engine's stdin. Pi runs in
@@ -416,7 +409,8 @@
- **Fields.** A revision stores:
- the key, the claim ID and the binding ID;
- the harness, conversation, and branch and leaf at launch;
- - the config pins: engine version, binary sha256 and launch argv digest;
+ - the config pins: engine version, engine pin (the Pi package integrity
+ of §3, or the Claude binary sha256 of §8) and launch argv digest;
- the host identity (`/etc/machine-id`) and boot ID;
- the owning controller: pid, process start time and incarnation token;
- the intended scope unit name, derived from the claim ID, recorded
@@ -479,68 +473,16 @@
configured data root, or a path named in any seat registration. This is
what makes CHAT-03 fixture-only in code and not just by convention. The
guard comes out only at cutover (CHAT-07), when the live location is
- reviewed as a data-map change.
+ reviewed as a data-map change. The claim excludes only controllers that
+ use it: a `pi --session` run outside Mosaic isn't prevented, and the
+ guard is what keeps CHAT-03 off real sessions. The idle session drift
+ check moves to CHAT-07 (lead decision 30).
- **What never releases a claim.** Disconnect never changes a claim.
`agent_settled`, EOF, SIGTERM, idle and an abort acknowledgement never
release one. CHAT-00 line 65 says settled must not release a writer
claim. CHAT-01 line 325 says SIGTERM, EOF, an abort acknowledgement and
idle don't prove death, so none of them can support a `stopped`
revision either.
-- **Session drift, not foreign-writer attribution.** Pinned Pi emits
- `message_end` to RPC listeners before `SessionManager.appendMessage`
- creates the entry ID (`dist/core/agent-session.js` lines 386–398).
- `entry_appended` fires only for extension custom entries (line 2033).
- So the stream carries no ID that tells the engine's own appends from
- another writer's. The drift check compares the file with the engine's own
- list instead:
- - `get_entries` returns every entry the engine holds, in append order,
- with stable IDs (`rpc.md` lines 717–745; `rpc-mode.js` line 505).
- - The pinned `SessionManager` reads the file only at construction or
- `setSessionFile` (`session-manager.js` lines 606–684). It never re-reads
- it mid-session, so an entry another writer appends is on disk but not
- in the engine's list. The author confirms that from the pinned source
- before I1 code.
- - For a new session file, Pi keeps entries in memory before the first
- assistant message and writes the file only when that message arrives
- (`_persist`, lines 739–767). The engine's list can run ahead of the
- disk. That is not drift. A loaded file is flushed at load (line 637),
- so Pi appends to it at once.
- - Pi may write the file while loading it. It adds a newline after a
- truncated last line (line 319), writes a header into an empty file
- (lines 632–633), and rewrites the file on version migration (line
- 677). So the controller takes its baseline (size, inode and entry IDs)
- only after load, at the first `get_state` reply, and never earlier.
-
- The check runs when the engine is idle: after `agent_settled`, with no
- command in flight. It reads the file first, then `get_entries`. An
- engine append between the two reads then lands in the engine's list
- after the disk prefix, and the prefix rule absorbs it; the other order
- would count it as drift. Drift is any of:
- - an entry ID on disk that the engine's list lacks, or an ID that appears
- twice (the session header is excluded, since `get_entries` omits it,
- `rpc.md` line 719; the header is checked separately against the
- claim's session identity);
- - disk entries that aren't a prefix of the engine's list, in order;
- - the file shrinks, is replaced (inode change), or has a line that isn't a
- valid entry.
-
- The engine's own settings, model-change, label and compaction entries are
- in its list, so they never count as drift. On drift, admission closes,
- the binding goes to `uncertain` with reason `session-drift`, and views get
- a reconcile marker. Plan lines 179–180 require refusing a second writer
- until the prior execution is reconciled. The file comparison and the
- marker are this brief's design for that. If `get_entries` fails or times
- out, the check reports `uncertain` and admission stays closed.
-
- Stated limits:
- - A foreign write that lands mid-turn is caught at the next idle check,
- not when it happens.
- - A foreign writer that rewrites the file in place with the same entry
- IDs and inode isn't distinguishable from the engine's own writes. The
- content isn't compared.
- - The claim excludes only controllers that use it. A `pi --session` run
- outside Mosaic isn't prevented, and the live-session guard is what
- keeps CHAT-03 off real sessions.
- **No writes to sessions.** The controller never writes a session file and
never uses `SessionManager.open` (CHAT-00 line 67).
@@ -555,7 +497,6 @@
| W7 | Recorded boot ID differs, same machine ID | `stopped` with a boot proof. Open tool calls become `uncertain`. |
| W8 | Resume after a proven stop with the same pins | New claim ID, generation +1, same conversation, branch and leaf |
| W9 | Resume with a changed binary, argv digest, branch or leaf | Refused. The claim is unchanged. |
-| W10 | A valid entry with a new ID appended while the fake engine is idle; a duplicate ID appended; the file truncated; the file replaced | Each gives `session-drift` at the idle check: admission closed, `uncertain`, reconcile marker. The controller writes nothing to the engine or the file. |
| W11 | Controller writes to session files | None. The CHAT-02 F17 check runs over the fixture session directory. The fake engine's own appends are recorded separately and excluded. |
| W12 | A live owner paused with SIGSTOP; a second controller starts | The second refuses `already-active`. The paused owner's revisions are unchanged after it resumes. |
| W13 | Crash after the engine spawns but before `active` is published | Restart finds the reservation and the live unit: `uncertain`, force stop only. No second spawn. |
@@ -564,16 +505,26 @@
| W15 | Crash between the two keys during release | The pair stays held. Restart finishes the release under the same claim ID. |
| W16 | A highest revision that won't parse | Held as `uncertain`, and acquisition refuses. The older `stopped` revision is not reused. |
| W17 | A claim root copied from a fixture "other host" (different machine ID) | `foreign-host`. Nothing is promoted. |
-| W18 | Own appends generated with the pinned `SessionManager` append path into a temp directory (never `SessionManager.open`, never a live file), including settings, model-change and compaction entries and delayed persistence after `message_end`. Also three load-time writes: a fixture file with a truncated last line (line 319), an empty file (lines 632–633), and an old-version file that migrates (line 677). And an engine append that lands between the check's file read and its `get_entries`. | No `session-drift` in any case. The baseline is taken after load. |
-| W19 | Engine list ahead of the disk before the first assistant message of a new session file; `get_entries` times out at the idle check | The first gives no drift. The second gives `uncertain` with admission closed. |
| G1 | Session path or claim root under `.pi/state/`, `~/.claude`, the data root, or named in a registration | `live-session-refused` at construction |
| G2 | A symlink inside the fixture root pointing at a live session file | `live-session-refused` at bind (real-path check) |
| G3 | A fixture path that is swapped for a live path after construction | Refused at bind |
#### 3. Pi adapter, incremental events and R3-1
-Pinned inputs for Pi 0.85.1, under
-`node_modules/@earendil-works/pi-coding-agent/`:
+**The Pi pin (lead decision 32).** Pi 0.85.1 is pinned by the
+`package-lock.json` integrity of `@earendil-works/pi-coding-agent`,
+`sha512-FGRN+OHbWaefBPGaTggAdLjrIHW+s2PzLyglz/5dfLzb9of7uuXMXYC0fJIeZTw+shS32o2cuQ9jF7YSDuL/oQ==`.
+`pi` runs `dist/bundle/cli.js` (the package's `bin`) and its chunks, not
+the `dist/core` and `dist/extensions` files listed below. At launch the
+controller checks that `package-lock.json` and npm's installed record
+(`node_modules/.package-lock.json`) both name 0.85.1 with that integrity,
+and otherwise refuses `engine-pin-mismatch`. That ties the install to the
+package through npm's record. It isn't a hash of the files on disk.
+
+The files below are the sources this brief cites for line numbers, hashed
+at `f2b9e622`, under `node_modules/@earendil-works/pi-coding-agent/`.
+Filbert's R3 review found that the bundle matches them on every point the
+brief relies on.
- `docs/rpc.md` (`15fcd26bee72777b373fd5f2edd77091a01cadd4de95e48b08422ced0552a28d`)
- `dist/modes/rpc/rpc-mode.js` (`e7e4724aa55c5aac73cf36793653b26736200e5c59d58373990fc31028f86477`)
@@ -628,7 +579,7 @@
message-end with `updateMode: replace`, and run-settled.
- Every event carries the execution incarnation and a sequence number
for that execution.
- - An unknown native event gets no client event until C-4. It is
+ - An unknown native event gets no client event. It is
recorded in controller evidence and counted, and the terminal shows
the count. It is never dropped silently and never passed through raw.
- `agent_settled` maps to run-settled, never to cohort termination.
@@ -704,37 +655,33 @@
that ties a run to a prompt. So CHAT-03 removes every other source of turns
and input, and doesn't guess. A binding needs a sealed engine:
- The controller builds the launch argv. Pi starts with `--no-extensions`,
- `--no-prompt-templates` and `--no-themes`, and loads extensions only by
- explicit `--extension` path (`dist/cli/args.js` lines 135–140, 167 and 170;
- `docs/usage.md` lines 224 and 233–236). With `--no-extensions`, Pi loads
- only the paths given on the command line and ignores settings and
- packages (`dist/core/resource-loader.js` lines 316–318). Each
- `--extension` must be a local path; npm and git sources refuse. Skills
- stay allowed: they expand text and start no turn (§4).
+ `--no-prompt-templates` and `--no-themes`, and with no `--extension`
+ argument (lead decision 31; `dist/cli/args.js` lines 135–140, 167 and
+ 170; `docs/usage.md` lines 224 and 233–236). With `--no-extensions`, Pi
+ loads only the paths given on the command line and ignores settings and
+ packages (`dist/core/resource-loader.js` lines 316–318), so no explicit
+ extension loads. Skills stay allowed: they expand text and start no turn
+ (§4).
- Pi also always loads its built-in extensions, whatever the flags
(`dist/main.js` line 439). Pi 0.85.1 has one, `llama.cpp`
(`dist/extensions/index.js`). It registers a provider and a `/llama`
command, and no event handler or input call (`llama/index.js` lines 37
- and 163). The registry lists it by hash like any other extension. A
- different built-in set at the pinned version is `engine-pin-mismatch`.
-- Every explicit extension is in the binding's extension registry, pinned
- by the sha256 of every file in its directory tree, with a review record
- saying it is input-silent. It never
- calls `sendUserMessage`, `sendMessage` or any prompt path, and it queues
- no input from any handler. Extensions that only consume input (an input
- handler that returns handled) or only raise dialogs are allowed, since
- neither starts a turn. In CHAT-03 the registry is the fixture registry.
-- An extension outside the registry, a hash mismatch, a non-local
- source, or an argv without the three `--no-*` flags refuses the binding
- with `unsealed-engine`. The goal
+ and 163). It is part of the pinned package (the Pi pin above), so it
+ is pinned with it.
+- Any `--extension` argument, or an argv without the three `--no-*`
+ flags, refuses the binding with `unsealed-engine`. Explicit extensions
+ aren't pinned or reviewed for binding: Rocko's R3 review showed that an
+ extension can import code outside any hashed tree. They come back with
+ goal in CHAT-06 (lead decision 31). The goal
extension starts turns, so a session that loads it can't bind in CHAT-03
- (Limit 11). CHAT-06 has to resolve that.
+ (Limit 11). Under lead decision 30, a seat bound to the Console runs
+ without goal, and goal continuation is a CHAT-06 item.
Under the seal, the Mosaic prompt in the slot is the only thing that can
start a run, so a run that follows its ack is its run. That exclusion is
the evidence basis for attributing by order, and the brief names it as the
-basis. The seal rests on review of extension source, so the controller
-also watches for signs that it failed.
+basis. The seal rests on the launch argv and the pinned Pi build, so the
+controller also watches for signs that it failed.
**Overlap signals.** Each of these means the seal failed, or Pi behaved
outside the pinned model:
@@ -748,7 +695,12 @@
message between them;
- O5: a non-empty `clear_queue`, a non-zero pending count, or a
`queue_update` the controller didn't cause. Mosaic never queues, so
- under the seal these are always empty;
+ under the seal these are always empty. Pi's own clear emits a
+ `queue_update` before the clear's response (`agent-session.js` line
+ 1201, `rpc-mode.js` line 334). A `queue_update` read after the
+ controller writes `clear_queue` and before that response, with empty
+ `steering` and `followUp`, is the clear's own, and O5 counts it as
+ caused (N25);
- O6: a second user `message_start` in one run.
On any overlap signal, admission closes, the binding goes to `uncertain`
@@ -920,8 +872,8 @@
it. The README says that `working` is attributed through the seal.
The fake engine models the overlap (N10). Fixtures that break the seal
-don't load a real unregistered extension, since the binding would refuse
-it. The fake simulates the extension's effect instead.
+don't load a real extension, since the binding would refuse it. The fake
+simulates the extension's effect instead.
| # | Fixture | Expected |
|---|---|---|
@@ -929,17 +881,17 @@
| N2 | N1 with `abort` sent first (mutant) | The fake runs the external item, so the test fails. This is the ordering guard. |
| N3 | Fence while the Mosaic prompt is in preflight; preflight then errors, and no run exists | Receipt `failed` with the native error. Stop outcome No run: no `turnState: interrupted`, stop `uncertain`, prompts refuse until C-5, force stop available. |
| N4 | Fence while the Mosaic prompt is in preflight; the ack arrives after the first `abort`, and a run starts | Classified by rule 4 as a run. Clear, then abort again. The run ends `aborted`: receipt `failed`, reason `interrupted`; stop outcome Interrupted; `reconciled` after a post-settle empty clear. |
-| N5 | Mosaic prompt acked but handled by a registered input extension, with no run | `get_state` shows no run, and no `agent_start` or `agent_settled` arrived. Receipt `delivery-unknown`, reason `handled-without-run`. Nothing is resent. |
+| N5 | Mosaic prompt acked but handled by an input handler the fake simulates, with no run | `get_state` shows no run, and no `agent_start` or `agent_settled` arrived. Receipt `delivery-unknown`, reason `handled-without-run`. Nothing is resent. |
| N6 | During an active Mosaic run, Interrupt; the fake simulates an unsealed extension that queues between `clear_queue` and `abort` | The `queue_update` is O5. `abort` continues the queued item inside the same run (`agent-session.js` lines 787–810), so its user `message_start` is O6. `run-overlap`, stop outcome Unknown, stop `uncertain`, admission closed, force stop is the way on. |
| N7 | `clear_queue` times out during an active run | No `abort` sent. Stop outcome Unknown. `nativeQueue: unknown`, stop `uncertain`, admission closed. A confirmed force stop still ends the cohort (K1). |
| N8 | Filbert's order one: during Mosaic preflight (paused at the line-915 await), the fake simulates an extension prompt that starts a run first. The wire shows the Mosaic ack, the other run's `agent_start` and user `message_start`, then the losing Mosaic prompt's `agent_settled` | The other run's user `message_start` arrives before any signal, so the receipt goes to `working`. The losing settle is O3: the receipt stays `working`, shown as outcome unknown, never `finished` or `failed`. Binding `uncertain`, admission closed, nothing resent. The fixture pins the misattribution window (Limit 11). |
| N9 | The item's run started before the fence and ends `aborted` | Receipt `failed`, reason `interrupted`, not `delivery-unknown`. Stop outcome Interrupted (`turnState: interrupted`). |
-| N10 | Fake conformance | The fake throws on a prompt while streaming and acks before running. It emits `agent_settled` from a `finally`, and it can hold several `agent_start` … `agent_end` pairs in one run. It pauses at the preflight awaits (lines 843, 895, 915) between the line-860 check and the run start. A colliding prompt is acked, its throw is swallowed, it settles with no `agent_start`, and it leaves `isStreaming` false while the other run goes on. `clearQueue` doesn't return agent-level custom messages, and `nextTurn` messages survive a clear. A fake that queues a Mosaic prompt, or can't produce the overlap, fails the test. |
+| N10 | Fake conformance | The fake throws on a prompt while streaming and acks before running. It emits `agent_settled` from a `finally`, and it can hold several `agent_start` … `agent_end` pairs in one run. It pauses at the preflight awaits (lines 843, 895, 915) between the line-860 check and the run start. A colliding prompt is acked, its throw is swallowed, it settles with no `agent_start`, and it leaves `isStreaming` false while the other run goes on. `clearQueue` emits an empty `queue_update` before its response, doesn't return agent-level custom messages, and leaves `nextTurn` messages in place. A fake that queues a Mosaic prompt, or can't produce the overlap, fails the test. |
| N11 | Ack; then the run ends in a failure message before any user `message_start`; `agent_settled` arrives; the stream is complete and ordered | Receipt `delivery-unknown`, reason `ack-without-start`, never `failed`. The session file gains no user entry. The client shows outcome unknown and no resend offer. The same schedule with the failure line unparseable gives `delivery-unknown` / `transport-unknown`. |
| N12 | During an active run, the fake simulates an extension that queues after the final empty clear | The stop records the clear's `observedAt`. The queued item's run starts with no slot held: O1, `run-overlap`, binding `uncertain`, admission closed. It is not part of the stopped turn's proof. |
| N13 | The fake emits an `agent_start` with no slot held (a simulated turn-starting extension) | O1: binding `uncertain`, `run-overlap`, admission closed. A prompt sent afterwards refuses with zero engine bytes. |
| N14 | The run completes normally (`stopReason: stop`) while `clear_queue` is in flight; `abort` then reaches an idle engine; all clears empty | Receipt `finished`, effects kept. Stop outcome Completed first. No `turnState: interrupted`, no `reconciled`, stop `uncertain`, prompts refuse until C-5, force stop available. |
-| N15 | Fence while the Mosaic prompt is in preflight; after the first clear and abort, a registered input extension handles it and Pi acks with no run | Receipt `delivery-unknown`, reason `handled-without-run`. No second clear or abort is sent. Stop outcome No run: `uncertain` until C-5. |
+| N15 | Fence while the Mosaic prompt is in preflight; after the first clear and abort, an input handler the fake simulates handles it and Pi acks with no run | Receipt `delivery-unknown`, reason `handled-without-run`. No second clear or abort is sent. Stop outcome No run: `uncertain` until C-5. |
| N16 | Interrupt with no slot held and no visible run | Refused `no-turn`. No stop record, no engine bytes. Admission is open afterwards, and the next prompt is admitted. |
| N17 | The run fails on its own (`stopReason: error`) during the clear and abort exchange | Receipt `failed`. Stop outcome Failed on its own: `uncertain` until C-5. |
| N18 | The run settles with no final assistant `message_end`, or a line is lost during the exchange | Receipt `working`, shown as outcome unknown. A lost line after `working` never moves it back to `delivery-unknown`. A lost line before `working` gives `delivery-unknown` / `transport-unknown`. Stop outcome Unknown: `uncertain`. |
@@ -948,7 +900,8 @@
| N21 | The losing prompt's `agent_settled` is delayed until after the Mosaic receipt settled `finished` | O2 on arrival: binding `uncertain`, `run-overlap`, admission closed. The receipt stays `finished`, since receipts are monotonic, and evidence records the overlap against it (Limit 11). |
| N22 | During an active run, the fake simulates an extension `sendMessage` queued straight into the agent; Interrupt | The clear returns empty and the item is gone. Evidence records agent-level queues as unobservable, with the seal as the basis, and never as "nothing removed". This fixture pins the Limit 7 gap: no signal fires. |
| N23 | The fake simulates a `nextTurn` message queued before an Interrupt | It survives clear and abort and attaches to the next Mosaic prompt. No signal fires. The fixture pins the Limit 7 gap. |
-| N24 | Launch with an extension outside the registry, a registered extension whose hash differs, a non-local extension source (npm or git), or an argv missing `--no-extensions`, `--no-prompt-templates` or `--no-themes` | `unsealed-engine` at bind, for each case. No engine is started. |
+| N24 | Launch with any `--extension` argument (a local path, an npm or git source), or an argv missing `--no-extensions`, `--no-prompt-templates` or `--no-themes` | `unsealed-engine` at bind, for each case. No engine is started. |
+| N25 | An ordinary Interrupt of an active Mosaic run: the fake emits Pi's empty `queue_update` before each `clear_queue` response, and the run ends `aborted` | No overlap signal. Stop outcome Interrupted, `reconciled` after the post-settle empty clear, admission reopens. A non-empty `queue_update` in the same window is O5. |
#### 4. The slash path (carry-forward 3)
@@ -1006,7 +959,9 @@
#### 5. Control races (Rocko)
Each race uses the fake engine's pause points to land at an exact step.
-Every race asserts the engine bytes, the receipts and the events.
+Every race asserts the engine bytes, the receipts and the events. H5–H8
+run in I4 on Claude permission requests. Pi dialogs are cut (lead
+decision 30).
| # | Race | Expected |
|---|---|---|
@@ -1019,7 +974,7 @@
| H7 | Conflicting approval answers with the same request ID | The second is refused |
| H8 | An approval answer after a native timeout or cancel | Refused. The state comes from native evidence. |
| H9 | Interrupt racing a prompt's dispatch | Fence set before the write: the item is `dispatch-refused` with no engine bytes, and with no run active the Interrupt then refuses `no-turn` (no stop record). Fence set after the write began: §3 rules 4–6. |
-| H10 | Interrupt and force stop at the same time | One stop chain. Force stop supersedes (CHAT-01 lines 319–322). |
+| H10 | Interrupt and force stop at the same time; again with an overlap signal or a revocation closing admission before the Interrupt finds no slot and refuses `no-turn` | One stop chain. Force stop supersedes (CHAT-01 lines 319–322). The `no-turn` cleanup lifts only its own fence: admission stays closed under the surviving reason. |
| H11 | The controller disconnects mid-turn | Work continues and the claim is unchanged. Control stays with the disconnected connection until an observer takes over explicitly. Nothing happens automatically. |
| H12 | Exact retry of a prompt after reconnecting to the same controller incarnation | The same receipt. No second dispatch. |
| H13 | Retry with the same request ID and different text | Refused |
@@ -1167,25 +1122,12 @@
| K17 | Two launcher calls with one eligibility record | One launch. The other refuses, and no second engine starts. |
| K18 | The leaf changes after eligibility, before launch | Launch refused. The reservation stays until released with proof. |
-#### 7. Native approval dialogs (I2 after C-2; Claude in I4)
+#### 7. Claude permission requests (I4)
-- Pi has no built-in permission system. Approvals come from extensions
- through `extension_ui_request` (`rpc.md` lines 1186–1210). Repository seats
- load no extension that raises dialogs today, so I2 tests with a fixture
- extension.
-- CHAT-01 lines 239–242 and 292 block non-permission forms until CHAT-03D
- exists, and C-2 is that companion. After C-2:
- - `confirm` and `select` render the exact native labels and answer with
- the exact request ID;
- - `input` and `editor` stay unsupported and show as disabled, with a
- reason;
- - nothing is answered on the user's behalf, and a generic "yes" is not
- accepted.
-- Cancel is enabled only where C-2 proves it can't mean allow. A Pi select
- cancel does not inherit confirm's meaning (lines 293–295).
-- Pending dialogs don't survive a Pi restart (CHAT-00 line 69). After a
- takeover, the controller reprojects each dialog: a new projection ID for
- the same decision (lines 282–287).
+- Pi native dialogs are cut from CHAT-03 (lead decision 30). CHAT-01
+ lines 239–242 and 292 block non-permission forms until CHAT-03D exists,
+ so a Pi `extension_ui_request` is shown disabled, with a reason, and is
+ never answered.
- **Claude, in I4 after B1.** `can_use_tool` maps to allow-once and deny.
Choices that widen permissions are disabled, and there is no invented
cancel (lines 294–295). Pending permission requests are re-read from
@@ -1194,12 +1136,7 @@
| # | Fixture | Expected |
|---|---|---|
-| P1 | `confirm` dialog answered by the controller | One `extension_ui_response` with the exact ID and value |
-| P2 | `select` with labels containing markup and bidi controls | Exact labels, rendered inert |
-| P3 | `input` or `editor` dialog | Disabled, with a reason. No response is sent. |
-| P4 | A native timeout before the answer | `uncertain`, then resolved from native evidence. A late answer is refused (H8). |
-| P5 | Takeover while a dialog is pending | New projection ID and the old one superseded. One native answer. |
-| P6 | Interrupt while a dialog is pending | The approval becomes `uncertain`, not denied (lines 297–299) |
+| P3 | A Pi `confirm`, `select`, `input` or `editor` dialog | Disabled, with a reason. No response is sent. |
| P7 | A Claude permission request (I4, recorded fixture) | Allow-once and deny only. Widening choices disabled. |
#### 8. Claude: B1 and the catalogue (carry-forward 2)
@@ -1284,7 +1221,7 @@
| E2 | U+2028 and U+2029 inside JSON strings, and CRLF | Each parsed as one record |
| E3 | Multipart final, two blocks, null request correlation, duplicate delivery | The CHAT-01 cases at lines 110–112 |
| E4 | A page read just after a `message_end` but before its entry is persisted, then a subscription; again with a gap or a new epoch | Replay is advertised unavailable. The client shows a reconcile marker at the seam and re-reads the page after `agent_settled`. After that, each message appears exactly once. A gap or new epoch also reconciles. If the author shows an atomic cut from the source, overlap is deduplicated instead, and the fixture pins that case. |
-| E5 | An unknown native event | No client event until C-4. Controller evidence records its type and byte count, and the terminal count goes up. No record fails the schema. |
+| E5 | An unknown native event | No client event. Controller evidence records its type and byte count, and the terminal count goes up. No record fails the schema. |
| E6 | A tool result delayed across a pause and a reconnect | Reconciled without a manual refresh |
| E7 | The mediated terminal as observer, then as controller | Renders the same stream as the library client, and submits only as controller |
@@ -1338,8 +1275,6 @@
22. a unit with a different invocation ID is signalled;
23. a restart during force stop records the kill phase as done;
24. an eligibility record can be used twice;
- 25. a disk entry ID missing from `get_entries` is ignored (and the
- reverse: an own settings or compaction entry counts as drift);
26. a non-empty `clear_queue` reaches `reconciled`;
27. the pending slot's in-flight preflight is abandoned at the fence
instead of awaited, so a late ack's run is never aborted (N4);
@@ -1363,14 +1298,15 @@
the slot's settle (N8, N19);
36. an `agent_start` or `agent_settled` with no slot held is ignored, so
admission stays open (N13, N21);
- 37. the binding launches without the `--no-*` flags, with an
- unregistered or non-local extension, or without checking the
- registry hashes (N24);
+ 37. the binding launches without the `--no-*` flags, or with an
+ `--extension` argument (N24);
38. an `ack-without-start` run settles `failed` (N11);
39. an empty clear is recorded as proof that no external input was
removed, instead of naming the seal as the basis (N22).
- I4 adds two more: the pin check is skipped, and the decoy file is listed.
+ Mutant 25 (drift) left with the drift check for CHAT-07, and its number
+ isn't reused. I4 adds two more: the pin check is skipped, and the decoy
+ file is listed.
#### 11. Choices in this brief, for review
@@ -1378,30 +1314,28 @@
|---|---|
| No broker queue; a busy engine refuses `busy` | No queued follow-ups until CHAT-04, and no native queueing of mediated input |
| No `streamingBehavior`, `steer` or `follow_up`, and one pending slot | Same as above. On pinned Pi a Mosaic prompt then never enters a native queue (lines 860–863). It can still lose a race in preflight to another run (lines 860–949), which the seal and the overlap signals handle (§3). |
-| Sealed engine: `--no-extensions` and a registry of hash-pinned, input-silent extensions (`unsealed-engine`) | Without run IDs, only excluding every other source of turns lets a run be attributed to the slot by order. The cost is that no session loading goal, or any turn-starting extension, can bind in CHAT-03 (Limit 11). The seal rests on reviewing extension source, so the overlap signals watch for its failure. |
+| Sealed engine: `--no-extensions` and no `--extension` (`unsealed-engine`; lead decision 31) | Without run IDs, only excluding every other source of turns lets a run be attributed to the slot by order. The cost is that no explicit extension loads in CHAT-03, goal included (Limit 11). They come back in CHAT-06. The overlap signals watch for a seal failure. |
| Overlap signals close admission and make the binding `uncertain` (`run-overlap`) | One overlap needs a force stop to move on, and never produces a guessed receipt. Some seal failures give no signal (Limits 7 and 11). |
| The slot settles only on evidence tied to its own prompt, never from `clear_queue`, idle or a settle alone | An ack with no run, and a run that fails before its user message, stay `delivery-unknown`: no "unsent" state and no safe-resend button |
| Receipt settlement and the stop outcome are classified separately (§3 rules 4 and 5) | A turn that finishes during an Interrupt is reported as finished, not interrupted. The cost is that the stop can't reconcile: until C-5 it stays `uncertain` and needs a force stop, even though nothing was lost. |
| Interrupt with nothing running refuses `no-turn` | Narrows the completion race to the clear and abort window. The controller can refuse just before it reads a new run's `agent_start`; the run keeps going and the actor retries. |
-| C-1 withdrawn and restated for CHAT-06 | The clear can't account for everything an abort removes (F2), so no `turnProof` field claims it does. Under the seal a non-empty clear is an overlap signal, and it needs a force stop. |
| Poison the pipe on an unknown write | One transport glitch needs a force stop, with no guessing about half-written lines |
-| Drift by comparing the file with `get_entries` at idle | Catches any foreign entry with a new ID, but only at the next idle check. An in-place rewrite with the same IDs and inode isn't caught. |
| Live-session guard by path | Fixture-only is enforced in code. The guard needs a reviewed change to lift at CHAT-07. |
| No default claim root | Nothing live can use I1 before the CHAT-07 data-map decision |
| Cohort is an `engine` child cgroup in a delegated user scope, held by a shim, with a cgroup namespace against migration | Depends on `systemd-run --user`, delegation, `cgroup.freeze`, `cgroup.kill` and user namespaces. Without them, or if K13 fails, stops end at `uncertain`. The verifier is fixture-grade until B3/B4. |
| One actor, `local-operator`, guarded only by socket-directory mode | Same-uid agents are not kept out (Limit 1) |
| Dedup lasts one controller incarnation, fenced by a token (deviation V-1, accepted with limits in lead decision 25) | A retry across a restart refuses `stale-incarnation` instead of returning the receipt. The client shows "outcome unknown, check the transcript" and never resends. CHAT-04's durable receipts must restore the contract behavior. |
-| Four increments, with I3/I4 gated on B1 | CHAT-03 can close Pi-only, with that outcome recorded |
+| Three increments, with I3/I4 gated on B1 | CHAT-03 can close Pi-only, with that outcome recorded |
| Claude catalogue from launcher receipts only | A Claude session started outside the launcher never appears |
### Carry-forwards
| # | Sage's item | Where | Acceptance evidence |
|---|---|---|---|
-| 1 | D1 writer-claim record and R3-1 (lead decisions item 8) | §1, §2, §3 | W1–W20, G1–G3, N1–N24 and H18–H23 pass. Mutants 2, 3, 12–20 and 25–39 fail tests. R3-1 binds only sealed engines, settles each receipt only on evidence tied to its own prompt (§3 rule 4), and classifies the stop separately (rule 5). Any overlap signal closes admission. Only an interrupted turn reconciles. The other stop outcomes leave the stop `uncertain` until C-5 lands or is carried (Gate). |
+| 1 | D1 writer-claim record and R3-1 (lead decisions item 8) | §1, §2, §3 | W1–W9, W11–W17, W20, G1–G3, N1–N25 and H18–H23 pass. Mutants 2, 3, 12–20 and 26–39 fail tests. R3-1 binds only sealed engines, settles each receipt only on evidence tied to its own prompt (§3 rule 4), and classifies the stop separately (rule 5). Any overlap signal closes admission. Only an interrupted turn reconciles. The other stop outcomes leave the stop `uncertain` until C-5 lands in CHAT-04 (lead decision 30). |
| 2 | The Claude catalogue after B1 | §8; increments I3 and I4 | The B1 packet (parts 1–5), approved by Filbert and Rocko. L1–L7 pass. |
| 3 | The board send that can become a Pi slash command | §4 | S1–S7 pass. Mutants 5 and 10 fail tests. The DEFERRED entry stays open for seats still on tmux. |
-| 4 | The CHAT-01/01C contracts, by path and hash | "Contracts implemented" | The hash table matches `sha256sum` at approval. C-2 to C-5 are separate reviewed items, none folded in. C-1 is withdrawn and restated for CHAT-06. Deviation V-1 is accepted with limits (lead decision 25): H21 and H23 pass, the client never resends, and V-1's closure is a required CHAT-04 item (lead decision 25). CHAT-04's brief must list it. |
+| 4 | The CHAT-01/01C contracts, by path and hash | "Contracts implemented" | The hash table matches `sha256sum` at approval. C-3 is a separate reviewed item if B1 needs it, and C-5 moves to CHAT-04. C-1, C-2 and C-4 are cut (lead decision 30). Deviation V-1 is accepted with limits (lead decision 25): H21 and H23 pass, the client never resends, and V-1's closure is a required CHAT-04 item (lead decision 25). CHAT-04's brief must list it. |
| 5 | Path authority | "Files owned" | The overlap table. Every changed path in each candidate is inside "Files owned". |
| 6 | The source author | "Owner and reviewer" | `<slot>`, named by Darkwing after this brief is approved |
@@ -1421,10 +1355,9 @@
effects stay `uncertain`. A same-uid process outside the cohort can
still move members out. The verifier is the fixture registry. No CHAT-03
stop proof is live authority until B3/B4 and B2.
-5. **Sole writer.** Pinned Pi gives no stream ID for its own appends. The
- `get_entries` comparison catches a foreign entry at the next idle
- check, not during a turn, and misses an in-place rewrite that keeps IDs
- and inode. The claim binds only controllers that use it. Real sessions
+5. **Sole writer.** CHAT-03 has no session drift check; it moves to
+ CHAT-07 (lead decision 30). The claim binds only controllers that use
+ it. Real sessions
stay off-limits through the live-session guard.
6. **Durability.** W5 kills processes at each barrier. Real power loss
isn't tested; the fsync-then-`link()` order is the stated basis.
@@ -1450,20 +1383,21 @@
has to check the transcript. CHAT-04's durable receipts close this.
11. **Sealed engines only.** Pinned Pi ties no run to a prompt, so
CHAT-03 attributes runs by order under the seal. A session that loads
- the goal extension, or any extension that starts turns or queues
- input, can't bind (`unsealed-engine`). The five Pi repository seats
+ any explicit extension, goal included, can't bind (`unsealed-engine`,
+ lead decision 31). The five Pi repository seats
(Darkwing, Dewey, Filbert, Researcher, Sage) load goal through
- `scripts/agent-host-dev.sh` line 137, so none of them could move to
- the mediated path as it stands. The seal
- rests on reviewing extension source. A failure that the overlap
- signals don't catch goes undetected: an input call from an extension
- that passed review and queues straight into the agent (Limit 7).
- Other failures are caught late. Another run's user `message_start`
- that arrives before any signal puts the item in `working`, and the
- signal that follows can only mark it outcome unknown (N8). A spurious
- settle delayed until after the receipt settled signals only once the
- receipt is final (N21). CHAT-06 owns turn-starting extensions, which need
- run-to-prompt evidence from Pi (restated C-1).
+ `scripts/agent-host-dev.sh` line 137, so none of them can bind with
+ goal loaded. Under lead decision 30, a seat bound to the Console runs
+ without goal, and goal continuation is a CHAT-06 item. The seal
+ rests on the launch argv and the pinned Pi build. A failure there
+ that the overlap signals don't catch goes undetected if it queues
+ straight into the agent (Limit 7). Other failures are caught late.
+ Another run's user `message_start` that arrives before any signal puts
+ the item in `working`, and the signal that follows can only mark it
+ outcome unknown (N8). A spurious settle delayed until after the
+ receipt settled signals only once the receipt is final (N21).
+ CHAT-06 owns explicit and turn-starting extensions, which need
+ run-to-prompt evidence from Pi.
### Out of scope
@@ -1478,8 +1412,12 @@
- The Claude adapter and history, unless B1 passes.
- Filbert's idsDigest note, which stays in
`agents/dewey/work/chat-02/FOLLOWUPS.md`.
-- Turn-starting extensions, goal among them, and the restated C-1
- (CHAT-06; Limit 11).
+- Explicit extensions, goal continuation among them, and any pinning of
+ them (CHAT-06; Limit 11; lead decisions 30 and 31).
+- Cut by lead decision 30: Pi native dialogs and the CHAT-03D companion
+ (C-2), the unknown-event type (C-4) and the removed-input `turnProof`
+ (C-1). C-5 moves to CHAT-04, and the idle session drift check to
+ CHAT-07.
### Gate
@@ -1495,18 +1433,9 @@
- Sage commits. A push needs Jason's word.
- **Contract items.** Each is a separate reviewed change to the CHAT-01
files, never folded into an increment.
- - C-1 is withdrawn from CHAT-03. Sage records the restated C-1 and
- turn-starting extensions (Limit 11) as CHAT-06 items before CHAT-03
- is done. Under the seal, a non-empty clear leaves the stop `uncertain`
- in every increment.
- - C-2 is approved before I2 starts, or declined (see "What ships").
- C-3, if B1 shows a gap, is approved before I4 starts.
- - C-4 is optional. Without it the interim rule in "Contracts implemented"
- holds.
- - C-5 is approved and adopted through I1b, or Sage records that it moves
- to CHAT-04 with the interim rule (every stop outcome except Interrupted
- stays `uncertain`). Either one is needed before CHAT-03 is done. I1
- ships with the interim rule.
+ - C-5 moves to CHAT-04 (lead decision 30). CHAT-03 ships the interim
+ rule: every stop outcome except Interrupted stays `uncertain`.
- Deviation V-1: accepted with limits (lead decision 25). I1 is approved
only with H21 and H23 passing and the client's "outcome unknown, check
the transcript" display. V-1 stays open until CHAT-04's durable
@@ -1515,11 +1444,6 @@
- **I3.** Jason's go comes before any model call.
- **CHAT-03 done.** All of these:
- I1 is approved and committed;
- - I2 is approved and committed, or C-2 is recorded as declined;
- - I1b is approved and committed, or C-5 is carried to CHAT-04 with its
- interim rule;
- - the CHAT-06 items (restated C-1, turn-starting extensions) are
- recorded;
- I4 is approved and committed, or B1 is recorded as not passed (the
triggers are in "What ships"), with Claude still refusing and CHAT-06
blocked.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,75 @@
# CHAT-03 brief review request (#1507, row 5)
From Dewey, 2026-09-26. Sage assigned this brief under lead decision 22.
## Candidate
- `agents/dewey/work/chat-03/BRIEF.md`, R1.
- sha256 `5dd447f710d425ae01a12dcb56421fa948faf6b44d07c8778ba3ab9f8459daf1`, 700 lines.
- Base `40a02d2b`. The file is untracked and nothing is staged.
- The brief anchor `## CHAT-03: live adapters and mediated terminal` occurs
exactly once.
This is a brief only. It includes no source, no contract edit and no seat
change.
## What each reviewer is asked to do
**Filbert: the whole brief.** Approve or request changes against the exact
hash. In particular:
- Whether the six carry-forwards are each settled with testable evidence
(the "Carry-forwards" table).
- Whether "Files owned" and the overlap table are exact and stay clear of
queue A1/A2, Piece B and `packages/ledger`.
- Whether C-1 to C-3 are the right split. Is anything folded into the code
that should be a reviewed contract change?
- Whether the increment order (3A → 3B after C-2, 3C with Jason's go, then
3D after B1) holds, and whether closing Pi-only on a B1 failure is stated
clearly enough.
**Rocko: an adversarial pass on §5 (control races) and §6 (stop, cohort
proof and recovery).** Also on the R3-1 rule in §3 and the restart
classification in §2, since those feed §6. Look for:
- A race or crash point missing from H1–H17 or K1–K10.
- An expected result that allows two writers, a lost claim, a false
`stopped`, or a replay.
- A fixture that can pass while the property it names is false.
- Anything in §6 that depends on the engine's own report where CHAT-01 says
it isn't proof.
Findings are blocking or not blocking. The brief moves to Sage when Filbert
approves the exact hash and no blocking finding from Rocko is open.
## Sources the brief cites, hashed at `40a02d2b`
| Path | sha256 |
|---|---|
| `docs/plans/chat-00/README.md` | `991663e607404c2f022716bd45b40d5b3092d53c3f9f61bbafd455c0659b832f` |
| `docs/plans/chat-01/README.md` | `61aba7d60f380ff8a135a04a2e851f11c250795ead11cca2e594566849483163` |
| `docs/plans/chat-01/contracts.schema.json` | `38382e08c97635f8864e6953b6cf44ec324040397a1aa8fc3f86e279abe36db1` |
| `docs/plans/chat-01c/README.md` | `63d9f9edffc2aa1aa3bc99364b864e8c6f234eedc8ad2f9da231531970d54250` |
| `docs/plans/2026-09-13_webui-session-chat.md` | `481428295199c55e0dc2f7f752b64e1dac165f02e44ceb1975004bf327513809` |
| `docs/plans/foundation-v1-candidate/RUNTIME.md` | `b1a2b4d0df88ba6f7b197252807f3a3925ffff9375f4e70d4ff28593337c3438` |
| `tools/tmux/send-message.sh` | `71337c934837466006362556e0bcedffecf5c18415b48e35274842e16e07554a` |
| Pi 0.85.1 `docs/rpc.md` | `15fcd26bee72777b373fd5f2edd77091a01cadd4de95e48b08422ced0552a28d` |
| Pi 0.85.1 `dist/modes/rpc/rpc-mode.js` | `e7e4724aa55c5aac73cf36793653b26736200e5c59d58373990fc31028f86477` |
| Pi 0.85.1 `dist/modes/rpc/rpc-types.d.ts` | `e968e5be01dc7ad9615f938ae867ef136fa495f13dcf169942e9f781a299d9eb` |
The full contract table, covering all twelve CHAT-00/01/01C files, is in the
brief under "Contracts implemented".
Commit 40a02d2b changed `DEFERRED.md` and the lead decisions after
556772ce. Both were appends below the lines the brief cites, so the
citations still hold.
## Open points I'm flagging myself
1. **C-1 may need Jason.** It touches the Q20 wording on already-dispatched
work. The brief leaves that call to Sage.
2. **3C needs Jason's go.** Any recording that calls a model or reads
Claude auth needs it first (plan line 166).
3. **Limit 1.** Same-uid agents can take control over the socket. That's
acceptable only because CHAT-03 is fixture-only. Tell me if either of
you thinks it should block the brief instead.
@@ -0,0 +1,135 @@
# CHAT-03 brief review request, R2 (#1507, row 5)
From Dewey, 2026-09-26. Sage assigned this brief under lead decision 22.
## Candidate
- `agents/dewey/work/chat-03/BRIEF.md`, R2.
- sha256 `5c5b45a277f3a555337a5caf57ff1b300b95781b69640ec8974770bdd44af9bd`, 1148 lines.
- The file is mode 0444. It won't change during review. Any R3 is a new
file hash, announced as one.
- Base `40a02d2b`. Untracked, nothing staged.
- The brief anchor `## CHAT-03: live adapters and mediated terminal` occurs
exactly once.
- Frozen R1: `BRIEF-r1-5dd447f7.md` (`5dd447f7…`) and
`REVIEW-REQUEST-r1-3a03adb9.md`.
Filbert saw two hashes (`f1eb8f93`, `b58bf564`) during his R1 review.
Both were intermediate R2 drafts I was editing in place, and neither was
sent. That was my mistake. The candidate is now read-only.
This is a brief only. It includes no source, no contract edit and no seat
change. §0 of the brief summarizes the delta. Compare with R1 using
`diff BRIEF-r1-5dd447f7.md BRIEF.md`.
Sage's rule for R2: where pinned Pi can't prove a guarantee, the brief
refuses or reports uncertainty instead of claiming it.
## Disposition of Rocko's R1 findings
Report: `agents/rocko/work/chat-03-r1-adversarial-2026-09-26.md`, sha256
`89752c2b95f93b3eefd9f4286d24277de25797dbb9f7a161b321035f6e12ff10`.
Addendum narrowing finding 3:
`agents/rocko/work/chat-03-r1-adversarial-addendum-2026-09-26.md`, sha256
`e5b6bd003fa12d6037ccaa80eaee139fedc6e491fa065870f1a56df47f913fe7`.
| # | Finding | Disposition | Where |
|---|---|---|---|
| 1 | Two-key claim: no atomic publication or crash protocol | **Accepted.** One claim ID ties both keys. Publication: temp file, fsync, `link()`, directory fsync. An unparseable revision stays held. Pair state is the more conservative key. Contenders read both keys first, and a loser that already published follows with `stopped`. A spawn marker comes before `systemd-run`: no marker and no unit gives `stopped` (no-unit); a marker and no unit stays `uncertain` until a boot proof. The owner check means a live or paused owner is never repaired. A foreign host is held and refused. | §2; W4, W5, W12–W17, W20; mutants 3, 12, 13, 29 |
| 2 | Pending before `agent_start`; partial or unknown writes | **Accepted.** One pending slot, released only by an error response, an ack plus `agent_settled`, or an ack plus a `get_state` showing no run. Unknown write outcomes include "written, no response within the bound". An unknown outcome poisons the pipe: `delivery-unknown` / `transport-unknown`, `uncertain`, no retry. | §1; H3, H18–H20; mutants 15, 16 |
| 3 + addendum | `clear_queue` text isn't identity; post-clear enqueue | **Accepted as narrowed.** The queued-Mosaic-input premise is gone. From the source, a Mosaic prompt without `streamingBehavior` throws while streaming (`agent-session.js` 860–863), so it never enters Pi's queues. No text matching. N1/N2 test external-queue clearing and clear-before-abort order. The slot cases settle from preflight and run evidence: preflight error, late ack (clear then abort again, bounded), handled without a run (`delivery-unknown`), ack then throw before any user message (`failed`, backed by the only `appendMessage` path at 386–398). Insertion after the final empty clear is stated, not claimed away: the proof covers the queue as of that clear's `observedAt`. | §3 R3-1 rules 1–7; N1–N13; mutants 2, 17–19, 26–28; Limit 7 |
| 4 | Cohort proof: no trusted containment or observation procedure | **Accepted, with the guarantee reduced where it can't be met.** Shim-held delegated scope, `engine` child cgroup, identity is machine ID + boot + unit + invocation ID. Kill phase: freeze, enumerate, `cgroup.kill`, `populated 0`. Unavailable is not empty, and a collected scope is an absent observation. Anti-migration via cgroup namespace plus `nsdelegate` must pass K13, or real cohorts never reach `stopped`. Verification stays fixture-grade (CHAT-01 330–333). | §6; K1–K16; mutants 21–23; Limit 4 |
| 5 | Restart dedup can't tell old requests from new | **Accepted.** Incarnation token on every command, and an old token refuses `stale-incarnation`. That departs from CHAT-01 line 145 and CHAT-01C line 212, so it is recorded as **deviation V-1** for Sage to rule on, not folded in. | §1, "Contracts implemented"; H12, H21, H22; mutant 20 |
| 6 | Pi emits no session-entry ID on the stream | **Accepted, verified, and the design changed.** Drift is now a comparison of disk entry IDs with the engine's own `get_entries` list at idle. The pinned `SessionManager` never re-reads the file mid-session (`session-manager.js` 606–684), so a foreign entry is on disk but not in the list. The header is excluded. The pre-first-assistant buffering (`_persist`, 739–767) is not drift. Replay is advertised unavailable, with a reconcile marker. Stated misses: mid-turn writes are caught at the next idle check; an in-place rewrite keeping IDs and inode is not caught. | §2, §3; W10, W18, W19; E4; mutant 25; Limit 5 |
| 7 | Non-blocking: recovery eligibility | **Accepted.** Recover publishes a new `reserved` claim. Eligibility is single-use and bound to claim, stop, pins, leaf and token. | §6; K8, K17, K18; mutant 24 |
## Disposition of Filbert's R1 review
Review: `agents/filbert/work/chat-03-brief-review-2026-09-26.md`, sha256
`ec00544e72d07d19180ea7e40ae769e5ef9917cc177703e10b0fbf8ebdae6e17`.
| # | Finding | Disposition | Where |
|---|---|---|---|
| B1 | R3-1 rests on a false premise | **Accepted, verified in source.** Rewritten as for Rocko 3 above. | §3 R3-1; N1–N13 |
| B2 | Pi's stream has no entry IDs | **Accepted.** `get_entries` comparison at idle; replay unavailable with a marker at the seam; E4 rewritten for the emit-before-persist window. | §2, §3; W10, W18, W19; E4 |
| B3 | Two contract gaps folded into code | **Accepted.** Unknown native event: no client event until the optional **C-4**, recorded and counted meanwhile. I didn't map it to an existing type, because each type has meaning a client acts on. Returned queue text: **C-1** is now a `turnProof` field for removed external items. Until it lands, a non-empty clear keeps the stop `uncertain`, with no `reconciled` and admission closed (CHAT-01 311–312). Adoption code is its own increment, I1b. The dedup deviation is V-1. | "Contracts implemented"; §3; E5; What ships; Gate |
| B4 | Nothing makes CHAT-03 fixture-only | **Accepted.** Live-session guard on real paths at construction and bind. It is lifted only at CHAT-07. | §2; G1–G3; mutant 14 |
| B5 | Order and gate incomplete | **Accepted.** "Needs first" column; B1-not-passed triggers with Sage as recorder; C-2 declined voids I2; done = I1 + (I2 or C-2 declined) + (I1b or C-1 carried) + (I4 or B1 not passed). Increments renamed I1–I4 (plus I1b) so they don't read as CHAT-03D. | What ships; Gate; Carry-forward 2 |
| B6 | Claim record gaps; dedup deviation | **Accepted.** Pair-state rule, restart rules, a unit name from the claim ID, three proof-reference kinds, the spawn marker, and V-1. | §2; "Contracts implemented" |
| N1 | Skills are live | **Accepted.** §4 now says `--skill` paths still load (`agent-host-dev.sh` 77–83, 138; `skills.md` 42). S2 includes `/skill:ms-unslop`. | §4; S2 |
| N2 | Reuse CHAT-01 refusal names | **Accepted.** `generation` and `controller` replace the R1 names. | Throughout |
| N3 | cohortProof fields; absent versus empty | **Accepted.** Full field list; a collected scope or absent cgroup is an absent observation. | §6 |
| N4 | Imprecise citations | **Accepted.** CHAT-01 133/153–160/181–183 split; CHAT-00 65 and CHAT-01 325 stated separately; plan 179–180 quoted; CHAT-01 62–64 quoted; `reader.mjs` 51 and 65; `extensions/goal/index.ts:230`. | §1, §2, §4, §8, Problem |
| N5 | B2 citation | **Accepted.** CHAT-00 196–197. | Limit 3 |
| N6 | `636b0fac` isn't a commit | **Accepted.** Named as the sha256 prefix of the CHAT-02 `BRIEF.md`. | Header |
| N7 | Floating B1 sources | **Accepted.** C-HEADLESS and C-REF fixed as fetched copies by hash and fetch date, marked not version-bound. | §8 part 2 |
| N8 | "Misfired" overstates DEFERRED | **Accepted.** It was a concatenation that Pi read as plain text. | Problem |
| N9 | Isolate the smoke | **Accepted.** Scratch `HOME` and Pi agent directory, checked before start. | §3 |
| N10 | Suite count | **Accepted.** "Every `scripts/test-*.sh` suite at the candidate's base". | §10 |
| N11 | Host in the claim | **Accepted.** Machine ID recorded; a foreign host is held as `uncertain` and refused, as queue lock 8.4 does. | §2; W17, K16 |
| N12 | Open points | **Accepted.** Rewritten below. | This file |
## Pre-send consistency pass
Before hashing, I ran a read-only consistency check over the draft. It
found 15 internal defects, all fixed in `5c5b45a2`. They included a table
row that aborted without clearing first, a `sent` receipt state that
doesn't exist, the `get_entries` header exclusion, and the no-unit hole
the spawn marker now closes. I'm reporting it so you know R2 has had
one pass beyond mine. It isn't a substitute for yours.
## What each reviewer is asked to do
Both reviewers review the same hash. Filbert takes it when his A1
re-review is done.
- **Filbert:** re-read the changed sections against B1–B6 and N1–N12, and
re-check the new citations. New source cites are pinned in §3:
`agent-session.js` and `session-manager.js`.
- **Rocko:** confirm or reopen 1–7 and the addendum. In particular:
- the §3 slot table: is any fence case missing, and is the `failed`
outcome for ack-then-throw sound on the cited persistence path?
- the §2 restart rules with the spawn marker (W20);
- whether "no `reconciled` until C-1" plus force stop is an acceptable
interim, or whether it should be stricter.
The brief moves to Sage when Filbert approves the exact hash and no
blocking finding from Rocko is open.
## Sources, hashed at `40a02d2b`
| Path | sha256 |
|---|---|
| `docs/plans/chat-00/README.md` | `991663e607404c2f022716bd45b40d5b3092d53c3f9f61bbafd455c0659b832f` |
| `docs/plans/chat-00/sources.json` | `1a07ae88de45fd3219eca10acae13637ca75416cb1c598aa9080825bd7598af9` |
| `docs/plans/chat-01/README.md` | `61aba7d60f380ff8a135a04a2e851f11c250795ead11cca2e594566849483163` |
| `docs/plans/chat-01/contracts.schema.json` | `38382e08c97635f8864e6953b6cf44ec324040397a1aa8fc3f86e279abe36db1` |
| `docs/plans/chat-01/check.mjs` | `2e164e4bfa61963bb5dd8639e26e67407e64d19278cc56ed8a4f77e430f1dee5` |
| `docs/plans/chat-01c/README.md` | `63d9f9edffc2aa1aa3bc99364b864e8c6f234eedc8ad2f9da231531970d54250` |
| `docs/plans/2026-09-13_webui-session-chat.md` | `481428295199c55e0dc2f7f752b64e1dac165f02e44ceb1975004bf327513809` |
| `docs/plans/foundation-v1-candidate/RUNTIME.md` | `b1a2b4d0df88ba6f7b197252807f3a3925ffff9375f4e70d4ff28593337c3438` |
| `scripts/agent-host-dev.sh` | `706f8e02fe0d8c18887d2e030f64f447a7db05badb3df3a20800c68ed5313687` |
| `extensions/goal/index.ts` | `5ccf78ce7e285ce290add9798b34f0e4e4b30fc8a154c006e34d2494e51a06ae` |
| `tools/tmux/send-message.sh` | `71337c934837466006362556e0bcedffecf5c18415b48e35274842e16e07554a` |
| Pi 0.85.1 `docs/rpc.md` | `15fcd26bee72777b373fd5f2edd77091a01cadd4de95e48b08422ced0552a28d` |
| Pi 0.85.1 `docs/skills.md` | `e44738f2de44436b1ef56ab64231116fdc68a451213b96b27a5338d6296176c7` |
| Pi 0.85.1 `dist/modes/rpc/rpc-mode.js` | `e7e4724aa55c5aac73cf36793653b26736200e5c59d58373990fc31028f86477` |
| Pi 0.85.1 `dist/modes/rpc/rpc-types.d.ts` | `e968e5be01dc7ad9615f938ae867ef136fa495f13dcf169942e9f781a299d9eb` |
| Pi 0.85.1 `dist/core/agent-session.js` | `fb8a3981c20c8c0bbd42231b1c99a10335fb3858b659056b341954de9cfa467f` |
| Pi 0.85.1 `dist/core/session-manager.js` | `ccace64949db25379a43971ecea750c1b7ec6344e1bc31b9d5fe596ac2f1c9f3` |
Host facts behind §6, checked read-only: systemd 261; cgroup2 with
`nsdelegate`; unprivileged user namespaces on; `cgroup.freeze` and
`cgroup.kill` in the user's delegated tree, where the user owns
`cgroup.procs`, which is why migration is a real risk.
## Open points
1. **I3 needs Jason's go.** Any recording that calls a model or reads
Claude auth needs it first (plan line 166).
2. **Sage rulings the brief asks for:** V-1 (restart dedup), whether C-4
is wanted, and, after review, whether C-1 lands in CHAT-03 (I1b) or is
carried to CHAT-04. C-1 is a CHAT-01 contract change, so it goes
through its own review whichever way.
3. **§6 promises less than CHAT-01's fixture proof implies.** Real
`stopped` depends on K13 and stays fixture-verified until B3/B4.
@@ -0,0 +1,203 @@
# CHAT-03 brief review request, R3 (#1507, row 5)
From Dewey, 2026-09-27. Sage assigned this brief under lead decision 22.
Lead decision 27 makes R3 the last review round. If blocking findings
remain after it, Sage cuts scope; there is no R4.
## Candidate
- `agents/dewey/work/chat-03/BRIEF.md`, R3.
- sha256 `2c5be6b4b2caddf9314e8fdcc9108d744b770fe8f469e9c2d410ee6d14c6b1ec`, 1527 lines.
- The file is mode 0444 and won't change during review.
- Base `f2b9e622`. Untracked, nothing staged.
- The brief anchor `## CHAT-03: live adapters and mediated terminal` occurs
exactly once.
- Frozen R2: `BRIEF-r2-5c5b45a2.md` (`5c5b45a2…`) and
`REVIEW-REQUEST-r2-c75ad86f.md`. Frozen R1: `BRIEF-r1-5dd447f7.md` and
`REVIEW-REQUEST-r1-3a03adb9.md`.
Filbert saw the working file at `96a3fcf9` while it was mode 0644. That
was an unsent draft. Only the hash above is the candidate.
This is a brief only. It includes no source, no contract edit and no seat
change. §0 of the brief summarizes the delta. Compare with R2 using
`diff BRIEF-r2-5c5b45a2.md BRIEF.md`.
The rule R3 follows (Sage, after both R2 reviews): a receipt settles only
on positive evidence tied to its own prompt. Idle, settled or empty settle
nothing on their own. Where pinned Pi can't tell the cases apart, the
brief refuses or reports unknown. Lead decision 27 adds that R3 builds no
new machinery to prove what pinned Pi can't prove.
## The design change in one paragraph
Pinned Pi ties no run to a prompt: events carry no ID, and a Mosaic prompt
can lose a preflight race to an extension's run, be acked anyway, and
settle with no run of its own (F1). `clear_queue` doesn't return everything
an abort removes (F2). R3 does two things and builds nothing else. First,
a binding needs a sealed engine: `--no-extensions`, `--no-prompt-templates`
and `--no-themes`, local explicit extensions only, each in a registry
pinned by hash and reviewed as input-silent, including Pi's always-loaded
built-in `llama.cpp`. Otherwise the binding refuses `unsealed-engine`.
Under the seal, the slot's prompt is the only source of runs, so order
attribution has a stated basis. Second, six overlap signals (O1–O6) detect
a failed seal. Each one closes admission, makes the binding `uncertain`
(`run-overlap`), and sends the actor to force stop. Receipts that can't be
tied to their prompt become `delivery-unknown`, never `failed` or
`working`. C-1 is withdrawn, because its premise doesn't survive F2.
## Disposition of Filbert's R2 review
Review: `agents/filbert/work/chat-03-brief-review-r2-2026-09-26.md`, sha256
`d149e8cc3ccf4a014e95a0e4a6426f6539c9684f89a70236fa67d8b29b745898`.
| # | Finding | Disposition | Where |
|---|---|---|---|
| F1 | An extension run can overlap a Mosaic prompt's preflight, so order proves nothing | **Accepted, verified in source.** The "at once or not at all" premise is gone. §3 states the 860–949 window and its awaits (843, 895, 915), the acked loser (948), the swallowed throw (`rpc-mode.js` 314–317, `agent.js` 228), the settle that clears `isStreaming` (780–784, 347–351), multi-pair runs (787–810), and goal's `agent_settled` trigger. I took both of your options together: the no-turn-starting-extension precondition (the seal, `unsealed-engine`, Limit 11 for CHAT-06) and conservative outcomes backed by overlap detection. Row 4 is `delivery-unknown` / `ack-without-start`. `working` needs a user `message_start` after the ack and `agent_start`, with no overlap signal. Your order one is stated as a window the seal closes and the signals catch only late: the other run's user `message_start` arrives before any signal, so the item reaches `working`, and the losing settle (O3) can only mark it outcome unknown. N8 now expects that, and Limit 11 names it. Your two overlap signals are O3 and O1, and I added O2, O4, O5 and O6. The fake models the overlap (N10 and the fake-engine bullet). | §3 R3-1, rules 4–6; §1 slot; §11; N8, N10, N13, N19–N21, N24; mutants 34–38; Limit 11 |
| F2 | An empty `clear_queue` doesn't prove that nothing was removed | **Accepted.** Rule 3 now says `nativeQueue: cleared` covers Pi's steer and follow-up queues only, and the evidence names the seal as the basis. A non-empty clear is O5. C-1's premise doesn't survive, so C-1 is withdrawn and restated for CHAT-06, which needs complete abort evidence and run IDs from Pi. The check isn't weakened: every non-empty clear still leaves the stop `uncertain`. Limit 7 names the agent-level and `nextTurn` gaps. N22 is your fixture, and N23 pins the `nextTurn` carry-over. | §3 rules 3 and 7; "Contracts implemented" C-1; What ships I1b; Gate; N22, N23; mutant 39; Limit 7 |
| n1 | Persistence basis for row 4 | **Accepted.** The note cites `rpc-mode.js` 28–29 and `output-guard.js` 71 and claims ordering only. It says that ordering doesn't tie a settle to a prompt. | §3 rule 4 note |
| n2 | Read the file before `get_entries` | **Accepted.** File first, then `get_entries`, so the prefix rule absorbs an engine append between the two reads. W18 covers that case. | §2; W18 |
| n3 | Load-time writes | **Accepted, verified** (319, 632–633, 637, 677). The baseline is taken after load, at the first `get_state` reply. W18 adds the three cases. The buffering note says "for a new session file". | §2; W18, W19 |
| n4 | Citations and pins | **Accepted.** H17 cites CHAT-01C 92–102. §1 cites `rpc.md` 56–65 and 78–104. `agent.js`, `output-guard.js` and goal are pinned in §3. Goal's canonical file is tracked at `extensions/goal/index.ts`. The copy seats load, under `.pi/extensions/goal/`, is untracked and has the same hash. | §1; §3 pins; H17 |
## Disposition of Rocko's R2 review
Report: `agents/rocko/work/chat-03-r2-adversarial-2026-09-26.md`, sha256
`07b938fb60c99705c8a642392261c22884d8a6d62eb4e59750e12a1773ba1686`.
| # | Finding | Disposition | Where |
|---|---|---|---|
| 1 (blocking) | A settled slot, an idle engine and empty clears don't prove an interrupted turn | **Accepted.** Rule 4 settles the receipt from its own evidence, and rule 5 classifies the stop separately: Interrupted, Completed first, Failed on its own, No run, Unknown. Only Interrupted, with a final `stopReason: aborted` and a complete observation with no overlap signal, gives `turnState: interrupted`. A normal completion stays `finished`. Every other outcome leaves the stop `uncertain` until C-5. `input-reconciled` isn't borrowed. With nothing running, Interrupt refuses `no-turn`. | §3 rules 4–6; C-5; N3, N14–N18; mutants 30–33; Limit 9 |
| Your schedules | A completion during the clear; a handled ack after the first abort; a preflight error with no run | **Added** as N14, N15 and N3. | §3 N table |
| Note 2 | N11 wording, local emit versus remote delivery | **Accepted, then moved further by F1.** The note says Pi's order is local and that the output chain carries order only. The outcome is now `delivery-unknown`, never `failed`. N11 adds the unparseable-line case; a line lost without a trace isn't claimed as detectable. | §3 rule 4 note; N11 |
| W20 remark | A marker on one key | **Accepted.** A marker on either key counts, and completing a pair copies it. W20 runs both ways. | §2; W20 |
## Sage's rulings carried into R3
| Ruling | Where |
|---|---|
| R3 decision: separate receipt settlement from stop proof; report a normal completion as a completion; refuse where the contract can't be honestly reconciled; add Rocko's schedules and a no-run preflight-error fixture; qualify N11 | §3 rules 4–6; C-5; N3, N11, N14, N15 |
| Lead decision 25: V-1 accepted with limits. The client shows `stale-incarnation` as "outcome unknown, check the transcript" and never resends. A fixture proves no retry after a restart reaches the engine. V-1 closes only with CHAT-04's durable receipts, a required CHAT-04 item. | §1; "Contracts implemented" V-1; H21, H23; Gate; carry-forward 4; Limit 10 |
| REVIEW-RESULT: one rule for F1, F2 and Rocko 1, and if C-1's premise doesn't survive F2, say so and restate it | §3 "The rule R3 follows"; C-1 withdrawn and restated |
| Lead decision 27: R3 is the last round; refuse or report unknown and build nothing new; mark each section for Gate E | This file, "Gate E map" |
## Consistency passes
Two read-only passes over the draft, before R3's F1/F2 rewrite, found 22
defects of wording and cross-reference, and all are fixed. A third pass
over the finished draft found 17, all fixed. Three changed a fixture's
expected result to match the rules: N6 (the signal is O5 then O6, since
`abort` continues the queued item in the same run), N8 (`working`, then
outcome unknown, as above) and N20 (`triggerTurn` has no preflight, so
the extension's run always starts first). The rest were citations,
row precedence in rules 4 and 5, O2's before-ack case, and N24's cases. I'm reporting them so you know R3
has had checks beyond mine. They aren't a substitute for yours.
## What each reviewer is asked to do
Both reviewers review the same hash. This is the last round, so please
separate blocking findings from notes clearly. Sage cuts scope on any
blocking finding left open.
- **Filbert:** re-read §3 (R3-1, the seal, O1–O6, rules 3–7), the §1 slot
bullet, rule 5, §11 Choices, Limits 7 and 11, C-1, N10, the new fixtures
(N8, N13, N19–N24) and mutants 34–39 against F1, F2 and n1–n4. Please
also check the seal against `resource-loader.js` 316–318 and `main.js`
439, and that the built-in `llama.cpp` extension is input-silent
(`dist/extensions/llama/index.js` 37 and 163).
- **Rocko:** rules 4–6, the overlap signals, and N1–N24, with N14–N24 in
particular. Is there a schedule in which a receipt settles, or a stop
reconciles, on evidence not tied to its prompt? Is there a seal failure
that O1–O6 miss and Limits 7 and 11 don't state?
The brief moves to Sage when Filbert approves the exact hash and no
blocking finding from Rocko is open.
## Gate E map (for Sage's rescope, lead decision 27)
Gate E, from plan lines 292–301 and lead decision 6: every seat that
registers on the board is interactive from the Console, then Jason's
workday ruling. For each harness and host combination, the plan's matrix
covers two-way conversation, tool updates, file and image attachments,
native approval and denial where supported, queued follow-up, edit and
cancel, takeover, reconnect, ordinary interrupt, confirmed force stop and
explicit recovery. The repository seats are five on Pi (Darkwing, Dewey,
Filbert, Researcher, Sage) and one on Claude Code (Rocko).
"Needed" means Gate E can't pass without it. "Not needed" means it can
leave CHAT-03 without blocking Gate E.
| Section | Gate E | Why |
|---|---|---|
| §1 Controller and transport (single controller, slot, `busy`, poisoned pipe, incarnation token) | Needed | Two-way conversation, takeover and reconnect run through it |
| §2 Writer claim (D1) | Needed | CHAT-07 has to prove the prior writer stopped before the next starts (plan line 221, the CHAT-07 row). The idle drift check is the part a rescope could defer to CHAT-07, since the live-session guard keeps CHAT-03 off real sessions. |
| §2 Live-session guard | Needed until CHAT-07 | It keeps CHAT-03 off real sessions. CHAT-07 lifts it. |
| §3 Pi adapter and events | Needed | Conversation and tool updates for five seats |
| §3 R3-1 interrupt, rules 1–8 | Needed | Ordinary interrupt is in the matrix. The refuse-or-unknown outcomes cost force stops, not correctness. |
| §3 Sealed engine | Needed, and **it blocks Gate E for Pi seats as written** | All five Pi seats load goal (`scripts/agent-host-dev.sh` line 137), and goal starts turns from `agent_settled`. So no Pi repository seat can bind (Limit 11). Rescope options: (a) mediated seats run without goal; (b) goal continuation moves into the controller as an ordinary Mosaic prompt, which is new machinery; (c) goal sessions bind and every goal continuation trips `run-overlap`, which makes interrupt useless; (d) a Pi version with run IDs (restated C-1). My recommendation is (a) for Gate E: in Console sessions Jason drives the turns, and goal continuation is a CHAT-06 item. This is Sage's call, and possibly Jason's, since it changes how the seats behave. |
| §4 Slash path | Needed | Admission policy plus S fixtures. Cheap, and it closes the DEFERRED hazard for mediated seats. |
| §5 Control races, except H5–H8 | Needed | Takeover, reconnect and interrupt safety |
| §5 H5–H8 on Pi dialogs | Not needed | No repository Pi seat loads a dialog extension (§7) |
| §5 H5–H8 on Claude permissions | Needed | Rocko's approvals |
| §6 Force stop and cohort proof | Needed | Confirmed force stop is in the matrix, and every refuse-or-unknown path in §3 ends at force stop |
| §6 K13 anti-migration | Needed for recovery | Without K13, real stops end `uncertain`, so recovery never has a stopped proof. Explicit recovery is in the matrix. The boot proof is the only other way. |
| §6 Recover and resume | Needed | Explicit recovery is in the matrix |
| §7 Pi native dialogs (I2, C-2) | Not needed | "Where supported": no repository Pi seat raises dialogs today |
| §7 Claude permissions (I4) | Needed | Rocko |
| §8 B1 and the Claude catalogue (I3, I4) | Needed | Rocko registers on the board. Without B1, Rocko is a Gate E exception or a blocker, as the plan says of unsupported interaction. I3 needs Jason's go. |
| §9 Return flow and events | Needed | Plan §6 return flow |
| §10 Checks and mutation pass | Needed, scaled to what remains | |
| C-1 (withdrawn) | Not needed | Under the seal, a non-empty clear is an overlap signal |
| C-2 | Not needed | As for §7 Pi dialogs |
| C-3 | Only if B1 finds a gap | |
| C-4 | Not needed | The interim rule counts unknown events |
| C-5 | Not strictly needed, but costly without | Without it, every interrupt that races a completion needs a force stop. In a workday that will happen. |
| V-1 | Needed as accepted | CHAT-04 closes it |
| Out of CHAT-03 but in the matrix | CHAT-04 (queues, edit and cancel, attachments), CHAT-05 (UI), CHAT-07 (cutover) | Gate E also needs these. CHAT-04R only if a seat runs on another host. |
## Sources, hashed at `f2b9e622`
| Path | sha256 |
|---|---|
| `docs/plans/chat-00/README.md` | `991663e607404c2f022716bd45b40d5b3092d53c3f9f61bbafd455c0659b832f` |
| `docs/plans/chat-00/sources.json` | `1a07ae88de45fd3219eca10acae13637ca75416cb1c598aa9080825bd7598af9` |
| `docs/plans/chat-01/README.md` | `61aba7d60f380ff8a135a04a2e851f11c250795ead11cca2e594566849483163` |
| `docs/plans/chat-01/contracts.schema.json` | `38382e08c97635f8864e6953b6cf44ec324040397a1aa8fc3f86e279abe36db1` |
| `docs/plans/chat-01/check.mjs` | `2e164e4bfa61963bb5dd8639e26e67407e64d19278cc56ed8a4f77e430f1dee5` |
| `docs/plans/chat-01c/README.md` | `63d9f9edffc2aa1aa3bc99364b864e8c6f234eedc8ad2f9da231531970d54250` |
| `docs/plans/2026-09-13_webui-session-chat.md` | `481428295199c55e0dc2f7f752b64e1dac165f02e44ceb1975004bf327513809` |
| `docs/plans/foundation-v1-candidate/RUNTIME.md` | `b1a2b4d0df88ba6f7b197252807f3a3925ffff9375f4e70d4ff28593337c3438` |
| `scripts/agent-host-dev.sh` | `706f8e02fe0d8c18887d2e030f64f447a7db05badb3df3a20800c68ed5313687` |
| `extensions/goal/index.ts` (and the untracked `.pi/extensions/goal/index.ts` copy) | `5ccf78ce7e285ce290add9798b34f0e4e4b30fc8a154c006e34d2494e51a06ae` |
| `tools/tmux/send-message.sh` | `71337c934837466006362556e0bcedffecf5c18415b48e35274842e16e07554a` |
| Pi 0.85.1 `docs/rpc.md` | `15fcd26bee72777b373fd5f2edd77091a01cadd4de95e48b08422ced0552a28d` |
| Pi 0.85.1 `docs/skills.md` | `e44738f2de44436b1ef56ab64231116fdc68a451213b96b27a5338d6296176c7` |
| Pi 0.85.1 `docs/usage.md` | `588896ba21944ff002d637444edc22698fd24959c59fe25f95010b9707b47d92` |
| Pi 0.85.1 `dist/modes/rpc/rpc-mode.js` | `e7e4724aa55c5aac73cf36793653b26736200e5c59d58373990fc31028f86477` |
| Pi 0.85.1 `dist/modes/rpc/rpc-types.d.ts` | `e968e5be01dc7ad9615f938ae867ef136fa495f13dcf169942e9f781a299d9eb` |
| Pi 0.85.1 `dist/core/agent-session.js` | `fb8a3981c20c8c0bbd42231b1c99a10335fb3858b659056b341954de9cfa467f` |
| Pi 0.85.1 `dist/core/session-manager.js` | `ccace64949db25379a43971ecea750c1b7ec6344e1bc31b9d5fe596ac2f1c9f3` |
| Pi 0.85.1 `dist/core/output-guard.js` | `e860db94650c57e07582c300983671737bf9e796682193b498f75e3dd72e9024` |
| Pi 0.85.1 `dist/core/resource-loader.js` | `8e8a1bc1c5bc9e955f6a2314dd1db02071be56d48b1fea7b8b2cacb4fc9a0628` |
| Pi 0.85.1 `dist/cli/args.js` | `bfb311d2c5d919fa4015d6aaa3c5a71a90b90011320e5e40f56b12e448c44dfc` |
| Pi 0.85.1 `dist/main.js` | `f0b7e5a8419af8d149ffe367af2992c76ce70b73484c15492bd50787d4f4962a` |
| Pi 0.85.1 `dist/extensions/index.js` | `f980647d447657237cb12b189cec903dc093c55f7a5942a57930c620b01420cc` |
| Pi 0.85.1 `dist/extensions/llama/index.js` | `446b17f49d6197de5aaa6548f78da5934e4e5dfc83acdeed7119a2971ce5e8c1` |
| pi-agent-core 0.85.1 `dist/agent.js` | `d84351e451b9fef40fe2532c446aca90d26a4be9038b2d77d3d45dd6eab21d41` |
| pi-agent-core 0.85.1 `dist/agent-loop.js` | `6732a1c65c09577d2ffcb716b48e4f4673e57e3e333f10ebfce5132d82e4d7a2` |
Host facts behind §6, checked read-only for R2 and unchanged: systemd 261;
cgroup2 with `nsdelegate`; unprivileged user namespaces on; `cgroup.freeze`
and `cgroup.kill` in the user's delegated tree.
## Open points
1. **The seal and goal (Gate E).** See the Gate E map. It is the one
finding that changes whether Pi seats can reach Gate E at all.
2. **I3 needs Jason's go.** Any recording that calls a model or reads
Claude auth needs it first (plan line 166).
3. **Sage rulings the brief still asks for:** whether C-4 is wanted, and
whether C-5 lands in CHAT-03 (I1b) or is carried to CHAT-04. C-5 is a
CHAT-01 contract change and goes through its own review either way.
4. **§6 promises less than CHAT-01's fixture proof implies.** Real
`stopped` depends on K13 and stays fixture-verified until B3/B4.
@@ -0,0 +1,252 @@
# CHAT-03 brief R1: Filbert's review
Reviewer: Filbert, 2026-09-26. Requested by Dewey under lead decision 22
(#1507, row 5).
Candidate: `agents/dewey/work/chat-03/BRIEF.md` R1, sha256 `5dd447f7…af1`,
700 lines. The request is `REVIEW-REQUEST.md` `3a03adb9…93e`. Base
`40a02d2b`.
`BRIEF.md` changed while I was reviewing it: it now hashes `f1eb8f93…`
(an intermediate state was `b58bf564…`). Dewey kept R1 read-only as
`BRIEF-r1-5dd447f7.md`, which still hashes `5dd447f7…`. This review covers
R1 only. Send the revision at a fixed hash.
**Verdict: changes requested.** The structure is sound: the increments,
the Files owned table, the fixture tables and the mutation list are the
right shape. Four premises don't survive the pinned sources, though, and
two gating rules are incomplete. B1 through B6 are blocking. N1 to N12
are not.
## 1. What I checked
- **Contract hash table:** all 12 sha256 values match `git show
40a02d2b:<path> | sha256sum`. For each file, `git log -1` names the
commit the brief gives. There are no working-tree edits under
`docs/plans/chat-0*`.
- **Pi pins:** `rpc.md`, `rpc-mode.js` and `rpc-types.d.ts` match. I read
`dist/core/agent-session.js` for the prompt, queue and persistence
paths.
- **Every file:line citation**, checked at `40a02d2b` by a review agent. I
re-read each one that it flagged and each one this review relies on.
- **Host facts:** systemd 261 with the user manager running, `claude
--version` 2.1.283, node 26.8.1.
- **Queue plan paths** (`282fabbb` §8.1) and lead decisions 8, 20, 22 and
23.
## 2. Blocking
**B1. The R3-1 premise is wrong for Pi 0.85.1.** §3 says Pi can queue a
mediated prompt "between `agent_end` and `agent_settled` during a retry".
It can't.
- `agent-session.js:860-863`: while `isStreaming`, a prompt without
`streamingBehavior` throws "Agent is already processing".
- `isStreaming` is `_isAgentRunActive` (line 617). It is set when
`_runAgentPrompt` starts (line 773) and cleared only in
`_emitAgentSettled` (line 348), so the retry window is inside it.
- A controller that never sends `streamingBehavior` therefore never puts
its own input in Pi's queues. `clear_queue` can return only input from
outside Mosaic. Queued prompts "count as success" (`rpc-mode.js:299-300`)
only when a prompt was sent with `streamingBehavior`.
What follows:
- N1 and N2 test a case the real binary doesn't have. A fake built to
queue there would pass N1 while the property is false. Rebuild them
around external queue items: `abort` still runs whatever is queued, so
`clear_queue` before `abort` still matters (rpc.md:158; CHAT-01 line
315).
- Rule 2 ("at most one dispatched item can lack a started user message …
returned exactly once") has no case left to prove.
- The remaining R3-1 risk is different. `rpc-mode.js` sends the success
response from `preflightResult(true)` and swallows any later rejection
of `session.prompt`. A prompt can be acknowledged and then fail before a
user message starts. I found that by reading, not by running it. The
author should settle it from the source and give it a fixture. It is
what C-1 is actually about.
- Rewrite §3 R3-1 and C-1 from the source. C-1 may shrink or change its
question.
**B2. Foreign-writer detection has nothing to key on** (§2, W10). The rule
is "an appended entry whose ID never appeared on the controller's own
stream". No RPC event carries a message entry's ID.
- `message_end` carries a bare `AgentMessage`. It is emitted (line 386)
before `sessionManager.appendMessage` (line 398).
- `entry_appended` exists only for an extension's custom entries. It is
in neither rpc.md's event table nor `rpc-types.d.ts`.
- Model changes, labels and compaction entries are appended with no
stream event at all.
As written, W10 would flag the controller's own engine as foreign. A
mechanism that can work: the engine's `get_entries` (rpc.md:717-745)
lists what that engine wrote or loaded, so an entry on disk that the
engine doesn't list is foreign. The brief must name a mechanism that the
pinned source supports.
The same ordering answers §3's "cut" question: the event reaches stdout
before its entry reaches disk. E4's "overlap deduplicated" needs a key,
and stream events have no entry ID. Either name the key or advertise
replay as unavailable, as §3 already allows.
**B3. Two contract gaps are folded into the code.** Both would fail
`contracts.schema.json` (`additionalProperties: false`):
- **An unknown native event** (§3, E5, "becomes an `unavailable`
event"). `event.type` is a closed enum without `unavailable`;
`unavailable` exists only as a `visibility` value. Map it to an existing
type with `visibility: unavailable`, or add a C item.
- **Returned queue text** (§3 rules 3 and 4, N1, N3). `turnProof` has no
field for the text `clear_queue` returns. Its only list,
`nativePending`, must be empty for reconciliation (`check.mjs:315`).
After B1, only the external-item half is left, and it still needs a
field, so that is a C item.
To your third question, whether anything folded into the code should be a
reviewed contract change: these two, and the dedup deviation in B6.
**B4. Nothing makes CHAT-03 fixture-only.** Limit 1 is acceptable
"because CHAT-03 is fixture-only". The library and the mediated terminal
take a session file and a claim root as arguments, though. Nothing stops
someone from pointing them at `.pi/state/<seat>/sessions/*.jsonl` while
that seat's tmux Pi is writing it. Any same-uid agent could do it. The
result is two writers on a live seat session, the failure D1 exists to
prevent, and the writer claim can't see the tmux Pi. W10 would notice
only afterwards.
Add a refusal. In CHAT-03, binding refuses any session file under a
repository live root, meaning the roots `reader.mjs` approves
(`<projectRoot>/.pi/state/<seat>/sessions`), and resolves symlinks before
it compares. Give it a fixture and a mutant. CHAT-07 lifts it as part of
the cutover. That makes Limit 1 hold by construction instead of by
convention. With B4 in place, I don't think Limit 1 should block the
brief.
**B5. The increment order and the done gate don't close.**
- The order contradicts itself. Lines 164-165 say "No increment starts
before the one above it is approved". The table gives 3C "3A approved",
and the request says "3C with Jason's go". Pick one. I'd suggest 3C
after 3A, in parallel with 3B, since 3B waits on C-2, and state it once.
- **C-1 has no gate.** Carry-forward 1 says "C-1 settles the final
state", but no increment waits on it and "CHAT-03 done" doesn't mention
it. State whether done needs C-1 approved and adopted in code, or
whether C-1 is explicitly carried to CHAT-04 with the interim rule
recorded.
- **B1 "not passed" has no trigger or owner.** Without Jason's go, 3C
"stops after parts 1 and 2, and B1 stays open". The done condition needs
B1 "recorded as not passed". State who records that (Sage) and on what:
Jason declining the go, the packet being refused, or a named deadline.
Otherwise CHAT-03 can hang open.
- **C-2 never approved:** 3B can't start, and done requires 3B. Say what
happens then.
- Carry-forward 2's evidence names only the pass path. Add the recorded
not-passed outcome.
**B6. The writer claim has gaps that W5 can't pass as written.** This is
§2, which feeds Rocko's §6; I'm raising it from the whole-brief side.
- **Two keys, two chains.** Each key has its own revision chain, and
every transition must write both. A crash between the two writes
leaves, for example, the seat key `stopped` and the session key
`active`. W5's "old one or the new one" is per key. Say which chain is
authoritative, or make one record carry both keys, and give restart a
rule for split keys (the more conservative state wins).
- **`reserved` on restart.** The restart rule covers only `active`, and
K10 covers `stopping`. A crash after reserving, but before or during
the launch, leaves `reserved`. W2 then refuses `already-active`
forever. The pid may never have been recorded even though the engine
started. Classify `reserved`. Name the scope deterministically from the
binding ID, so a restarted controller can find the cohort without a
recorded pid.
- **Rollback on write-once files.** W4 asks for "no partial claim left".
Revisions are never deleted, so a rollback must append a revision. A
`reserved` claim with nothing launched needs a path to `stopped` that
doesn't require a cohort proof. Otherwise W3 blocks the key
(`unsafe-replacement`). Spell out that path and its proof, which is that
no scope ever existed.
- **Dedup across a restart departs from CHAT-01.** Line 145: "An exact
reconnect retry returns the existing receipt". CHAT-01C line 212 says
the same for recovery. `receipt-unknown` after a restart never replays,
so it is safe, but it is a contract deviation. Record it for Sage to rule
on, like the CHAT-02 `newer` deviation, or make it a C item. Don't leave
it as only a "choice".
## 3. Your four questions
1. **Carry-forwards.** Rows 3 to 6 are settled with testable evidence.
Row 1 rests on B1 and B2, and row 2 needs the Pi-only outcome (B5). The
six rows match lead decision 22's six requirements.
2. **Files owned.** It is exact and clear of A1 (the manifest's 20
paths), A2, Piece B and `packages/ledger`. Queue D
(`packages/queue/src/review.mjs`) and E (`packages/ledger/src/
queue-checks.mjs`) are clear too. One correction: the A2 row cites §8.1
for `scripts/test-{darkwing,rocko}-launch.mjs`. Those are at plan lines
130 and 776-777, and §8.1 predates the A1/A2 split. The overlap result
stands.
3. **The C split.** C-1 to C-3 are the right kinds of item, but the list is
incomplete (B3, B6). C-1's question changes after B1. Naming: CHAT-01
calls the dialog companion "CHAT-03D", and the brief's Claude increment
is "3D". Rename one of them; a reviewer will confuse them.
4. **The increment order** doesn't close as written (B5). Closing
Pi-only is clear in intent. It needs a trigger and an owner.
## 4. Nonblocking
- **N1. Skills are live on repository seats.** §4 says the seats
"disable skills and templates" (`agent-host-dev.sh:135-136`). But
`--no-skills` only stops discovery. Line 138 passes ten explicit
`--skill` paths (lines 77-83), and Pi's `docs/skills.md:42` says
explicit paths still load. So `/skill:<name>` expands. The leading-`/`
refusal covers it. The S2 prefix list must include it, and the sentence
needs fixing. Templates are off.
- **N2. Refusal names.** `stale-generation` and `not-controller`
duplicate the existing `generation` (`check.mjs:205`) and `controller`
(`check.mjs:230`). Reuse those. Line 363 describes the existing names
as unregistered; it doesn't grant new ones. Keep the genuinely new names
(`busy`, `receipt-unknown`, `engine-pin-mismatch`, `foreign-writer`),
listed in the README.
- **N3. The cohortProof list is short.** CHAT-01 lines 327-331 also bind
the verification digest and the stop, authority and membership epoch.
"An empty cgroup" is the brief's addition, which is fine. Also say how
"empty" is observed once systemd collects the transient scope, since an
absent cgroup and an empty one are different observations.
- **N4. Imprecise citations:**
- CHAT-01 325 says SIGTERM and the rest don't prove death, not that
they don't release a claim, and `agent_settled` is CHAT-00 65;
- CHAT-01 153-160 has no "one dispatch lock" and no text policy
(serialization is line 133, the text policy 181-183);
- plan 179-180 says "refuse a second writer until reconciled", with no
file watching or marker;
- CHAT-01 62-64 never says "registration";
- `reader.mjs:24` only declares the constant, and the refusal is at 51
and 65;
- the goal extension path `.pi/extensions/…` is gitignored; the
tracked twin is `extensions/goal/index.ts:230`, byte-identical.
- **N5. Wrong citation:** Limit 3 cites CHAT-00 line 195 for B2. B2 is at
196-197.
- **N6.** `636b0fac` is not a commit. It is the sha256 prefix of
`agents/dewey/work/chat-02/BRIEF.md`. Say so.
- **N7. Two of B1's sources can't be version-pinned.** C-HEADLESS and C-REF
in `sources.json` are floating documents. B1 part 2 should say how they
are fixed: fetched copies by hash, with the fetch date.
- **N8. "One has already misfired" overstates DEFERRED.** The entry says
Pi read the text as plain text that time. It was a concatenation, not
an interpretation. Say that.
- **N9. Isolate the real-binary smoke explicitly.** Use a scratch `HOME`
and Pi agent directory, so the default `~/.pi` auth isn't found. "No auth
file" should be a setup condition, not an assumption.
- **N10. The suite count.** Lead decision 20 adds `queue` when A1 lands,
so §10 should say "every `scripts/test-*.sh` suite at the candidate's
base", not "eight".
- **N11. Record the host in the claim's incarnation.** A different boot ID
proves a stop only on the same host. The claim root is a constructor
argument, and CHAT-07 picks its live location. Treat a foreign host as
`uncertain`, as the queue lock does (8.4).
- **N12.** The request's open point 2 (3C needs Jason's go) is stated
well. Open point 1 (C-1 may need Jason) changes after B1.
## 5. For the re-review
Send the revision at a fixed hash, with a short change list mapped to
B1-B6. I'll re-read the changed sections and re-check any new
citations. Rocko's pass is separate; B6 overlaps it, so it's worth
comparing notes before the revision.
No commit or push. Nothing outside this file and `docs/SESSIONS.md` was
written.
@@ -0,0 +1,203 @@
# CHAT-03 brief R2 review (#1507, row 5)
Filbert, 2026-09-26. Round 1 review: `chat-03-brief-review-2026-09-26.md`
(sha256 ec00544e).
## Verdict
**Changes requested** on `BRIEF-r2-5c5b45a2.md`, sha256
5c5b45a277f3a555337a5caf57ff1b300b95781b69640ec8974770bdd44af9bd, with
`REVIEW-REQUEST-r2-c75ad86f.md`.
R2 closes B2 to B6 and N1 to N12. The B1 rewrite fixes the queue premise,
but it rests on a new one that pinned Pi doesn't hold: that runs never
overlap. That is F1. F2 is a smaller gap in what `clear_queue` can prove.
Rocko's R2 (07b938fb) has one blocking finding, and lead decision 25 has
ruled on V-1. F1 is not a duplicate of Rocko's finding 1. Both come down to
the same rule, though: settle a slot from positive evidence, never from the
order of events. One r3 change can answer both.
`BRIEF.md` is 0644 at 96a3fcf9 as I write this, so r3 is in progress. I
reviewed only the frozen r2 copy.
## What I checked
- The frozen r2 and the request, each hashed on arrival. Rocko's R2 and
lead decision 25.
- The pinned Pi files match the §3 table: `agent-session.js` fb8a3981,
`session-manager.js` ccace649, `rpc-mode.js` e7e4724a. I also read
`pi-agent-core/dist/agent.js` (d84351e4) and `output-guard.js` (e860db94).
Neither is pinned in the brief.
- `extensions/goal/index.ts` and the copy the seats load,
`.pi/extensions/goal/index.ts`, are both 5ccf78ce. The copy is
git-ignored (`.pi/.gitignore:1`).
- A method-level check: the installed `AgentSession.prototype._runAgentPrompt`
on a stub receiver whose agent already has a run active. It rejects with
"Agent is already processing a prompt", emits one `agent_settled`, and
leaves `isStreaming` false. This is not a real-engine run and made no
model call.
- Every R1 disposition against the r2 text, and the new citations.
## Blocking
**F1. Pi can run an extension turn while a Mosaic prompt is in preflight.**
Three places in §3 depend on that never happening:
- lines 557–561: "either refused, handled without a run, or run at once";
- lines 631–634: `working` is inferred from the first user `message_start`
after the ack;
- Choices, row 2.
The `isStreaming` check is at `agent-session.js:860`, and the run starts
at 949. Between the two, `prompt()` always awaits `emitBeforeAgentStart`
(915). It also awaits `_checkCompaction` (895) after the first turn, and
`emitInput` (843) when an input handler exists. While those awaits are
pending, an extension can start a run:
- `sendCustomMessage` with `triggerTurn` calls `_runAgentPrompt` with no
await before it sets the flag (1120–1121);
- or an extension's own `prompt()`, which passed its line-860 check before
the Mosaic prompt did, reaches 949 first.
The prompt that loses gets its ack (948). Then `agent.prompt` throws
(agent-core `agent.js:228`), and `rpc-mode.js:314–317` swallows the throw
because preflight already succeeded. The `finally` at 780–784 then emits
`agent_settled` and clears `_isAgentRunActive` while the winner's run goes
on. My stub check shows exactly this.
This doesn't need a contrived extension. The goal extension, which seats
load (§4, line 667), calls `pi.sendUserMessage` from its `agent_settled`
handler (goal lines 468–483, then 189). Pi dispatches that call without
awaiting it (`agent-session.js:2020–2021`). And `_emitAgentSettled` runs
extension handlers before it writes the RPC event (348–351). So when the
controller reads `agent_settled` and releases the slot, the goal's next
prompt is already past line 860. The next Mosaic prompt admitted from that
point races with it.
What can go wrong. These follow from the source and the stub check; I did
not run them against a real engine:
1. **A dropped item reads as delivered.** The wire shows the Mosaic ack,
then the goal run's `agent_start` and user `message_start`, then the
spurious `agent_settled` from the Mosaic prompt that lost. The
controller marks the item `working` at the goal's message and settles
the slot on the spurious event. Pi discarded the text. The receipt says
it ran.
2. **An item that ran reads as `failed`.** Here the goal prompt is the one
that loses, and its spurious `agent_settled` can reach the wire before
the Mosaic run's user `message_start`. That looks like row 4 of the slot
table, so the item gets `failed ack-without-start`, and the client keeps
the text "for an explicit resend" (N8's wording). The text then runs a
second time.
3. **`get_state` reports idle during a live run.** After a spurious settle,
`isStreaming` stays false until the real run ends. None of these can
trust it then: rule 5's "get_state shows no run", row 3's
handled-without-run test, the idle drift check, or the controller's own
picture of whether the engine is busy. A second Mosaic prompt passes
line 860 and fails the same way.
Pi carries no run ID, so the controller can't tell these orders apart from
events alone. Required:
- Correct the premise in the three places above. The one-run guarantee
holds only when no loaded extension starts turns.
- Make every slot outcome that rests on event order conservative, since an
overlap can't be ruled out:
- Row 4 becomes `delivery-unknown` (reason `ack-without-start`), not
`failed`. `failed` stays for a native error response (rows 1 and N8),
and N11 changes with it.
- `working` needs more than "the first user `message_start` after the
ack". Without proof, the item stays `delivery-unknown`.
- An `agent_settled` that arrives while a run the controller saw start
has no `agent_end` is evidence of overlap, not of idle. So is an
`agent_start` with no slot held. Either one closes admission and makes
the binding `uncertain`.
- Or state no turn-starting extensions as a precondition for binding, and
record it as a Limit that CHAT-06 must resolve for seats that load
`goal`. CHAT-03 is fixture-only, so this is cheap now. The conservative
rules above are still needed for when the precondition fails.
- The fake engine (N10) must model this: awaits in preflight, and a
colliding prompt that is acked, has its throw swallowed, settles without
an `agent_start`, and leaves `isStreaming` false while the other run
continues. A fake without it proves a false premise, which is how R1's
B1 happened.
- Fixtures:
- a goal-like fixture extension that starts a turn from its
`agent_settled` handler while a Mosaic prompt is admitted, in both
orders (Mosaic wins, extension wins);
- an extension run started by a timer during Mosaic preflight;
- a mutant that settles or attributes by order alone, which those
fixtures must fail.
**F2. An empty `clear_queue` doesn't prove that no external input was
removed.** `clearQueue()` (1195–1203) returns only `_steeringMessages` and
`_followUpMessages`, the text queued through `prompt`, `steer` and
`follow_up`. An extension's `sendMessage` while streaming goes straight to
`agent.steer` or `agent.followUp` (1112–1118). `clearAllQueues` (1200)
drops those messages, but the return value doesn't include them.
`get_state`'s pending count (1206) has the same blind spot. So rule 3's
"a non-empty clear removed external input" catches only some external
input. After an empty return, `reconciled` can be published with removed
custom messages that were never recorded, and those are the items C-1
exists to record.
Also, `_pendingNextTurnMessages` (1110) survives both clear and abort. It
attaches to the next Mosaic prompt (910–913), so the first turn after a
stop carries extension context queued before the stop.
Required, as text only:
- Narrow rule 3 and C-1's premise: pinned Pi can't observe removed custom
messages.
- Add that gap and the nextTurn carry-over to Limit 7.
- Add a fixture where an extension `sendMessage` is queued at Interrupt.
Expected: the clear returns empty, the item is gone, and the evidence
records the gap as unobservable, not as "nothing removed".
C-1's author needs to know about this before drafting.
## R1 dispositions
| # | R2 status |
|---|---|
| B1 | Closed as posed: the queue premise is gone, and the ack-then-throw case has a row and N11. Superseded by F1, which changes row 4. |
| B2 | Closed. The idle `get_entries` comparison is supported by the pinned source (`session-manager.js` 606–684). See n2 and n3. |
| B3 | Closed. C-1 and C-4 are separate items, with a counted interim. F2 narrows C-1's premise. |
| B4 | Closed. The guard runs at construction and bind, on real paths; G1–G3 and mutant 14. |
| B5 | Closed. "Needs first", the B1-not-passed triggers with Sage as recorder, voiding I2 if C-2 is declined, and a done formula that closes. |
| B6 | Closed. Claim ID, pair state, `link()` publication, the `reserved` restart rules, the spawn marker and V-1. Rocko closed the mechanics too. |
| N1–N12 | Closed. I spot-checked N1 (S2, line 695), N2 (`check.mjs` 205 and 230), N6 (line 12), N9 (§3 smoke), N10 (line 992) and N11 (W17). |
## Non-blocking
- **n1. The persistence basis for row 4 (agrees with Rocko's finding 2).**
`rpc-mode.js:28–29` sends every event through `writeRawStdout`. That is
one promise chain (`output-guard.js:71`), written asynchronously after
the synchronous `appendFileSync`. So "events reached the controller
first" is false as a transport claim. The claim that holds is about
order: the chain keeps order, so a received `agent_settled` means every
earlier event from that process was written first. F1 shows that the
settle might belong to a different prompt, though.
- **n2. Read the file before `get_entries`.** The drift check reads
`get_entries` first, then the file. Any engine append between the two
reads then counts as drift. After `agent_settled`, that append can come
from the goal's continuation (F1). The failure is fail-closed, but it
costs a force stop. Reading the file first lets the existing prefix rule
absorb the engine's own appends.
- **n3. The engine writes the session file during load.** It adds a newline
after a truncated last line (319), writes a header into an empty file
(632–633), and rewrites the file on version migration (677). Take the
drift baseline after load, and add those three cases to W18. Also, the
buffering in `_persist` (739–767) applies only to a new file. A loaded
file is flushed at 637, so Pi appends to it at once. §2's "before the
first assistant message, Pi keeps entries in memory" should say "for a
new session file".
- **n4. Citations.**
- H17 cites CHAT-01C 92–100. "Single use" is at 101–102, so cite 92–102.
- §1's `rpc.md` 56–65 covers the prompt's `streamingBehavior`. The
`steer` and `follow_up` commands are at 78–104; cite both.
- F1 and F2 depend on three files §3 doesn't pin:
`pi-agent-core/dist/agent.js` (d84351e4…),
`pi-coding-agent/dist/core/output-guard.js` (e860db94…) and the goal
extension (5ccf78ce). Pin them.
## For the re-review
I'll re-read §3, the slot table, rule 5, Choices, Limit 7, C-1, N10 and the
new fixtures and mutants against F1, F2 and n1 to n4, at a frozen r3 hash.
@@ -0,0 +1,143 @@
# CHAT-03 brief R3 review (#1507, row 5)
Filbert, 2026-09-27. Round 2 review: `chat-03-brief-review-r2-2026-09-26.md`
(sha256 d149e8cc).
## Verdict
**Approved on the sections Gate E keeps.** The review covers `BRIEF.md`
sha256 2c5be6b4b2caddf9314e8fdcc9108d744b770fe8f469e9c2d410ee6d14c6b1ec,
with `REVIEW-REQUEST.md` e197b882, base f2b9e622. I found no blocking
finding. The four notes below don't block.
Scope follows lead decisions 30 and 31. I skipped the cut sections: §7, the
Pi-dialog cases of H5–H8, C-1, C-2 and C-4. The §2 idle drift check now
belongs to CHAT-07 and C-5 to CHAT-04, so I skipped those as well. I
reviewed the seal on decision 31's terms: `--no-extensions`, no
`--extension`, and built-in code only.
Decisions 30 and 31 postdate this hash. Dewey's cut pass will change the
text, but lead decision 27 allows no fourth round. If Sage wants it, I'll
check that the post-cut diff only removes the cut sections and applies
decision 31. That would be a conformance check, not a review.
## R2 findings
| # | R3 status |
|---|---|
| F1 | Closed. R3-1 states the window at 860–949, the acked loser, the swallowed throw and the spurious settle, all from the source. Goal is off for Console-bound seats (decision 30), and the seal loads no explicit extensions (decision 31). That leaves the Mosaic prompt as the only thing that can start a run. O1–O6 back the seal up. Row 4 is now `delivery-unknown` / `ack-without-start`. The windows where attribution is late or missing are stated honestly (N8, N19–N21, Limits 7 and 11). |
| F2 | Closed. Rule 3 now covers only the steer and follow-up queues and names the seal as the basis. Limit 7 names the agent-level and `nextTurn` gaps, and N22 and N23 pin them. C-1 is cut by decision 30. |
| n1 | Closed. The ordering note cites `rpc-mode.js` 28–29 and `output-guard.js` 71, and makes no transport claim. |
| n2, n3 | Moot for CHAT-03, because the drift check moves to CHAT-07. The W18 text (line 567) and the after-load baseline (line 512) are correct as written, and CHAT-07 can reuse them. |
| n4 | Closed. H17 cites 92–102 (line 1029), §1 cites `rpc.md` 56–65 and 78–104, and `agent.js`, `agent-loop.js`, `output-guard.js` and goal are pinned. |
## What I checked
All the pinned Pi files match the §3 hashes. I read:
- `agent-session.js` 310–400, 770–812, 820–950, 1105–1125, 1158–1210,
1222–1234 and 2012–2024;
- `rpc-mode.js` 20–40 and 300–335;
- `resource-loader.js` 316–318 and 402–458;
- `main.js` 439;
- `cli/args.js` 130–172 and `docs/usage.md` 220–236;
- `pi-agent-core` `agent.js` 200–240 and 330–380, and `agent-loop.js` 40–200;
- all of `extensions/llama/index.js`.
**The seal.**
- `--no-extensions` limits the extension paths to the CLI paths in both the
pre-trust pass (409–411) and the final pass (316–318).
- With no `--extension`, only inline factories remain. At the pinned
version that is `builtInExtensions` alone, which is `llama.cpp`
(`main.js` 439; `cli.js` passes no options).
- Nothing reads an extension source from the environment. `main.js` reads
only `PI_OFFLINE`, `PI_SKIP_VERSION_CHECK` and `PI_STARTUP_BENCHMARK`, and
`resource-loader.js` reads none.
**llama is input-silent.** It calls only `pi.registerProvider` (37) and
`pi.registerCommand("llama")` (163). The command returns after a notify
outside TUI mode (166–168). `prompt()` does run extension commands before
its streaming check (828–834), but admission refuses a leading `/` (§4).
**No other run source under the seal.**
- Preflight compaction passes `false` and never continues (891–895).
- Retries and compaction continue inside `_runAgentPrompt` (772–810).
- `_handlePostAgentRun` continues for queued messages only, and under the
seal nothing queues.
- The controller sends no other run-starting command.
**Normal sealed runs raise no O1–O6.** I looked for false positives.
- O3: an aborted or failed run still emits `agent_end` before the settle
(`agent-loop.js` 124–127; `agent.js` handleRunFailure 349–364).
- O4: a failure before the loop starts has no `agent_start`, but it emits a
failure message, which O4 exempts.
- O6: a continuation adds no user message (`runAgentLoopContinue` 58–70).
- Row 3: the ack and the flag set at 949 happen in the same synchronous
step, so a `get_state` sent after the ack sees streaming whenever a run
follows.
The one possible false positive is O5. See n2.
**New fixtures and mutants.** N8, N13 and N19–N24 follow from rules 4 and
5. Mutants 34–39 each map to a fixture that kills them. N23 has no mutant,
and it doesn't need one: it pins a gap.
## Non-blocking
- **n1. Pin what the binary executes.** `node_modules/.bin/pi` runs
`dist/bundle/cli.js` (e6d7fcf3…). That file loads
`dist/bundle/chunks/chunk-JVUZSMYM.js` (3d8b2dec…), which imports seven
more chunks.
- The chunk contains agent-session, the resource loader, the built-in
list, llama and a bundled copy of `pi-agent-core`.
- The files §3 pins are not what `pi` loads: `dist/core/*`,
`dist/extensions/*` and `node_modules/@earendil-works/pi-agent-core/dist/*`.
- I matched the cited logic in the chunk: the 860 check, `_runAgentPrompt`
and its `finally`, the settle order, the "already processing a prompt"
guard, `clearQueue`, `triggerTurn`, `nextTurn`, the `noExtensions`
branch, the llama TUI check, and the aborted and error `stopReason`
paths. So the citations hold.
- Decision 31 ties the seal to "built-in code tied to the pinned Pi
artifact", and `engine-pin-mismatch` checks the built-in set. Both
should check the executed artifact. That is either
`package-lock.json`'s integrity for `pi-coding-agent` 0.85.1
(`sha512-FGRN+OHb…`) or a manifest of `dist/bundle/**`. Keep the dist
pins as reading citations.
- `pi-coding-agent` declares `^0.85.1` for `pi-agent-core`. The binary
doesn't load that package, so the caret has no effect on the executed
code.
- **n2. The controller's own clear emits a `queue_update`.**
`clearQueue()` emits one before it returns (1201). `rpc-mode.js` 334
evaluates it before it writes the response, so the event precedes the
`clear_queue` response on the wire.
- O5's "a `queue_update` the controller didn't cause" has to count that
event as caused. An implementation that marks "caused" only once the
response arrives fires O5 on every Interrupt, and every stop then ends
`uncertain`.
- State the order in §3. Make the N10 fake emit it in that order. Have
the real-binary smoke record it on its idle `clear_queue`.
- **n3. `aborted` with no stop in progress.** Row 4 maps `aborted` to
`failed` with reason `interrupted` and links the stop ID.
- Under decisions 30 and 31, only the controller's `abort` can produce
`aborted`, so this case is unreachable.
- Say that an `aborted` result with no stop in progress is an overlap
signal, or leaves the receipt `working` as "outcome unknown". Then the
code never has to invent a stop link.
- **n4. For Dewey's cut pass (decision 31).** These places still describe
the explicit-extension registry:
- §3 seal bullets 1, 3 and 4;
- N5 and N15 ("a registered input extension");
- N24's hash and non-local cases;
- mutant 37;
- the review-based wording in Limit 11.
With no explicit extensions, nothing on a sealed engine acks a prompt
without running it. Keep row 3 as a conservative row, and run N5 and N15
on the fake alone. N24 and mutant 37 become "any `--extension` refuses".
Limit 11's "none of them could move to the mediated path" is now
answered by decision 30 option (a). The Gate's C-1 line, its C-5 line
and "CHAT-03 done" change with decision 30.
Rocko's r3 finding 1 (report 19e3fcff), an import that escapes the hashed
tree, is resolved by decision 31, so I haven't repeated it. I agree with his
note 2 on the `no-turn` fence cleanup.
@@ -0,0 +1,193 @@
# CHAT-03 R1 — adversarial review of control and recovery
Verdict: **request changes**. Rocko, 2026-09-26.
Verified pins before reading:
- BRIEF.md: `5dd447f710d425ae01a12dcb56421fa948faf6b44d07c8778ba3ab9f8459daf1`
- REVIEW-REQUEST.md: `3a03adb9001d6e07c044cbb91b3b9dadf7516a6804e17097ac6a3f4dcd74193e`
- Stated base: `40a02d2b`.
Scope: sections 5–6 and their section 2/3 dependencies. Findings below are
specification failures/schedules, not executed tests of unbuilt code.
Pinned Pi source was inspected for the native-protocol claims. No live
engine, scope or session was changed.
## 1. Blocking — exclusive revision files do not define a crash-safe two-key claim transaction
Section 2 specifies highest-revision-wins files, exclusive create and two
keys taken in order. Ordering prevents a lock-order deadlock, but says
nothing about how those two durable histories commit together. Crash after
reserving the seat key but before the session key leaves a partial claim.
Crash after advancing one key to stopped leaves disagreement. A crash
between exclusive creation of the final revision pathname and completion
of its contents exposes an incomplete highest revision, not the promised
old or new complete state. fsync after the write cannot prevent that window.
W5's SIGKILL-only fixture also does not test power-loss durability. W4's
“no partial claim left” requires a protocol, not just an assertion.
**Fix:** specify an atomic publication boundary for complete revisions and
an explicit transaction/claim id tying both keys together. Describe restart
handling of partially reserved and partially transitioned pairs, including
reserved/stopping/uncertain, not only active. Uncertain/invalid publication
must retain exclusion; do not skip a damaged latest record and reuse an
older stopped state. Define controller ownership so a losing contender
cannot change an active holder's claim into an orphan merely because that
contender cannot attach to its pipe. Include launch-before-active-publication
and crash-before-cohort-identity-publication windows.
**Add fixtures:** crash between every key publication and file publication
barrier; paused live holder versus second startup; engine spawned before
active publication; partial release. Prove both keys remain exclusive and
that recovery never deletes another holder's reservation.
## 2. Blocking — dispatch tests omit concurrent prompts and partial/unknown pipe writes
H3's “never a partial write” is not guaranteed by a controller mutex.
A large JSON line can be partially delivered when the controller or pipe
fails; stdin.write acceptance/backpressure is not native consumption proof.
Crash after complete dispatch but before the client receipt is another
unknown-outcome point.
Also, “busy engine” is not defined to include a sent prompt whose
agent_start has not yet been read. Two successive prompt commands can both
see native idle before the first start and violate the one-unstarted-item
assumption that R3-1 depends on. This is the same class of race found in
Discord 6b, even though CHAT-03 uses a separate adapter.
**Fix:** reserve a pending dispatch slot under the dispatch lock before
writing; count it as busy through delayed start/acceptance and terminal
reconciliation. Distinguish local write, native acknowledgment and unknown
outcome. On partial/error/crash, fence and retain uncertainty, never claim
unsent or retry automatically.
**Add fixtures:** two prompts before any native output; backpressure and
partial line followed by EPIPE/controller death; completed write with lost
acknowledgment. Retain H3's actor/generation ordering, but permit an honest
unknown transport outcome rather than claiming impossible write atomicity.
## 3. Blocking — exact returned text is not unique input identity or a lasting empty-queue proof
R3-1 treats one exact clear_queue text match as proof that the dispatched
Mosaic request was unconsumed. The brief explicitly allows extension input.
An extension may enqueue the identical text while Mosaic's input is still
in asynchronous preflight or its start event is pending. Clearing that
external item once satisfies the proposed predicate without proving the
Mosaic item was removed. The UI then invites the actor to submit the text
again, potentially duplicating work.
There is a second interval: an extension or in-flight preflight can enqueue
after clear_queue but before/during abort. Command order alone does not
prove CHAT-01's cleared native queue with no remaining IDs at reconciliation.
A fake that never injects after clear_queue can pass N1/N2 while this
property is false. The pinned RPC handler launches session.prompt
asynchronously and forwards its preflight result; clear_queue returns the
session's current queue, not a dispatch-id-bound removal receipt.
**Fix:** state the provenance assumptions that can actually be enforced.
Without unique attributable native evidence, treat returned text as
ambiguous, not proven-unconsumed. Do not offer an apparently safe resend.
Require the turnProof to cover in-flight preflight and any post-clear
insertion before reopening. If the protocol cannot establish that, remain
uncertain. Contract C-1 cannot make a text equality test into identity proof.
**Add fixtures:** extension queues exactly the dispatched text once;
preflight resumes after clear_queue; extension enqueues between clear and
abort; native events arrive late. Also specify whether abort is issued on
a timed-out clear and how a confirmed force stop remains able to terminate
the cohort without waiting forever for native cooperation.
## 4. Blocking — cohort proof lacks a trusted containment and observation procedure
A systemd scope survives setsid, which is useful, but an empty cgroup
snapshot is not by itself the complete-membership/death proof required by
CHAT-01. The brief does not define who establishes the membership epoch,
how engine launch is contained before it can fork, how scope identity is
protected from reuse, or how it distinguishes an empty scope from a missing
or inaccessible path. Sampling current pids misses short-lived members;
same-uid processes may have ways to leave a delegated scope. A fake that
reports its own membership/death cannot supply the missing authority.
Section 6 lists some proof fields but omits the contract's explicit
stop/authority/conversation/execution/cohort/epoch binding and trusted
verification digest provenance. A different boot proves old local
processes dead only when the claim is bound to this same host; no host
identity or nonportable-root rule is stated. A copied/foreign-host claim
must not become stopped just because its boot id differs.
**Fix:** name the supervisor producer/verifier and containment assumption
for these fixtures, distinguish unavailable evidence from an empty cohort,
and require immutable scope incarnation/host/boot/epoch attribution. Either
provide trustworthy full proof or leave stopped unavailable. Specify what
happens if the controller dies before sending TERM or scheduling KILL;
K10's “resumes observing” cannot imply that missing escalation has happened.
**Add fixtures:** fork during enumeration/termination, unavailable or reused
scope, escape attempt or explicitly refused unsupported containment,
foreign-host boot mismatch, controller death before TERM and between TERM
and KILL. Verify other cohorts survive using independent observations.
Process-group fallback remaining uncertain is correct.
## 5. Blocking — restart dedup refusal has no way to distinguish old requests from new ones
Section 1 discards its dedup index on controller restart but promises that
an old retry gets receipt-unknown and is never dispatched. With only actor,
conversation and arbitrary client request id, an empty index cannot tell
an old id from a new one. H12 tests reconnect, not restart.
**Fix:** require a controller-incarnation token in admitted requests and
reject obsolete tokens even when content/id is retried, or persist enough
request identity to refuse old attempts. The first option preserves the
stated non-durable scope. Unknown old outcomes stay explicit; no heuristic
based on text or timestamps.
**Add fixture:** crash after native dispatch before receipt, restart,
reconnect and retry exactly the same request; prove no second engine write.
Then prove a genuinely new request can be admitted after valid recovery.
## 6. Blocking — foreign-writer detection assumes stream IDs Pi does not emit
Section 2 calls every appended session-entry id absent from the controller's
stream foreign. In pinned Pi, agent-session.js emits message_end to
listeners before calling SessionManager.appendMessage. appendMessage then
generates the session-entry id; RPC simply forwards toJsonEvent(event).
The generated entry id is not thereby present in that message event.
Settings and other session entries need a mapping too. A fake that assigns
the same fabricated id to both sides would falsely validate W10 and mark
normal Pi appends foreign in the real adapter.
**Fix:** identify an actual native correlation/snapshot protocol and its
race bounds, or advertise foreign-writer attribution as unavailable and
fence on unexplained drift without claiming a known foreign writer.
get_entries/get_tree can expose ids, but merely trusting the engine's
post-hoc report is not proof of sole writer attribution.
**Add fixture:** generate session entries with the pinned SessionManager
and pinned event shape, including settings and delayed persistence. A
normal own append must not trigger the foreign-writer result; an external
append cannot be blessed solely because a later snapshot contains it.
## 7. Not blocking — tighten recovery acceptance at implementation review
Recovery eligibility is correctly separated from launch. State explicitly
that eligibility is a single-use, incarnation/pin/leaf-bound input, and that
the launcher reacquires/retains both claims and revalidates it before spawn.
Add two simultaneous launcher calls with one eligibility record and a leaf
change after eligibility. The existing exclusivity requirement already
implies refusal, so this is a missing acceptance case rather than a new
architecture blocker. Wrong-leaf refusal must retain containment/claims
until the mistakenly launched engine is independently proven stopped.
## What is fine
Same-uid control is honestly limited to fixtures and explicitly blocks live
use. Disconnect retaining claims/control, revoked-generation checks,
confirmation binding, no automatic replay, unknown effects after kill,
settled-not-stopped, explicit force-stop supersession and refusing changed
resume pins all match the intended contract. Retain those requirements.
No finding here authorizes broader source ownership, a live seat migration,
model calls, new credentials or a production scope configuration.
Only this report was written. No source, contract or session edits; no
commit. Please resolve the six blocking findings before calling the brief
ready to build. Filbert's independent whole-brief review remains separate.
@@ -0,0 +1,30 @@
# CHAT-03 R1 — correction to finding 3
Rocko, 2026-09-26. Supplements, without changing, report
`89752c2b95f93b3eefd9f4286d24277de25797dbb9f7a161b321035f6e12ff10`
against brief R1 `5dd447f7…af1`.
Filbert's review `ec00544e72d07d19180ea7e40ae769e5ef9917cc177703e10b0fbf8ebdae6e17`
correctly identifies a stronger problem with R1's N1/N2 premise. I checked
pinned agent-session.js: isStreaming reads _isAgentRunActive; that remains
true across post-run continuation/retry until settlement. A prompt without
streamingBehavior throws while it is true. The stated retry-window native
queueing of Mosaic input is therefore not a reachable case under this
adapter policy. My original finding 3 did not establish that reachability
and should not be read as doing so.
Correction: remove the claimed Mosaic-queue attribution case and rebuild
N1/N2 around external queue clearing and abort order, following Filbert B1.
Do not add a complicated text-identity mechanism to support an unreachable
path. An exact text match still cannot turn external input into proof about
a Mosaic request. Any accepted-but-not-started request needs its own native
preflight/failure analysis and an honest uncertain outcome, not deduction
from clear_queue text. Post-clear external insertion remains an independent
queue-reconciliation question; show the supported assumptions or stay
uncertain before reopening.
This narrows the proposed remedy for my finding 3; it does not change the
request-changes verdict. Filbert B2 corroborates finding 6's lack of stream
entry IDs, and B6 corroborates finding 1's split-key/reserved recovery gap.
No revised brief has been approved by this addendum. No source or contract
was edited.
@@ -0,0 +1,59 @@
# CHAT-03 R2 — control, stop and recovery adversarial review
Verdict: **revise**. Rocko, 2026-09-26. One blocking finding.
Verified candidate pins before review and again at completion:
- BRIEF.md: `5c5b45a277f3a555337a5caf57ff1b300b95781b69640ec8974770bdd44af9bd`.
- REVIEW-REQUEST.md: `c75ad86fc014b7a13f72132c0378d6e8abf09663820eee701dc67e3ed50ef92f`.
- Stated base: `40a02d2b`.
- Installed `agent-session.js` matches the packet: `fb8a3981c20c8c0bbd42231b1c99a10335fb3858b659056b341954de9cfa467f`.
Scope: sections 5–6, their section 2/3 dependencies, and disposition of my R1 findings and addendum. This is a specification review, not an implementation certification. No source, live session, cgroup, service, configuration or contract was changed.
## 1. Blocking — settling the slot does not establish an interrupted turn
The slot table at section 3 lines 591–595 assumes that a late preflight ack means a run started, and that a visibly started run follows an interrupted turn. Rule 5 then permits reconciliation after a settled slot, native idle and empty clears. Those observations do not distinguish interruption from ordinary completion or no run.
Concrete schedules:
1. A run has emitted its user message. Interrupt fences admission and sends `clear_queue`. While that exchange is in flight, the run completes normally, emits its successful assistant result and settles. The controller then sends `abort` against an idle engine. A subsequent `get_state` is idle and the post-settle clear is empty. Every clear can be empty, so C-1 does not catch this. The table says `turnState: interrupted`, despite the successful completion.
2. Interrupt fences a prompt still in preflight. An input extension returns `handled` after the first clear/abort. Pi acknowledges it without starting a run. The first table row instead says “Ack: a run started.” Repeating clear/abort cannot manufacture a run or an interruption. Likewise, a preflight error can settle the slot as `failed` without there ever being a turn to interrupt.
This is a contract boundary, not just a missing test name. `docs/plans/chat-01/check.mjs:315` accepts `reconcile-interrupt` only with `turnState: interrupted`; line 325 records `turn-interrupted`. `input-reconciled` is reserved by that checker for revocation. A fixture that always aborts an active fake run can pass N3/N4/N9 while the real schedules above remain unrepresented.
Native evidence: `agent-session.js:832–847` acknowledges extension-handled input without `_runAgentPrompt`; `abort()` at 1222 only aborts current operations and waits for idle. It does not certify that a run was interrupted. I invoked that installed abort method on a minimal idle receiver: it resolved with no run events. That is a method-level check, not a real-engine smoke or model call.
**Required fix:** separate receipt settlement from interrupt-proof construction. Route a late ack through the same observed-run/handled-without-run classification as an earlier ack. Preserve an already completed receipt and its effects; do not relabel completion as interruption. Specify the no-run, naturally completed, failed and genuinely interrupted outcomes of the stop itself. With the current contract, any case lacking evidence for the required interruption must remain uncertain with admission closed and force stop available, unless a separately reviewed contract change supplies an honest reconciliation outcome. Do not silently borrow revocation's `input-reconciled` value.
**Required fixtures:** successful completion between clear and abort; late handled ack after the fence with no run; preflight error after the fence with no run. Assert both the request receipt and the stop proof/state. Include a mutant that substitutes idle + empty queue for interruption evidence and ensure it fails. A response to this finding may choose a conservative interim instead of expanding CHAT-01.
## 2. Not blocking — bound the ack-without-start explanation to complete observations
The N11 `failed` result is sound for the stated narrow case: positive run-failure evidence, settlement for that run, and a complete ordered native stream with no user-message start/end. The native run emits user start and end before persistence; `agent-session.js:386–398` persists the user message only after notifying listeners. The core loop also emits the initial user events before entering the model loop. No additional native user persistence path was found in the inspected code.
However, “events reached the controller first” is stronger than the cited function establishes: it invokes local listeners first. It does not itself prove successful delivery to a separate controller. Qualify the explanation accordingly. Missing output, parse failure, lost transport or missing settlement must retain uncertainty, as section 1 already requires. `failed` also is not proof that an input/before-agent-start extension had no external effects; keep effects accounting separate and do not present automatic replay as safe.
This is a wording/evidence qualification, not a request to restore text identity or queued-Mosaic-input machinery.
## R1 disposition and the remaining design
| Prior finding | R2 assessment |
|---|---|
| 1: two-key claims and crash publication | Closed for the specified cooperative protocol. Complete immutable publication, claim IDs, conservative pair state and live-owner checks supply the missing mechanism. W5 explicitly stops short of claiming a power-loss test. |
| 2: pending dispatch and unknown writes | Closed. Reserving the slot before the write and poisoning unknown transport outcomes prevents the second dispatch and unsafe retry scenarios. Slot settlement still needs finding 1 above when used to build an interrupt proof. |
| 3 plus addendum: native queues and fence | The original text-identity scenario is retired correctly. External queue clearing, bounded late preflight handling and the post-clear limit are honest. Interrupt reconciliation remains open only as finding 1 above. |
| 4: cohort observation and containment | Closed at brief level within the expressly fixture-only guarantee. The shim, invocation identity, freeze/enumeration/kill/empty observation and missing-is-not-empty rule are concrete. K13 must actually pass before real cohorts receive `stopped`; otherwise the stated fallback is uncertainty. This does not certify the unbuilt implementation or live trust. |
| 5: restart dedup | Mechanism closes the replay scenario. V-1 honestly names the contract deviation and gates I1 approval on Sage's ruling; this review does not substitute for that ruling. |
| 6: unavailable stream entry IDs | Closed within the stated drift-detection limits. Idle `get_entries` comparison replaces the nonexistent stream key, and delayed persistence, unchanged-ID rewrites and non-cooperating writers are disclosed. |
| 7: recovery eligibility | Closed at brief level. A retained reservation, single-use eligibility and claim/leaf/pin revalidation address double launch and changed-leaf races. |
**Spawn marker / W20:** fine and deliberately conservative. A marker with no unit cannot establish whether an engine ran and escaped observation. Holding it until same-host boot proof avoids inventing a death proof. A marker visible on either half of a pair must retain that classification when the pair is completed. Foreign-host and live-owner refusals are also correct. This rule may strand a fixture reservation until reboot; that is the documented availability tradeoff, not a safety defect to work around.
**C-1 interim:** strict enough and acceptable. Non-empty clear means no reconciled transition, admission closed, force stop available. There is no need to relax it for CHAT-03. It does not solve finding 1, whose schedules can have entirely empty clears.
**Cohort restart:** refusing when the recorded invocation cannot be checked is correct, including a crash before identity publication. A boot-proof implementation must preserve the declared contract/verifier boundary rather than fabricate a freeze observation or member death times; assess its exact evidence representation in the code review. Same-uid interference and live producer/verifier trust remain explicitly deferred.
## Evidence and limits
Reviewed the pinned brief/request, previous Rocko report/addendum, CHAT-01 schema/checker and the installed Pi session/RPC/core-loop paths. Rechecked both candidate hashes and the cited session source hash. Ran the small idle-abort method check described above. No candidate implementation exists, so no H/N/K tests or repository suite pass is claimed. Only this report was written.
@@ -0,0 +1,48 @@
# CHAT-03 R3 — final adversarial pass
Verdict: **one blocking finding remains; return to Sage for the decision-27 scope cut, not R4.**
Rocko. Target pins verified before reading and again at completion:
- BRIEF.md: `2c5be6b4b2caddf9314e8fdcc9108d744b770fe8f469e9c2d410ee6d14c6b1ec`.
- REVIEW-REQUEST.md: `e197b8822df1d04b4c89ee9873855525db3880258cbcdd0eafa2ffd2ef689823`.
- Stated base: `f2b9e622`.
Scope incorporates Sage's decision 30: no goal in mediated seats; Pi dialogs, Pi H5–H8, C-1/C-2/C-4 are cut; idle drift moves to CHAT-07 and C-5 to CHAT-04. None of those cuts is treated as a blocking omission. Claude permission races remain in scope. No source, contract, live engine or service was changed.
## 1. Blocking — the retained seal hashes an extension directory, not necessarily the code it executes
Section 3 permits a local explicit extension if every file in its directory tree is hash-pinned and it has an input-silent review record. It does not require executable imports to stay in that tree or in another pinned, reviewed artifact. The installed loader (`dist/core/extensions/loader.js:409–428`) uses `jiti.import`; a local extension can import a sibling outside its tree. Refusing npm/git *extension arguments* does not refuse module imports from a local entry point.
Concrete schedule, without a hostile actor or goal:
1. `extension/index.ts` imports `../helper.ts` and invokes `install(pi)` from it. At review time the helper does nothing; the extension is input-silent.
2. A subsequent helper edit adds `pi.sendMessage(..., { deliverAs: 'nextTurn' })`. Nothing in the registered extension directory changes.
3. The next bind has all three required flags, the same local entry path, matching registered tree hashes and the existing review record. N24's checks all pass, but Pi executes code different from the reviewed code.
4. The helper can queue input invisible to clear/abort and attach it to a later Mosaic prompt. As N23 correctly documents, O1–O6 need not fire. Receipt and stop attribution nevertheless name the seal as their basis.
I reproduced the import/hash part using the installed `jiti/static` importer, a temporary directory and a stub `sendMessage` API. The entry imported a sibling helper. Before the helper edit, zero calls; afterwards, one `nextTurn` call; the hash of the sole file in the registered extension directory was unchanged. Module and filesystem caches were disabled for the check. No Pi engine, model, network request or live session was involved; the temporary files were removed.
Limits 7 and 11 honestly admit missed signals when the seal fails. They do not make this a sufficient binding precondition: here a correct review becomes stale without any specified hash check failing. This is an unpinned executable dependency, not merely a reviewer incorrectly approving the bytes subsequently loaded. The same issue includes dependencies reached through symlinks outside the registered tree.
**Recommended scope cut:** defer arbitrary explicit extensions. Permit only a fixed, reviewed fixture set whose executable dependencies are entirely inside the pinned artifact(s), with no escaping imports, dynamic code loading or external symlink targets. An empty explicit-extension list is the smallest starting scope now that Pi dialogs and goal are out. Keep the mandatory built-in extension tied to the pinned Pi artifact and its reviewed dependency code. Do not add a general dependency crawler or new run-proof machinery for CHAT-03. If a fixture extension is retained, its acceptance evidence must show that changing code it imports cannot leave the binding accepted under the old seal.
This is in the retained sealed-engine section and therefore remains blocking after decision 30. Sage owns the cut; this report requests no fourth brief round.
## 2. Note — keep the no-turn fence local to its interrupt
The new no-turn refusal is a reasonable narrowing and avoids needless stop records. At implementation review, verify that its fence cleanup cannot reopen admission closed by a concurrent force stop, overlap signal or revocation. A useful schedule is: interrupt begins, force stop supersedes or an overlap signal closes admission, then the interrupt finds no slot/run and returns `no-turn`. Admission must remain closed under the surviving reason. H10 and the general dispatch gates already require that behavior; add this branch to their fixture coverage. This is not a new contract requirement or a blocking brief defect.
## Prior findings and retained behavior
**My R2 blocking finding is closed.** Rules 4 and 5 now separate the item's receipt from the stop outcome. N14 preserves normal completion, N15 covers a late handled ack without a run, and N3 covers preflight failure. Only an observed aborted run, with complete ordered evidence and no overlap signal, can support `turnState: interrupted`. Until C-5, the other outcomes retain uncertainty and closed admission. Moving C-5 to CHAT-04 makes this more expensive operationally, but does not make it dishonest.
**My R2 persistence note is closed.** N11 now retains `delivery-unknown`; the text distinguishes local event/persistence ordering from prompt attribution and from absence of extension effects. It does not promise to detect a silently lost line.
**O1–O6 are useful alarms, not independent proof of a seal.** The disclosed late-attribution windows in N8/N19/N21 and invisible queues in N22/N23 are honest. Their tests must assert the conservative presentation as well as admission closure. They cannot compensate for finding 1. With genuinely fixed input-silent code, one pending slot and the native response ID provide the stated basis for attributing the subsequent run by exclusion. Removing goal is consistent with that basis.
**Control and recovery remain acceptable at brief level.** The slot is reserved before a write; unknown writes poison the pipe; restart tokens forbid replay; a marker on either claim key prevents no-unit release; foreign-host and live-owner refusals remain conservative. Force stop still requires independent cohort observation rather than engine idle/settled, checks invocation identity, and treats absent evidence as uncertain. Recovery retains both claim keys and single-use eligibility. K13 and the producer/verifier limitations remain explicit acceptance gates, not established implementation facts. The section-6 change from R2 chiefly incorporates the corrected interrupt outcomes; it does not weaken the cohort rules.
## Evidence and limits
Read the pinned R3 brief/request, prior Rocko and Filbert findings, the retained Pi protocol/session paths, and the installed extension loader. Ran the isolated importer/hash reproducer described above and rechecked both packet hashes. This is an unbuilt brief: no claim is made that the proposed N/H/K tests or repository suites have run. Only this report was written in the checkout.
+3
View File
@@ -428,3 +428,6 @@ are never rewritten or removed; corrections are new entries.
2026-09-27T00:38:30Z | Filbert (T3 Claude Code, thread 9cb9731e) | CHAT-03 brief R3 review (#1507 row 5), BRIEF 2c5be6b4, scoped by lead decisions 30 and 31 | approved on retained sections, no blocking finding; F1, F2 and R2 n1/n4 closed, n2/n3 moot (drift to CHAT-07); notes n1 pin executed bundle, n2 own-clear queue_update precedes response, n3 aborted with no stop, n4 decision-31 cut-pass list; review agents/filbert/work/chat-03-brief-review-r3-2026-09-27.md 48447592
2026-09-27T00:39:00Z | Sage (T3 Claude Code, thread 1ef1e4f8) | CHAT-03 r3 closed | Filbert r3 approve (48447592), Rocko blocker closed by decision 31. Lead decision 32: pin pi lockfile integrity (n1), O5 counts clear queue_update (n2); Dewey one edit, Filbert diff-only scope check, Sage pins.
2026-09-27T00:40:05Z | Sage (T3 Claude Code, thread 1ef1e4f8) | goals review ratified | Jason: "goals-review doc looks good". Page marked ratified, QUEUE priority line updated, AGENTS.md pointer to the north star added.
2026-09-27T00:42:33Z | Dewey (T3 Claude Code, thread 84fb346c) | CHAT-03 brief final edit (#1507 row 5), lead decision 32 | R3 frozen as BRIEF-r3-2c5be6b4.md; final BRIEF.md 1ef15ac0 (1451 lines, 0444) applies decision 30 cuts, decision 31 (no explicit extensions), n1 lock-integrity pin, n2 clear queue_update (O5, N25), Rocko fence build note; diff BRIEF-r3-to-final.diff c6bd1f1e sent to Filbert for scope check, then Sage pins. No commit or push.
2026-09-27T00:44:53Z | Filbert (T3 Claude Code, thread 9cb9731e) | CHAT-03 final scope check (lead decision 32): BRIEF.md 1ef15ac0 against r3 2c5be6b4, diff c6bd1f1e | PASS, all five items applied, nothing else in the diff; result sent to Sage, copy to Dewey; no commit or push
2026-09-27T00:45:04Z | Sage (T3 Claude Code, thread 1ef1e4f8) | CHAT-03 brief pinned | Filbert scope check PASS; BRIEF.md 1ef15ac0 committed with rounds and reviews; lead decision 33; QUEUE row 5. Source author after A2.
+10
View File
@@ -418,3 +418,13 @@ which stay with him. Each item names who decided it and what happened.
31 and 32, and add Rocko's fence note to the build. Filbert checks that
the diff only does that. That's a scope check, not a review round.
Then Sage pins the hash, and CHAT-03 is ready for a source author.
33. **CHAT-03 brief pinned.** Filbert's scope check passed. The final
`agents/dewey/work/chat-03/BRIEF.md` (sha256 1ef15ac0…, 1,451 lines,
down from 1,527) differs from r3 (2c5be6b4) only by items 30 to 32 and
what follows from them. Dewey's diff c6bd1f1e matched Filbert's own
regeneration. His two observations are accepted as written. The P3 rule
disables every Pi dialog, which is wider than CHAT-01, but no dialog can
reach the controller without explicit extensions. Sage committed the
brief, the earlier rounds and the six review reports. Sage names the
source author after queue A2 lands, so Darkwing isn't split between
them. The code may not start before then.
+2 -1
View File
@@ -42,7 +42,7 @@ which clears rows 9-13 to proceed; it does not authorize publication/access chan
| 2 | Seat registration and `mosaic seat task` | darkwing | #1504 | done 2026-09-13 | B passed 2026-09-12 | plan page, Step 3 |
| 3 | Reply-from-board (piece 2 on the plan page) | darkwing | #1505 | done 2026-09-12 | C passed 2026-09-12 | plan page, "Piece 2: reply-from-board" |
| 4 | Ledger (piece 3 on the plan page) | darkwing | #1506 | done 2026-09-13 | D: 18.9 human messages per closed issue, week of 2026-09-06 | plan page, "Piece 3: ledger" |
| 5 | WebUI first screen on the Console design (piece 4 on the plan page) | dewey; filbert reviews | #1507 | CHAT-00/01/01C contracts published (370823b3, 28d4e98a, b023841c); CHAT-02 done: backend a5beb6d9, Console c9e771cf, live check passed 2026-09-26; CHAT-03 chartered 2026-09-26 (Jason approved); after brief r3, source work waits for Sage to rescope against Gate E (lead decision 27), Dewey writes the brief, Filbert reviews, Rocko adversarial, source author named by Darkwing after brief approval; CHAT-04..08 not chartered; Gate E blocked | E: all-seat interactive demonstration then Jason workday ruling; live cutover separately approved | `2026-09-13_webui-session-chat.md`, CHAT-00..08 |
| 5 | WebUI first screen on the Console design (piece 4 on the plan page) | dewey; filbert reviews | #1507 | CHAT-00/01/01C contracts published (370823b3, 28d4e98a, b023841c); CHAT-02 done: backend a5beb6d9, Console c9e771cf, live check passed 2026-09-26; CHAT-03 brief approved and pinned 2026-09-27 (BRIEF.md 1ef15ac0, rescoped to Gate E, lead decisions 30 to 33; Filbert r3 approve, Rocko blocker closed, Filbert scope check pass); source author named by Sage after queue A2 lands; CHAT-04..08 not chartered; Gate E blocked | E: all-seat interactive demonstration then Jason workday ruling; live cutover separately approved | `2026-09-13_webui-session-chat.md`, CHAT-00..08 |
| 6 | Darkwing on point: darkwing assigns and gates filbert's work (piece 5 on the plan page) | darkwing; filbert authors pilot; darkwing/dewey review | #1511 code phase; #1512 pilot | done 2026-09-27 (Sage, lead decision 29): #1511 and #1512 landed in af4203ca (pushed) and are closed; Gate F not passed: the ledger counts 2 human messages in filbert's T3 thread during #1512 (one is Jason's 09-26 seat takeover) and its first 11 days predate the T3 source; Gate G (row 9) now carries the zero-human-message test | F: filbert's item closes with zero human messages from Jason; code phase does not claim Gate F | `2026-09-15_relaunch-activity.md`; `2026-09-14_task-attribution.md`; plan page Piece 5 |
| 7 | Weekly ledger run and rails number | sage (lead decision 27); Jason reads | — | recurring, every Monday; 09-13..19: 47.7, 09-20..26: 33.0 human messages per closed issue | under 10 human messages per closed issue for the week of 2026-09-13 | `packages/ledger/README.md` |
| 8 | Fleet seats (`~/.mosaic`) onto `mosaic launch` | unassigned | — | parked by current owner direction; no `~/.mosaic` changes during internal bootstrap | Jason's call | none yet |
@@ -146,3 +146,4 @@ Gate F or when blocked."
- 2026-09-27 — sage (lead): row 5 State: CHAT-03 source work waits after brief r3 for a rescope against Gate E. Row 7: Sage runs the ledger each Monday. Weeks 09-13..19 and 09-20..26 came to 47.7 and 33.0 human messages per closed issue, against a target under 10. See `docs/plans/2026-09-27_goals-review.md`, lead decision 27. No other row changed.
- 2026-09-27 — sage (lead): rows 1, 6, 23, 24 and 25 done (lead decision 29). #1503, #1509 and #1511 closed with evidence comments. Gate F recorded as not passed; Gate G carries its test. No other row changed.
- 2026-09-27 — sage (lead): priority line: Jason ratified the goals review (north star and goal order). No row changed.
- 2026-09-27 — sage (lead): row 5 State: CHAT-03 brief pinned at 1ef15ac0 after the scope check. Source author named after A2. No other row changed.