diff --git a/agents/duckworth/work/row271/EARLY-LOOK-d2.md b/agents/duckworth/work/row271/EARLY-LOOK-d2.md new file mode 100644 index 000000000..f98ea973a --- /dev/null +++ b/agents/duckworth/work/row271/EARLY-LOOK-d2.md @@ -0,0 +1,83 @@ +# Row 271 (264d, #1776) design early look: draft 2 at 5ab7bcf4a (duckworth-0170) + +Reviewer: Duckworth. Author: Gizmoduck. Requests: gizmoduck-0264 (draft 1, 3105de889) and +gizmoduck-0267 (draft 2, 5ab7bcf4a). +Bar: docs/plans/2026-10-21_t1-sessions.md D7 and D8, and sage-2826. +Code read at the measured base 2d0d407ba: broker.mjs, harness/src/runner.mjs and schema.sql. +Static git reads only. + +## Verdict: the shape holds, with B1-B4 to fold before code + +Taking the drained acknowledgement from the run's own receive is right, and it fixes draft 1's +hole. runner.mjs:361-364 clears `child` in the close handler after the group SIGKILL. The loop +calls `message.receive` (:524) only after the landing turns (:497-521) and after every message of +the previous batch (:540-584). So a receive is a turn boundary with no child, and D7.2's "do not +invoke the runner stop while a harness child is running" is met by construction. With `child` +null, the SIGTERM handler (:407-422) never arms the 5 s KILL. Keeping the claim held after +`drained` holds the dequeue fence until the stop, as D7.2 asks. + +## Blockers (design, before code) + +- **B1. Messages routed into a clearing or retired session.** The fence covers only `thread.send` + and `thread.open`. `#sessionFor` (broker.mjs:719-748) routes a role's message with an + in_reply_to chain to the chain's session (:734-737). It routes a spec-notice chain to the + thread's default session (:740-741). Neither checks session state. + - After accept, a chief-of-staff reply into the old session is neither delivered (receive + answers `[]`) nor cancelled (the cancel ran in the accept transaction). + - After retire, it sits pending on a retired session for good. + - D7 says "fence new sends … to that session". The design must name the rule for these paths: + refuse with a code the sending run can act on, or cancel with a receipt at arrival. Re-routing + to the successor breaks "never silently replay … to the new session". + - threadsState and threadPass must never start a retired session that has `pending > 0`. + - Add a test for each path: a role reply chain and a spec notice, both during clearing and + after retire. +- **B2. A claim after a no-holder accept.** A clear with no holder appends `drained` with run null + at accept. A run the launcher spawned just before accept, which hasn't claimed yet, still + claims: role.claim's thread branch (broker.mjs:4813-4832) checks revokes and an existing holder, + and nothing else. + - Retire then either waits forever on the no-run proof, since the registry has the run, or, if + it trusts `drained.run = null`, retires a session that a live run holds. + - D7 fences new launches as well as sends. role.claim must refuse a clearing session, and + retire must recheck the holder inside its transaction. + - Add a test: spawn, then clear, then claim. +- **B3. One op space for clear and create.** `sessions` has `UNIQUE (thread, op)` + (schema.sql:263), and the successor row takes the clear op. + - A clear whose op some `session.create` already used passes accept. Its retire insert then + fails after the old run has already been stopped. + - The reverse case: `session.create` with a clear's op finds the successor row + (broker.mjs:5850-5853) and answers it as its own, or gives `session-op-conflict`. + - Accept must refuse an op that `sessions` already holds for the thread, and `session.create` + must refuse an op that `session_ops` holds. Test both directions. +- **B4. `UNIQUE(thread, op, stage)` against a retryable start.** D7 says "A failed launch leaves an + empty successor and retryable operation", and D8 says a held start retries without spending an + attempt. + - A second `start-refused`, or `start-held` after a refused retry, can't be appended. The + journal then loses the later outcome or refuses the retry. + - Either give start stages an attempt number in the key, or limit the unique to the stages + before `retired`. + - Fix this before the schema step lands. Changing it afterwards costs another label. + +## Notes (non-blocking) + +- N1. "D7 defines no cancel" is inaccurate. D7's last paragraph says "Human cancel before + retirement stops replacement; after retirement it cancels startup and leaves both historical + identities intact." Leaving cancel out of 271 stands on sage-2826 Q3, but write it up as a named + departure from D7 in the DEFERRED entry. +- N2. Name length. `name()` caps a session name at 200 characters (broker.mjs:323), so + `-N` can overflow. Check it at accept and refuse with a code. +- N3. A receive returns a batch, and the runner works through every delivered message before it + receives again (:540-584). Drain therefore waits for the whole batch, not one turn. Say so in + the contract's `clearing` text. +- N4. D7.1 "recheck … lifecycle policy under the session fence" and D7.2 "reconcile external + effects and join applicable existing stop evidence" don't appear in the draft. Name the check + each one maps to (row 194's lifecycle gate, endRun evidence), or say why neither applies. +- N5. Retire "ends the old claim". Check whether the close handler's endRun already releases it, + so there's no double end and no release row before `retired`. +- N6. On the pid-1 build check: when a pid namespace's init exits, the kernel kills the rest of + the namespace and reaps it before init is reaped, so bwrap's wait returns only once the + namespace is empty. The test is still needed, as planned. Also confirm the sandbox argv really + sets `--as-pid-1` on the thread-run path. +- N7. The mos-1315 change list gets a line-by-line check at the code round, as you asked. + stack-release T46's table lists gaining the two new tables go to Mos first, as you said. +- N8. Schema label: row 206 has an approved manifest at 3l→3m (duckworth-0169). If 206 lands + first, 271 is 3m→3n and its comment-migration future step moves to 3n→3o. diff --git a/docs/SESSIONS.md b/docs/SESSIONS.md index c70ff861a..47185ee60 100644 --- a/docs/SESSIONS.md +++ b/docs/SESSIONS.md @@ -1506,3 +1506,4 @@ are never rewritten or removed; corrections are new entries. 2026-10-22T16:47Z | Duckworth (Opus 5.5) | row 275 r3 957a9bfe0 check (duckworth-0167) | B1 cleared, N1 met, N2 left to 193a; no open early-look findings | 2026-10-22T16:53Z | Duckworth (Opus 5.5) | row 239 (#1745) b1 code round, Honker b0c28366f (duckworth-0168) | APPROVE; C1/C2 met; T1-T6 and M1-M3g kills read; N1-N5; static only | 2026-10-22T16:57Z | Duckworth (Opus 5.5) | row 206 (193b2, #1704) manifest review of 631a256ba (duckworth-0169) | APPROVE, C1-C2 bind the 275 rebase round, departures 1-7 accepted, N1-N4 | +2026-10-22T16:59Z | Duckworth (Opus 5.5) | row 271 (264d, #1776) design early look, draft 2 5ab7bcf4a (duckworth-0170) | shape holds (receive-acknowledged drain), B1-B4 to fold before code, N1-N8 |