feat(execution): a round's store and board channel are scoped by run - #61
Merged
Merged
Conversation
wefio
force-pushed
the
feat/ooo-run-namespace
branch
from
September 13, 2026 12:17
8dcfe9c to
d9104bd
Compare
Migration step 2 of docs/design/task-unit-semantics.md. One store can now hold several rounds:
ooo_probe_runs replaces ooo_probe_meta (id CHECK(id=1)), tasks and checks gain run_id with
composite keys, and the channel is ooo-probe:<runId> instead of a constant. Adoption: a named run
opens it, a single-run store is continued, a multi-run store refuses to guess. The legacy store is
migrated in one transaction, and a refused store closes its handle rather than leaving the file
locked.
checkId stays run-free on purpose: an earlier draft scoped it by run, and replay ("new run,
same attempts") went red with 2 !== 3.
The round suites, not this message, are the evidence: see the record in the next commits.
…ther The design's persistence bullet had no test while a store held exactly one run. Four cases: two runs do not write each other's rows for the same task id and cancel separately; a multi-run store refuses to guess; a one-run store is still continued; a pre-namespace store is migrated in place and its restored cancellation then refuses a claim.
Two anchors moved with the refactor (both had started reporting "not applicable"), and the claim write gains a tooth of its own: a claim that is not scoped by run claims the neighbouring run's row too. That tooth was NOT caught by the first version of the new test — next() and accepted() cannot see a neighbour's row — so the assertion reads the other run's raw owner.
…bles The design's persistence section says a task's immutable manifest, the run facts the board does not carry, and the state that can be recomputed are three different things, and that the third has no authoritative storage. One table held all three. Reads go through ooo_probe_task_view, a projection rather than storage. migration accepts both earlier shapes (pre-namespace and run-scoped) and copies each column into the table that owns it.
Three cases: a claim touches no manifest byte; deleting the cache and rebuilding it yields the same cache and a round that can still deliver; a corrupted cache is repaired by the sources rather than trusted. The rebuild is also where the split was corrected: owner and claim_time looked recomputable and were not, because the board stops reporting claimedBy once the round resolves the entry. Who claimed is a fact, and the cache now holds only the input digest.
The claim tooth follows its statement into the facts table, and the new cache surfaces mean a rebuild that returns without writing must fail the test that deletes the cache.
The design's integration contract: one connection is not one transaction, the store is the only place that runs BEGIN/COMMIT, and a caller already inside a transition joins it through a capability issued for that transition. writeTransaction() is that boundary; withPort() joins it by identity against the store's own open transaction, so a port from another store, a port whose callback returned, and a write entry reached inside a transition without a port are refused by name rather than guessed at. A failure inside a port marks the transaction rollback-only even if the caller catches it, and a failed ROLLBACK quarantines the connection instead of pretending it is usable. putTaskBoardEntry() becomes a thin entry over insertTaskBoardEntry(): standalone it opens the boundary, composed it joins the caller's, and its own BEGIN is gone.
Eight cases, because the contract breaks silently otherwise: the value is committed and returned; a write inside a transition without a port is refused and leaves nothing; a transition cannot open another; another store's port is refused; a port used after its callback returned is refused; a callback returning a thenable is refused; a failure the caller swallows still forbids the commit; a failed rollback quarantines the connection.
…a swallowed failure that commits
Left out of the B2 commit: the committed version still queried ooo_probe_tasks, which that commit dropped, so this suite was red on the branch as pushed. Raw reads now go through ooo_probe_task_view and the retired legacy table is named as ooo_probe_tasks_pre_split.
…longs to The integration contract requires the composed write entries to reuse one implementation: standalone they open the boundary, inside a transition they join it through the port. publish() and publishReady() now take the port and hand it to putTaskBoardEntry(), so a publication inside a transition is part of that transition rather than a second BEGIN (which is refused, not nested). No caller publishes inside a transition today, which is why the suites were green before this: the change makes the rule structural instead of depending on the current ordering of publishReady() and the commit path.
Three cases: a publication commits with the transition; the round's own publication rolls back with a transition that fails after it; and a publication without a port inside a transition is refused rather than nested. The middle one is the check a second BEGIN cannot pass.
removeMemoryFromChain and the three external-embedding upserts ran their own BEGIN/COMMIT/ROLLBACK. The store is now literally the single owner: one BEGIN, one COMMIT, one ROLLBACK, all inside writeTransaction. The embedding upserts already refreshed their vector cache after the commit; the conversion kept that outside the transaction, so a batch that did not commit cannot warm the cache either.
Counting the three statements in the source, and checking that the BEGIN and COMMIT belong to writeTransaction and that every rollback call is one the boundary made. A behavioural test cannot see a second boundary that still works by accident.
…oth exits The early-cancel path returned after gate.close() and left the directory that mkdirSync(tmpdir(), "ooo-cycle-db-") had created to the finally clause it never reached. Both exits now call one releaseDefaultDirectory(), so the two cannot drift apart again. Unverified, stated rather than implied: removing the release from the early path leaves the cancellation suite green, so no test reaches that branch today. The fix is right by construction - the early return creates the directory and does not remove it - but the early branch's reachability is not established. The normal exit's release is what the suite observes.
…tory The cancellation test already points TMPDIR/TMP/TEMP at a directory it owns, so the default store directory's fate is observable from it. The assertion says what it checks: this path releases it. A mutation check says what it is not: with the early path's release removed the suite still passes, so this assertion covers the exit the round actually takes (the finally), not the early return.
wefio
force-pushed
the
feat/ooo-run-namespace
branch
2 times, most recently
from
September 13, 2026 14:58
3366d7a to
553560a
Compare
0001 unchecked tooling: tools/** is outside the type check, so a syntax error in a mutant anchor - written through a shell path twice in one session - reached a run instead of a check. The candidate guardrail is measured: adding tools/**/*.ts to the include list reports 3 errors in 2 files. 0002 wrong-tree verification: a working tree whose suite was green was pushed while the adaptation it depended on stayed uncommitted, so the branch under review was red. The root cause is the object of the verification, not the edit: the gates ran against a directory and the push published a commit. Both are Status: open with their candidates named and not landed, and both name what they do not prove.
wefio
force-pushed
the
feat/ooo-run-namespace
branch
from
September 13, 2026 14:58
553560a to
d6d46e0
Compare
…heckpoints The design puts connection creation, schema migration, checkpoint and close with the Store owner, and requires that a view of a finished round's private database reads it without migrating or writing. This adds that factory as owner configuration: NmgStoreOptions.readOnly opens a true read-only handle, skips migrate(), skips the header-writing `journal_mode` (the other pragmas are connection-local), and skips both checkpoints. Read-only is the capability of that one connection, not a mode of a shared one: this deliberately does not set `query_only`, because a shared connection that stops accepting writes is the failure the design warns about. A missing file and a file with no recognisable schema are refused by name instead of being created, migrated, or reported as an empty store. Not in this commit: the tooth that reverts the handle to a writable open. Its first attempt was not written (an assertion in the same script failed first), and the run that followed it reported 3 teeth as not applicable - prettier reflowed base.ts and broke three hand-written anchors, so they no longer run. The tool excluded them from the caught count instead of counting them, but a default run tolerates them; re-taking those anchors is the next step.
… unmigrated schema The suite pins the three refusals and the property behind them: the owner can write, the view cannot, the database's bytes are unchanged after the view reads and closes it, the owner can still write once the view is released, and a schemaless file is refused with nothing migrated into it. The write assertion is what distinguishes the handle from `query_only`, and the tooth that reverts the handle to writable is the next commit.
A tooth's site was located by exact bytes, so the commit hook's prettier run could retire a tooth without
saying anything: 27 caught became 24 caught plus 3 "not applicable", exit 0. Three changes:
- `ast: { within: "<member>" }` locates a site through the syntax tree, scoped to the member that owns the
rule; `ast: { call, argCount }` locates a call or constructor. The 15 mutants in the two files this
migration keeps editing (base.ts, ooo-board.ts) now use the scoped form.
- A byte anchor is still tried exactly, then with whitespace normalized, and a re-taken anchor is printed.
- A site that cannot be located is a failure even in the default list. "Not applicable" is reserved for a
target file that is not on this branch, which is a claim about the branch rather than about the code.
The header states the rule: use an `ast` locator in any file that is still being edited; a bare byte anchor
is for settled files.
Two things this exposed, both worth keeping in the record rather than smoothing over:
- Scoping a mutant can weaken it. `a-method-opens-its-own-transaction` had a `to` written against the old,
longer anchor and, once scoped, produced a syntax error instead of the mutant - caught by the wrong test.
The from/to pair was made consistent (`this.db.exec("BEGIN IMMEDIATE")` inserted before the boundary).
- The escape trap fired a third time in this session: a shell path wrote `\n` as a real newline, and the
repair was an editor that treats the text literally. Three occurrences, one cause.
`demoteMemory: demotes LTG memory to STG` failed once, under load, and was recorded in the ledger as "flaky, not fixed" - a label, not a diagnosis. Reproduced in a write-then-read loop (2 of 3000 rounds): the row was stamped `valid_from=...38.468Z` while SQLite's `now` read `...38.467Z`, so `valid_from <= now` was false and a memory written microseconds earlier read as not current - a `memory <id> is not active` error from `requireActiveMemory`, or a row missing from maintenance, demotion, dedup and search paths. The window's boundaries now come from one named grace in one home, `src/core/store/clock.ts` (`CLOCK_GRACE_MS = 50`, expressed as `'+0.050 seconds'` - SQLite has no `milliseconds` modifier and an unknown one makes `strftime` return NULL, which silently excludes every row). The grace only ever widens what counts as current, and the two directions are separate so the asymmetry is visible at each of the four predicate sites. - `tests/core/store/current-value-window.test.ts`: 6 cases - both boundaries, 400 write-then-read rounds, and that the window widens on both boundaries and never narrows. - The choice and its alternatives: docs/decisions/implemented/2026-09-18-clock-grace-window.md. - The incident, including why the label was the real damage: docs/postmortem/0004-flaky-was-a-clock-boundary.md. The ledger row that carried the wrong label is corrected in the next commit, with the count (1446/1 before, 1457/0 now). Checks: `npm run test:product` 1457 pass, 0 fail; `npm run check`, `lint`, `complexity:gate` and `docs:check` clean; current-value-window 6 pass; the four window mutants are caught (scoped run reported in the ledger).
…sion policy The offline half of the fusion slice in `docs/design/task-unit-semantics.md`, with the ledger's correction of the row that had called the clock defect flaky. - Legality is one pair predicate in the shared dispatch module (`sharedSessionLegal`, the design's five conditions one line each, with the compatibility, cancellation and pending-branch rules extracted so each reads as one rule) composed with the board's candidate answer, which stays the authority on staleness, cancellation, delivery and waits. `fusionSuccessors`/`fusionCandidates` build only what the rule accepts; the bound and the ranking are runtime policy, not shared state. 12 cases, 8 named mutants. - Cost is two lines (`boundarySavedMs`, `sharedStartupMs`) and a verdict that stays `unmeasured` while the startup term is assumed - one net number would hide which term answered. `cost-model.ts` gains the fusion block and `--session-start-ms` / `--session-start-measured` / `--units-per-session`; 12 cases, 4 named mutants; the sweep prices the thresholds (fusing 4 units per session pays while the shared startup is under 6000 ms, 2 per session under 3600 ms). - Policy: `PlanDriverSpec.fusion` turns the fused path on, `PlanRun.sessions` reports one entry per session, and fusion is reported only from the worker's own `WorkerMetrics.sessionId` - never from the driver's request. `piWorker` refuses a continuation by name, because `executePiPatch` creates a session per call: the live half of this slice is therefore not runnable as specified, and **no paid call was made**. - A driver check that only restated `sharedSessionLegal`'s own rule was deleted rather than kept: the suite could not distinguish it from the shared predicate, which is what one home for that rule means. - The ledger's F4 row carries the scoped counts, and its `test:product` row is corrected from "flaky, not fixed" to the clock boundary fixed in the previous commit. Checks: `agent:verify` passed (check, test:product, build, verify:static); fusion 12, cost-model 12 and plan-driver 15 cases pass; scoped mutation runs 17/17, 4/4 and 11/11 caught, restored byte-identically; `complexity:gate` clean with the two functions this slice pushed above the limit brought back under it by extracting helpers.
…rd the sweep Running the full sweep in four lanes (see the ledger row) exercised the lock, and the lock was wrong in three ways. Each is fixed here, with the case that would have caught it: - `live` was never written on substitution: one multi-hunk edit failed as a whole and only the restore half was reapplied, so a running sweep reported `live: false` and the field lied in the direction that makes a wedge look impossible. `tests/tools/mutation-lock.test.ts` now runs a real (cheap) sweep and asserts a substituted mutant is reported live. - `NODE_TEST_CONTEXT` is inherited when a sweep is started from inside a `node --test` process, and the nested runner then exits 0 **having run no test at all** - which the harness reported as "the suite passed" and which turned every mutant of that target into a false "not caught". The harness strips it before running a suite. - The refusal in `agent:verify` also fired on `--dry-run`, which broke two of the verifier's own tests while a sweep held the tree. A dry run reads the route config and the change list, not the mutated file, so it is exempt; a run that would record a verdict is the one that must refuse. Also fixed: the lock root reads `MUTATION_LOCK_ROOT` in every helper rather than in some of them, so a test can point a sweep at its own directory instead of writing into the tree under test; and `repo-context` reports a sweep from the root its report is about rather than `process.cwd()`, which had made the same report differ depending on where it was rendered from. The integration case that watches a sweep now waits for it to finish instead of killing it - its first version killed the child and left a mutant in `src/core/store/clock.ts`, the tree the test itself was running in. `docs/postmortem/0003` gains those faces of the class (a guardrail is code and carries the same failure modes), and the ledger's mutation row carries the full run: 136 of 136 caught, 20 of 20 targets restored byte-identically, run as four lanes with one sweep per tree. Checks: `npm run lint`, `check`, `complexity:gate`, `docs:check` and `glossary:check` clean; `tests/tools/mutation-lock.test.ts` 4 pass (one of them a real sweep) and `tests/tools/agent-verify.test.ts` 23 pass; the tool targets re-run after the fixes: 3 of 3 caught.
The three normative lines added to skills/repo-development/SKILL.md - a running sweep makes the tree unreadable for checks, an intermittent failure is recorded with its reproduction attempt or left open rather than labelled "flaky", and a lane's progress signal is read from what that lane actually emits - were written before this record, which the approval tier requires to be explicit. The operator approved them on 2026-09-19; this record is that approval, names the mechanical half that code and tests carry (the lock, the refusal, the pinned readings), and states the ordering mistake instead of hiding it.
The ledger's closing section still said the research drivers "still write directly and are not refused" and that the wiring needed "a run surface a different process can reach" - both contradicted by two rows of the same file, D13 (the daemon's register/freeze/bind/cancel/status surface, proven by tests/cli/task-run-surface.test.ts) and D14 (the evidence drivers reach the board through the daemon and open no database of their own). The section now states what landed and what genuinely remains: nothing adopts an entry into a run outside the tests, and that is not a missing surface but a missing caller.
… it a blocker F4 landed fusion's policy half and left the mechanism open: the live worker creates a session per call, so a fused live arm had nothing to reuse. The proposal reported that as a blocker, which framed a missing mechanism as a property of the design. This note records the mechanism the design asks for and the three SDK facts that make it buildable without new capability: session.prompt accepts further turns, the tools read host-owned mutable state so they can hold a box rather than closed-over values, and a tool surface is fixed at creation so a chain registers the union with per-unit refusals. It names the change (UnitState box + createPiSessionRunner, with executePiInput as the one-unit case so no tool surface is duplicated), the two consequences to decide in advance (the artifact schema's per-unit literal union, and per-unit abort versus one runtime signal), and the cost of the three steps: offline mechanism, a ~10-30k live smoke, then the ~400k pilot already approved under the 1000k ceiling.
…its Chinese pair The previous commit pushed this proposed record with docs:check failing - five errors: a proposed record needs Problem / Proposal / Alternatives considered / Acceptance criteria / Risks, not the implemented pair, and it needs a bilingual counterpart. Both are now in place and docs:check is clean (253 files, 0 errors). The substance is unchanged: what the mechanism is, the three SDK facts that make it buildable, the two consequences decided in advance, and the three steps with their costs.
…chanism, step 1) Fusion's policy half landed in F4; this is the mechanism's first step, and it changes no behaviour. The four patch tools closed over their unit's snapshot, check, budget and counters, so a session's fixed tool surface could only ever serve the unit it was built for. They now read one mutable 'UnitState' box, which is what a fused chain needs: the session and its surface are created once, and each unit re-points the box. The single-unit path builds one box and never re-points it, so both paths are one code path. Two consequences are stated in the code rather than left to be discovered: a tool whose capability the current unit lacks refuses by name (a chain registers the union of its units' surface because a session does not allow rebuilding it), and the artifact tool takes an optional permissive conclusion kind for chains, where the per-unit literal union cannot be sampled - artifactEnvelope still refuses an invented kind and the host still validates the result. Next: createPiSessionRunner (the session/prompt loop, per-unit token deltas) and piSessionWorker in the driver, then a cheap live smoke, per docs/decisions/proposed/2026-09-19-fusion-session-mechanism.md. Checks: npx tsc --noEmit -p tsconfig.json clean for the file; npm run lint clean.
…ism, step 2) createPiSessionRunner creates a session and its tool surface once and runs units through it; each unit re-points the same UnitState box, so the tools are never rebuilt. executePiPatch and executePiSnapshot are now thin callers of it (through patchSessionInput, which is the one place a unit's prompt, snapshot and bounds are built), so the single-unit path and a chain are the same code rather than two descriptions of the same task. Fusion's claim is a delta, so PiRun separates the two: tokens/cacheRead/cacheWrite are the unit's own spend and sessionTokens/sessionCacheRead/sessionCacheWrite the session's cumulative totals. A single-unit runner reports them equal, so every existing caller reading tokens is unchanged. Two facts are recorded in code where they would otherwise be discovered by a paid run: the startup term is still unmeasured (fusion's cost line says 'unmeasured' until the driver reports a real session), and a chain's artifact tool takes a permissive conclusion kind because a session's surface is fixed before any unit is known (artifactEnvelope still refuses an invented kind). Checks: npx tsc --noEmit -p tsconfig.json clean for the file; npm run lint clean.
…chanism, step 3) The driver could ask for a session but no live worker could honour one - executePiPatch created a session per call - so a spec declaring fusion was refused by name and the fused arm could not be run at all. piSessionWorker holds one Pi session runner per PlanSession id and re-points it per unit, and --session-runner selects it; a spec's fusion block is what turns the session on. A spec file's fusion block was also silently dropped by specFrom: a spec asking for fusion ran as the control arm and reported sessions: [] without saying why. It is now copied into the run, and a test asserts both readings differ. First live evidence (deepseek-v4-flash, 2 units, --slots 1): one session [alpha, summary], both units accepted, 21093 tokens, 9.6 s. Two costs were measured rather than assumed and are part of the arms' comparison: a chain's tool surface is the union of its units' capabilities (a session's surface is fixed at creation), so a unit can be offered a tool that refuses by name and spend a turn on it; and the run only forms a chain when a unit's own limits are wide enough for that. Offline checks: plan-driver suite 16/16; npm run lint; npm run check; docs:check 253 files 0 errors; tsc clean for the changed files.
…trol Fusion's live half was refused by name because no mechanism held a session across units. It now does, so the arm was run: same spec, same envelope, --slots 1, one model, three reps per bound, and fusion.unitsPerSession the only difference (1 = the control, 2 = fused). Result: fused ran one session of two units where the control ran two sessions of one; every unit was accepted in all six runs, so the wall-time comparison is admissible. Median 11 048 ms against 12 948 ms (fused ~1.9 s faster, consistently, ~15 % of the unfused wall) and 22 533 against 22 498 tokens - no token saving, because the second unit's ~8 % saving is cancelled by the first unit costing ~0.7k more: a chain's tool surface is the union of its units' capabilities, a session's surface being fixed at creation, so a unit can spend a turn on a tool that refuses by name. The startup term the cost model reported as 'unmeasured' is now priced: one fused session removes exactly one startup, so it is ~1.9 s at this model and plan size. The ledger's F4 row and the F row count follow, and the pilot record gains the D arm with its sample-size caveat. Offline checks: docs:check 253 files 0 errors; plan-driver suite 16/16.
… note (F5)
The design's speculation is a guess about one declared, finite-valued fact, with three outcomes and bounds on the first experiment. Both are now the shared module's business, beside the fusion conditions: SpeculationAssumption is the design's assumptions=[{predicateId, version, expected}] as a declaration the candidate binds to (the summary never discovers for itself that the guess was false), isBoundedSpeculation refuses a candidate that guesses several facts at once or has already prepared a successor or an irreversible operation from the guess, and speculationOutcome reads authoritative evidence into publish / wait / discard. Asking for the outcome of a candidate that is not the bounded shape throws, so 'not this shape' is never folded into the three.
Two sentences that were prose are now rules a caller has to obey. A missing or unattested reading, or evidence about another version, waits - it is not permission to publish. And a contradicted guess returns sessionReusable: false, which is the design's '失效会话不能复用到真实路径': the real path may not take an answer from a model that has already been told the guess. The last test joins this half to fusion's condition 5, so an invalidated branch is not a legal predecessor either.
Tests: tests/integration/ooo-speculation.test.ts (9 cases). Mutants: 5 named, and the target now registers that suite, so the same sweep went from 17 to 22 mutants - 22 of 22 caught, targets restored byte-identically. That first sweep run is worth recording: the four outcome mutants survived because the new suite was not in the target's list, which is a check that looked like evidence and was not - the harness said 'suite passed: true' rather than anything about the rule.
Ledger: F5 row added, and the F count now names what is still unrun - the E arm's instrument (who decides the guessed fact, and the re-run under a new ticket), not the lifecycle.
Checks: agent:verify green (docs:check, glossary:check, verify:static, test:product, check, build, agent:context:check all blocking and passing); mutation:teeth 22 of 22 on the target.
…run did not support The E arm asks what bounded speculation costs: one declared fact - whether this round needs the unit at all - one candidate prepared ahead of it, and the shared layer's own three outcomes deciding its fate. evals/ooo-execution/speculation-pilot.ts builds that, with the design's own guards: every attempt under a fresh ticket, the fact decided by the host, and a published candidate still verified by the host with the unit's own frozen check, because that check is the quality term and nothing may be claimed without it. First paid run (deepseek-v4-flash, 6 units, ~43k tokens): baseline 13 544 tokens / 10 358 ms post-fact against speculation 17 151 tokens / 186 ms post-fact when the fact held, and 12 543 tokens of pure waste when it did not. No speedup is claimed: the quality term was false in all four verified candidates - each submitted patch failed its own check - so the design's rule forbids reporting the latency shape as a gain, and the record says exactly that. Two defects came out of the run. The extension accepted a submission it should not have (keys digest, kind, conclusion, summary, evidence, citations, no files) and patchCandidate then refused it as 'invalid patch structure', so the adapter can admit an artifact the board can never accept. And the instrument deleted the failed candidate tree it needed as evidence, which is why the first run could not say why every check failed; it keeps the tree now and the control rows carry the check's own output. Checks: lsp_diagnostics clean (evals has no tsconfig, so that is the type gate there - tsc on tsconfig.json is silent about this tree, which is how a wrong runner argument reached the first run); eslint clean; prettier clean; docs:check 253 files, 0 errors; npm run check green.
…he next spend Two paid runs were reported and their evidence was not: the D arm's six reports, the E arm's eight, and the smoke runs all lived in .temp/, which git ignores. A number in a record is a claim about bytes nobody can look at any more. The samples are now in docs/experiments/execution/archive/ooo-arms-2026-09-19/ (the D arm's own specs included, so the two arms differ only in fusion.unitsPerSession), with a README saying which entry point produced each file. This is the second time on this branch: archive/ooo-continuation-2026-09-14/README.md was written for the same failure mode after row G7, and the lesson did not survive the sessions in between. So the plan that now goes with every remaining step - ooo-arm-plan-2026-09-19.md - states the rule first: a step may run only once its data list is fixed (which fields, where they are written, what is read off them), evidence goes into a tracked archive directory before the claim that needs it, no run deletes its own evidence, and .temp/ holds working copies only. The four steps it plans, each with its data, analysis and decisive condition: P1 diagnose why every published candidate failed its own check (offline, one unit) by a three-way check of stub, model candidate and the fixture's known-good canned answer, with the artifact bytes and check output stored this time; P2 the E arm's economics (paid, <=150k) with latency, waste and quality kept apart and a break-even hit rate; P3 make the extension's envelope reader as strict as the host's, pinned by a matrix test and a mutant; P4 mechanise retention in docs:check, which is a convention change and therefore its own decision. Checks: docs:check 255 files, 0 errors.
…with the evidence kept The E arm's first run reported quality failures nobody could explain because it kept no bytes. It now reads an artifact by its kind, stores every artifact, candidate tree and check output in a marked scratch directory (CLEANABLE-scratch-<date>.md), and copies the run into the tracked archive when a record quotes it. The harness itself was validated before any quality term was believed: the frozen stub fails, the fixture's canned answer passes and a wrong answer fails, for both units. What that found corrects the earlier claim. artifactEnvelope builds two legitimate shapes - a patch, and a conclusion carrying kind/conclusion/summary/evidence/citations - and the first harness fed every artifact to patchCandidate, which reads patches only. Eight of nine attempts answered with a conclusion (legitimate for a task whose rule admits one, no files, so this unit's check cannot pass and the board would refuse it); the one patch attempt failed on a real mistake, writing rows where the frozen interface requires lines. There was no envelope defect, and the archive README, the arms record and the ledger now say so. The economics, 3 reps per condition, 9 paid units, ~62k tokens (deepseek-v4-flash): fact holds - baseline 18 183 tokens over 19 200 ms of post-fact work against speculation 18 602 tokens and 175 ms of post-fact verification; fact absent - speculation spends 20 332 tokens on nothing. So the mechanism does what the design says and the cost is real, but no gain is realised: the prepared candidate was publishable in 0 of 3 holding reps, which makes admissibility, not speed, the binding constraint. Checks: docs:check 256 files, 0 errors; eslint clean; plan-driver suite 16/16.
The E arm's first harness fed a conclusion artifact to the patch reader and reported the reader's complaint as the candidate's quality. The rule that was missing has one home now: a conclusion is a legitimate artifact for a task whose rule admits one, and the patch reader refuses it by name rather than reading half of it. 14 cases in the suite, checked by node --test. Not done, and said so in the plan: a mutant for the host reader. src/integration/ooo-patch.ts is not a mutation target, so that is a sweep of its own rather than a line in the plan.
… fails it The arms need a cost model rather than a declared bound, and the design names autodiff as the reuse target for cost or action scoring, so the terms were fitted to the 19 paid runs with src/lab/autodiff.ts: one stacked design-matrix matmul (not a loop of per-run subgraphs), no compiled tape for a one-shot fit, standardised features, plain gradient descent. It is not a model. Leave-one-out residuals run to -15 955 and +11 805 tokens against a mean of 11 184, and the session-startup term fitted out as 0 ms where the D arm measured ~1 900 ms directly - which says the term is not identifiable from these runs: the D arm holds units at 2, so units is collinear with the intercept and there are three runs per level. So the next step is a design matrix, not a better optimiser, and its cheap half is offline: per-session unit counts of 1, 2 and 3 through canned workers so units varies, plus the features that can actually carry signal (the union tool surface's own token count, turns, snapshot size, per-unit tokens), re-fitted with the same call and reported again as leave-one-out residuals. A term enters the planner only when its interval excludes zero. Legality stays a predicate over declared facts; no fitted number may widen it. The fitting script is kept as scratch under .temp/ rather than committed as an entry point that produced a negative result; the plan carries the numbers, the diagnosis and the next step. Checks: docs:check 256 files, 0 errors.
Fusion had one knob, a bound on units per session, and a bound is not a decision: it does not choose among legal successors and it cannot say whether fusing is worth taking. This splits the question in two and keeps the cheap half free. Online is one move about the current session - admit the next legal successor or close with the condition that closed it (src/integration/ooo-fusion-plan.ts). A fused session is irreversible, so no move rewrites a committed one; the default is to continue (repair-first, the documented middle of the repair-versus-replan trade-off); the same plan and facts yield the same move, ties broken by plan order, because otherwise two runs of one plan are not comparable. The cost model stays read-only online, the way a query optimizer re-plans against collected statistics and never re-collects them. Offline computes a ceiling from an optimistic projection fed to the same sharedSessionLegal, so the five conditions keep one home: a floor from the minimum chain cover (Dilworth: units minus a maximum bipartite matching over the relation's closure) and a feasible bound from greedy list scheduling at a declared cap. Two restrictions make that graph a graph: a chain is a linear extension, and it follows plan order - the relation alone is not a partial order, since two independent units with compatible declarations may each follow the other. evals/ooo-execution/fusion-ceiling.ts is the measurement, and it reproduces the one paid run: for the D arm's own plan it predicts 1 900 ms saved at cap 2, and the arm measured 12 948 ms against 11 048 ms. On fixtures/report/fine.spec.json it reports a floor of one session, 3.8 s saved at cap 2 and 5.7 s at cap 4, and that cap 3 buys nothing over cap 2 on that shape. Not modelled, and named so a chain is never assumed free: the union tool surface's extra first-unit turn and the context a longer chain resends. Also recorded: the autodiff cost-model fit is a negative result (the blocker is a design matrix, not an optimizer), and the off-the-shelf shape this follows - stage-barrier re-planning, ready-set scheduling, plan baselines, deterministic replay, dwell time, Dilworth - with the two claims the search corrected. Checks: docs:check 259 files 0 errors, glossary:check, check, verify:static, test:product (1479 passing), plus 11 new cases for the plan module.
…he move Two things the design left open, closed. (a) The driver no longer keeps a second copy of the move. plan-driver.ts's nextInChain now calls nextSessionMove, so "which unit may continue this session" has one home, with the reason a session closes carried by name instead of implied by two early returns. Behaviour is unchanged: the 16 plan-driver cases pass, and the rule's own 11 cases pin the same shape. (b) The ceiling's prediction for a four-unit plan was tested live, on the plan it was computed for: fixtures/pipeline/fine.spec.json, three independent units and one that joins them, --slots 1, two reps per bound, the spec's canned answers stripped so the units really run. bound 1: 4 sessions, 24 258 / 22 280 ms, 45 158 / 41 673 tokens bound 4: 1 session, 19 431 / 15 515 ms, 83 865 / 55 143 tokens Medians save 5 796 ms against a predicted 5 700 ms - so the ceiling is now calibrated against two independent measurements (the D arm's 1 900 ms for a single avoided session, and this 5 796 ms for three). The second half is the part the design could only name before: tokens go up, never down, by 1.3x to 1.9x, because a chain carries its context forward - measured per unit in the cheaper rep as 11.4 k, 10.1 k, 14.5 k, 19.1 k, monotone. A cap is therefore not "5.7 s saved" but "about 5.8 s saved for 1.3-1.9x the tokens", and the ceiling is a wall-clock ceiling that must be read next to that cost. Runs, specs and the aggregate are archived under docs/experiments/execution/archive/ooo-arms-2026-09-19/cap4-darm/ , and the archive README names the entry point. Checks: docs:check 259 files 0 errors, glossary:check, check (tsc), plan-driver 16/16, the plan module's 11 cases.
…p 2, not cap 4 The cap-4 run priced fusion at 1.3-1.9x the tokens, which is a number nobody can act on: a token count is not a cost, and a chain carries its context forward, so most of what a later unit sends should be served from cache. WorkerMetrics already had cacheRead and cacheWrite; the two live pi workers simply never filled them, and the report had nowhere to put them. They are now recorded per unit and summed per run, which is what makes the next sentence measurable rather than arguable. Re-run at bounds 1, 2 and 4, two reps each, same four-unit plan, cache recorded: bound 1: 4 sessions, 26 620 ms, 45 685 tokens, 37 760 cache read bound 2: 2 sessions, 17 867 ms, 39 750 tokens, 31 808 cache read bound 4: 1 session, 16 645 ms, 55 286 tokens, 46 144 cache read Three findings, one of them a correction of this work's own earlier claim: - Every arm spends 80-84 % of its tokens on cache reads, and the tokens that are not - the part priced like fresh input - are nearly flat: 7 925, 7 942, 9 142. Fusing four units costs about 15 % more fresh input, not 1.3-1.9x. That earlier multiplier came from unpaired medians; it does not survive the cache-aware reading, and the design and both decision records now say so. - Fusion saves more wall clock than the constant predicted: cap 1 to cap 2 saves 8 753 ms against 3 800 ms predicted from the D arm's 1 900 ms. The startup term is plan-dependent, so the ceiling's honest primary quantity is *sessions avoided* - exact and model-free - with milliseconds as an estimate that names its constant. - Cap 2 is the knee: it takes 8 753 ms of the 9 975 ms available at the fewest tokens of the three, while cap 4 buys the last 1 222 ms for 39 % more. The policy worth declaring is two units per session. Two type errors in the driver were fixed while passing through - a `readonly string[]` that was pushed to, and a report variable typed to one half of the union it is assigned - which `tsc` never saw because `evals/**` is outside its program; `lsp_diagnostics` is the only gate there. Runs, specs and the aggregate are archived under docs/experiments/execution/archive/ooo-arms-2026-09-19/cap-cache/ , and the README names the entry point and records that the earlier cap4-darm reading is superseded. Checks: docs:check 259 files 0 errors, glossary:check, check (tsc), lsp_diagnostics 0 on the driver, plan-driver 16/16.
An archived run directory keeps the candidate tree the run was judged on, so its .test.ts files are evidence rather than suites. Adding docs/experiments/execution/archive/ to ACKNOWLEDGED_ROOTS, with the reason next to the list, stops a required job from failing on a false positive. Goal state is now ignored too: a scratch plan is not repository content. Record: docs/decisions/implemented/2026-09-19-acknowledge-archived-run-directories.md
Read the seven records still in docs/decisions/proposed/. Six have their acceptance criteria met in the code and in the measurements, so they move to docs/decisions/implemented with the evidence that justifies closing them: board governance addressing, OoO bootstrap, speculation gating (measured at no realised gain), task-unit semantics, the fusion/speculation pilot (its recorded blocker is gone - the live worker holds one session), and the fusion session mechanism (one runner, one tool surface, the smoke's shared session). The proposal-era `## Acceptance criteria` heading becomes `## Consequences`, which the lifecycle check requires, and links that pointed at the old paths now point at implemented/. `2026-08-29-session-active-graph-runtime.md` stays proposed: its remaining criteria (automatic task/branch lifecycle, combined budget accounting, shared disclosure ledger, TTL reasoning artifacts) are not met. docs: 261 files, 0 errors, 0 warnings; decisions: 49 implemented, 15 with open items (was 43).
The pointer in src/integration/task-semantics.ts names the record that moved from proposed/ to implemented/.
… they are `ooo-check.ts` (38 lines) held one identity type, `CheckTicket`; `ooo-verifier.ts` spawns the external check process and returns its result. Neither name said identity or runner, so they become `check-ticket.ts` and `check-runner.ts`. The other six `ooo-*.ts` files keep their prefix: `ooo` is this project's name for the scheduling model it implements, and it is load-bearing in more than a hundred documents - including the frozen run archives, which are evidence rather than drafts. Six code files updated, no documentation reference to repair (there were none), and no public surface changed: neither name appeared in a CLI command or a tool name. While routing the layer the type check surfaced a false-green assertion in tests/integration/ooo-verification.test.ts: `expectedRename(...)` was never imported, so `assert.throws` caught the ReferenceError and passed for the wrong reason. It now calls `expectedRenameOf`, which throws "rename baseline is incompatible with the fixed oracle" - the reason the assertion exists. Decision: docs/decisions/implemented/2026-09-19-name-the-check-and-runner.md
`src/integration` was declared in the `memory-runtime` capability but claimed by no route, so every edit in it answered "no verification route matched: src/integration/..." - no owners, no tests, no checks. Two routes now split the layer by owner document: `agent-surface` (design.md) and `ooo-execution` (ooo-execution-bootstrap.md, task-unit-semantics.md, ooo-fusion-planning.md), both verifying [check, test:product, build] and keeping the default shared checks. They list their files one by one because `matches()` in tools/repo-context.ts reads the first `*` in a pattern as a directory prefix: `dir/**` and exact paths match, while a mid-name pattern such as `src/integration/ooo-*.ts` would match nothing at all while still looking declared. A list rots silently, so tests/tools/repo-context .test.ts asserts that the two routes' paths plus the four knowingly unrouted enrichment files are exactly the directory listing - verified by adding a file and watching that test fail, then removing it. Decision: docs/decisions/implemented/2026-09-19-route-the-integration-layer.md
Both sides added the same two paths, and both sides are the same work, so the merge keeps this branch's version of each, which is the newer of the two: - the archived continuation README points at design/task-unit-semantics-obligations.md, the ledger's home after 0d3dc09 moved it (#65); the incoming side still named the old path - evals/ooo-execution/live-continuation.ts keeps the declared throws helper from 7d39f6f; the incoming side inlined the same cases Nothing else conflicted: the rest of #69 was already present on this branch.
The four retrieval-index enrichment files - leaf-summarizer, node-summarizer, summary-drain and the OpenAI-compatible completion client - are neither the Agent Surface nor the execution orchestra: an external LLM writes index text that the LLM-free store persists. They now have a retrieval-enrichment route owned by docs/design/design.md and docs/design/tiered-disclosure-design.md, with the tests that cover them. The guard test's list of knowingly unrouted files is now empty, so the assertion is unchanged - the routes' paths plus that list are exactly the directory listing - and any new file under src/integration must be claimed. Verified: agent:context selects ooo-execution, agent-surface and retrieval-enrichment for a file from each bucket; tests/tools/repo-context.test.ts 22/22; npm run agent:context:check and npm run docs:check clean.
wefio
added a commit
that referenced
this pull request
Sep 20, 2026
…ks read data (#70) * docs(decisions): a unit's session comes from the board Fresh slice on main now that #61 is merged (branch feat/ooo-session-continuation-surface). The fusion mechanism is landed but nothing in the product decides to reuse a session: the only caller is the live arm, and it keys its runners off the spec's own session id, which is an eval artifact. This proposes the product path shape with no new tool: resolution and use both go through the board, which already records the session that wrote each entry (source_session_id), already keys delivery receipts on (entry_id, session_id), and already fences managed writes to a registered run. Legality is unchanged: sharedSessionLegal stays the only rule, no bound, no switch. Status is proposed: the resolution helper, the runner keyed by board session, the tests, the registry row and the field trial follow on this branch. * docs(decisions): the session identity is already there, so nothing resolves it Correcting this record's own wording: it is not a resolution module. The identity already exists in three places nothing has to derive - the caller's own session id (ctx.sessionManager.getSessionId()), the entry's source_session_id, and the managed-write fence on a registered run - so the change is keying the runner by that identity and recording the move on the board, not adding a helper or a column. The eval arm's sessions: [[...]] and session: stay in the spec, where a measurement artifact belongs. * docs(decisions): the continuation is an in-band JSON block read by a deterministic pass Two shape corrections to this proposal. The declaration is not a tool and not a schema change: like the memory=<id> pointers a board entry already carries, an entry may carry one fenced nmg: block whose body is JSON - the parameters of the call that wrote it - and a reader that does not understand it reads the prose unchanged, with rendering the block optional. And the thing that turns those blocks into a session grouping plus the next move is a compiler-like pass over the board: prose through untouched, no model, recomputed at each boundary rather than cached, living beside the board on the daemon side so CLI, extension and driver see one layout instead of three. * docs(decisions): the declaration is a parameter and the result is a run fact Replacing the fenced-block-in-the-entry idea with the simpler shape: the call that already exists carries one dedicated JSON field, so nothing is parsed out of prose and no text convention is invented; the computed decision is written as a run fact in the table that already exists for it (task_run_facts, today holding entry-bound and run-cancelled), which gives it a sequence number and lets the existing as-of-sequence read replay it. The pass stays deterministic and model-free, and showing the decision anywhere is optional. * feat(execution): a session move has a name and a home in the run log First step of the board-side continuation: src/integration/ooo-session-facts.ts owns one fact kind (session-move) and the one write that appends it, declared next to that write the way task-coordinator.ts declares its own kinds - one home for the vocabulary, because the write and the read must agree and neither may guess the string. It carries the move nextSessionMove already computes (admit a unit, or close the session naming the condition) as the fact's JSON payload, and reads it back defensively: a payload that is absent or is not a move this module wrote is skipped rather than trusted. Nothing here decides - the decision stays nextSessionMove's pure function of the plan and the facts. The new file is claimed by the ooo-execution route, which the guard test in tests/tools/repo-context.test.ts requires (it lists files one by one, so an unclaimed file fails it). Verified: agent:context selects ooo-execution for the new file, the guard test passes 22/22, npm run agent:context:check is valid, lsp_diagnostics reports 0 diagnostics. * test(execution): the session move is recorded once and read back as itself Four checks on the record src/integration/ooo-session-facts.ts keeps: an admit reads back as the same admit, a close carries the condition that closed the session through the log and back, the same move written twice is one fact (the second write reports recorded:false and the first sequence, because the store keys a fact on run, kind, task, attempt), and a log with no move reads as empty while a payload under this module's kind that this module did not write (an admit without a unit) is skipped rather than trusted. Teeth, named mutant: changing parseSessionMove to accept an admit without a string unit fails exactly the fourth test and leaves the other three green - the check is not satisfied by any payload of the right shape. Restoring the original leaves all four green and the source byte-identical. * feat(execution): a boundary decides and records its move in one call decideSessionMove reads the run's facts, computes the move, and appends it under this module's kind. It decides nothing of its own: the move is nextSessionMove's, and its one added input is the fact that can end a session early - a cancelled run admits nothing further whatever the plan says - which is re-read from the run's log rather than taken from the caller, the way the managed-write fence reads its two refusals. Three more checks ride on that: the decision and the log agree (the recorded move is the move that was returned), a session that cannot continue closes by name and never by guessing (bound reached, or nothing legal on offer), and a cancelled run admits nothing. One semantics pinned by a test rather than left implicit: a move belongs to a boundary, and a boundary is a unit and an attempt - the same fact identity the store keys on. So one boundary is one move however often the caller asks, and the second answer is the first one rather than a second fact; a caller that decides again after the facts changed must say it is a new attempt, or the second decision is not recorded and the log stops describing what the code did. * feat(execution): the session key is a function of the board, not of the harness Grouping and reuse are the mechanism, so the key one board session's runner is held under is computed in the shared layer from what the board already holds - the run and the session that owns its entries - and from nothing else. Two callers that see the same run and same session therefore agree without talking to each other, and a replay computes the key again rather than remembering it. A missing part refuses instead of composing a key that could collide with a real one. This is the first piece of moving the session mechanism out of the harness: today createPiSessionRunner is defined in .pi/extensions/nmg/ooo-execution.ts and the eval drivers import it from there, so measurement borrows its mechanism from one harness. The pure parts (unit state, the completion policy, text-to-artifact, and this key) belong in src/integration/; only the code that actually opens a pi session belongs in an adapter. * refactor(execution): the session mechanism moves to the shared layer, the harness becomes an adapter Which unit runs under which session, what a unit's state is, the completion policy, the artifact contract, the snapshot/text conversions and the PiSessionRunner contract are all decided the same way whoever is running, so they now live in src/integration/ooo-session-mechanism.ts. What is left in .pi/extensions/nmg/ooo-execution.ts is the adapter: it is where createAgentSession, ModelRuntime, defineTool and the tools built from them live, and it imports the mechanism. 797 lines became 491 adapter plus 449 shared. The split is mechanical, not editorial: a block belongs to the adapter when it mentions an imported pi or typebox name outside a comment, and that mark propagates through references - a block that calls a block which talks to pi also talks to pi. The report refused the split when a shared block reached an adapter block, so nothing here compiles only by accident of the move. Consumers were repointed by hand: the verification test takes the artifact contract and the completion policy from the shared layer and resourceLoader from the adapter, the speculation pilot takes patchSessionInput from the shared layer and createPiSessionRunner from the adapter, and live-pi takes the PiRun type from the shared layer. The eval drivers still reach the adapter for the model call, which is what an adapter is for, but they no longer borrow the mechanism from one harness. Verified: npm run check clean, the affected suites 30/30, the repository-context guard accepts the new file and npm run agent:context:check is valid. The hidden-features registry row now says which half is shared and which half is the adapter. * feat(execution): one call opens a unit's session - the key it runs under, and the move that put it there openUnitSession is the caller's side of the mechanism: it computes the key the unit's runner is held under from the run and the session identity the caller already holds, decides the boundary through decideSessionMove, and returns both. A caller that is not the extension needs no other session surface: it supplies the run, its own session, and where that session stands, and the board decides the rest and records it under session-move. The test pins the three consequences that matter to a caller: the same run and session give the same key and the first answer stands (a second ask reports recorded:false), a different session is a different runner, and both decisions are in the log with their own sequences. * test(execution): a move belongs to the boundary, not to the session that asked The first version of this test asserted that two sessions deciding the same unit and attempt leave two moves. They do not: a fact is keyed on (run, kind, task, attempt), so the session that asked is not part of the move's identity. That is now what the test says - a second session asking about the same boundary is the same fact and gets the first answer, and a caller that would decide the boundary again must say it is a new attempt. The earlier commit carries the assertion that was wrong; this one corrects it, and the check was green only after this change. * test(execution): no file may ask the adapter for a name the shared mechanism owns The move put the mechanism in src/integration/ooo-session-mechanism.ts and left the pi file as an adapter. Nothing checked the rule: evals/** has no tsconfig coverage, so tsc never reads the drivers, and lint does not know which module owns which name. It broke exactly there - plan-driver.ts still got patchSessionInput from the adapter, the live fused run failed with "patchSessionInput is not a function", and only the run itself said so. The check reads the names the shared module exports and looks for them in every import a driver makes of the adapter, in all three forms a driver uses: static named imports, awaited dynamic imports with destructuring, and the type-position import("path").Name. It refuses to pass on an empty export list, which would be a guard that agrees with anything. plan-driver.ts now takes patchSessionInput and the PiSessionRunner type from the shared layer and keeps only createPiSessionRunner from the adapter. Verified: the guard fails on the exact two names before the fix and passes after it; 41/41 in the four affected suites. * docs(experiments): the fusion trial's control arm, and the arm that could not be measured Two arms one declaration apart over the pipeline fixture, run live. The control arm passed: 4/4 units accepted, the composed parent accepted, 24849 ms wall, 30000 tokens, 17408 cache read, 0 failures. The fusion arm produced no measurement at all: it fails on the first unit with stopReason=error, turns=4, reads=1 and no artifact, on the only path it takes - piSessionWorker with --session-runner, which creates the first runner with chain: true. The control arm's worker goes through executePiPatch (chain: false) and works with the same provider and model. The same spec run at 0296466, the commit before the session mechanism moved to the shared layer, fails identically, so the failure predates the move and is not caused by it. The specs, both results and that pre-move run are in this directory, along with the script that builds the two specs and refuses a pair that differs in more than the arm's declaration. * fix(execution): a fused session tells its units which conclusions are admitted A single attempt gets a per-unit literal union for the artifact's conclusion, so the model cannot answer with a kind that unit does not admit. A fused session cannot: the tool surface is fixed when the session is created, so the shared envelope loosens the conclusion to a plain string and refuses an invented kind afterwards. patchPrompt deliberately does not repeat the schema in the prompt, which is right when the schema carries the rule and wrong the moment it stops carrying it - and in a chain it stopped. The model then guessed, the envelope refused, and the retry spent the turn budget: the live fused arm was aborted at turn 4 of a declared 3, which is why it produced no measurement at all. patchSessionInput now takes looseConclusion and, when it is set, names the admitted kinds in the prompt, one sentence appended exactly where the check and pushback notes are. The runner refuses a session and an input whose flags disagree, before it creates a runtime, because the cost of the disagreement is a wrong guess per unit paid silently. The bounded-contract error now prints turns and reads against their limits, which is what made this failure hard to read: the abort said stopReason=error and nothing about which budget it had passed. Verified offline: the strict input does not name the kinds, the loosened one does and says a kind outside the list is refused, and both disagreement directions are refused. npm run check clean and test:product 1507/1507. * fix(execution): a fused session says which conclusions and which tools its unit may use Two rules the fixed surface pushes out of the schema, and both cost the live fused arm its turn budget. First: a chain registers the tools of every unit it will run, so a unit with no check is still shown run_check - the model called it, was told the unit has no check, and spent a turn finding out. Second, and this is what actually ended the attempt: the envelope refuses a submission that carries files and a conclusion at once, and no schema can express that exclusivity, so the prompt is the only place it can be said. The trace that named it, from the instrument added here: calls=read_snapshot,run_check,submit_artifact, artifact=no artifact, last refusal: a patch carries files only; it cannot also carry a conclusion. patchSessionInput now appends, for a loosened surface only, which tools this unit may call and that the two answer channels are exclusive, alongside the admitted conclusion kinds. The strict path is byte-identical to patchPrompt plus its existing notes, which is asserted in the test, so the control arm already recorded still describes this code. The instrument is part of the fix: the box records the tools a unit called and the envelope's last refusal, and the bounded-contract error prints turns and reads against their limits. Before that, an aborted unit said stopReason=error and nothing else, and finding out why took three paid runs. Verified live: the fused arm now completes, 4/4 units accepted with the composed parent accepted. npm run check clean, test:product 1508/1508. * docs(experiments): the fused arm measured - same quality, 15% more tokens, 35% less wall time Both arms accepted all four units and the composed parent. Fusion spent 34601 tokens against the control's 30000 (1.15x) and 24064 cache reads against 17408 (1.38x), while wall time fell from 24849 ms to 16054 ms (0.65x) over two fused sessions instead of four fresh ones. Every per-unit delta is positive, so on this fixture a continued session does not spend less on its next unit; the warm context is a longer context. The README also records what it cost to measure: the fused arm first died on its first unit at turn 4 of a declared 3, and the two defects behind that - a chain showing a unit a check tool it has no check for, and an envelope rule about the two answer channels being exclusive that no schema can express - were only visible after the error line was made to print turns and reads against their limits, the tools called, and the last refusal. One rep per arm, so this is a pair and not a rate. * fix(execution): a task-level cancellation closes that task, not the whole run managedWriteRefusal matched the cancellation fact by kind alone, so cancelling one task of a run refused every other task's lifecycle writes in it - cancelRun has taken a taskId since it was written, and the fence ignored it. The same predicate was copied into the session decision, where a cancel for one unit closed another unit's session. The rule now has one home: taskCancellation(store, runId, taskId) returns the cancellation that applies to a task, with the run-level one carrying the schema's empty task id and applying to everything. managedWriteRefusal takes the task it is asked about - coordinatedBoardWrite resolves it from the entry's own binding, bindRunEntry already has it, and freezeRunPlan asks about the run, which is what a plan freeze is - and the session decision asks about the unit's task. A caller that names no task now hears only about a run-level cancellation. Verified: cancelling T1 refuses T1's entry and leaves T2 claimable by the coordinated path, with the reason line naming the task that was cancelled; a run-level cancellation still closes both; and the session test shows another task's cancellation no longer ending this unit's session. npm run check clean, test:product 1510/1510. * docs(execution): the fusion claims say what their two reps actually carry Three readings had outrun their samples. The D arm's 1.9 s wall saving was attributed to session startup and called "wider than either arm's own spread". The numbers deny both: the gap between the medians is 1.9 s and each arm's own spread is 2.2 s, so the difference is narrower than the noise it claims to exceed and does not separate a startup term from ordinary run-to-run model time. The term stays unmeasured until the cap experiment, which finds it plan-dependent. The cap experiment's knee at two units per session was written as a declared policy and restated as one in the proposal that depends on it. Every cell is two runs, and fewer sessions is not the same quantity as a shorter parent task - over one slot, fusing removes parallelism. It is a hypothesis for the A-D comparison now, in both languages. tokens - cache read was called fresh input. Output tokens are billed too and no subtraction of cache reads removes them, so it is a lower bound; the per-unit "real" second-unit saving and the mechanism that explained it are marked as directions, and the mechanism half that was repaired today is cross-referenced so a re-run is not expected to reproduce it. * docs(execution): an A-D plan built on the samples that exist, and two corrected readings tokens - cache read was called a lower bound on billed input. It is not: tokens counts input and output together, so the remainder is the tokens not served from cache - uncached input plus every output token - and the reports do not say whether the cache figure is nested inside the total. Pricing needs the three recorded apart, which this experiment did not do. Corrected in the fusion planning document, the decision record that quotes it and the proposal that leans on it, and the archive README now carries the same note for the column it introduced. A spread wider than a difference was also read as the difference not existing. It says the sample cannot resolve the effect; the D arm's two-rep spreads (2.2 s) exceeding its 1.9 s median gap is a reason to run more reps, not evidence that fusion saves nothing. P6 of the arm plan asks which cells of the A-D comparison already exist and finds the answer in the archived specs: the cap experiment is a controlled same-task comparison, its three specs differing by exactly one field (fusion.unitsPerSession), one instrument, two reps per cell, with per-run values and spreads published there for the first time - the cap1-to-cap2 saving (8.2-9.1 s) is an order of magnitude wider than the noise inside the cells, which is why the 2-unit D arm's weak result and this experiment's strong one are both true. It names the cells that are missing, the hypothesis each would distinguish, their measured cost (85 k for the recommended pair of extra reps), and the stop rule. It also records what the archive does not hold: the A, B and C arms were not rescued, so for the coarse and slot arms there is a summary, not a sample - the archive README's 'every sample' claim is corrected in a dated note. * docs(execution): the ledger says which evidence base each proof lives on The reviewer's point was that the research instrument's tables sit on a different path from the product's run facility, and the answer is not to join them - it is to say what each side's tests prove. The ledger now carries that boundary. Two bases, no shared storage, no transferred proofs. The product path (the store's task_runs/task_run_facts, the coordinator's fence and cancellation rule, the session mechanism, the harness adapter) is proven by ten named files. The research instrument (BoardAdmission and the ooo_probe_* family with the task-semantics modules, driven by evals/ooo-execution) is proven by the twenty-seven test files whose static imports reach ooo-board.ts: sixteen under tests/integration, eleven under evals. Five of those sixteen are named in the section because their file names read like the product's own board - ooo-managed-fence, ooo-transition-atomicity, ooo-run-namespace, ooo-task-tables, ooo-read-paths-agree - and each constructs BoardAdmission itself. Reachability is a screen, not a verdict (a fixture builder or a type import reaches the same module while the assertion stays on product code), so the rule is stated as a rule: an OoO claim may cite only the product-side proofs, and the two rg lines that reproduce the screen are in the section. * measure(execution): a third rep on the cap cells - the wall saving survives, the token reading does not Bought to give the A-D cells a range instead of a two-point gap: one more rep each for bounds 1 and 2 of the four-unit fine plan, same instrument, same single variable (fusion.unitsPerSession). Spent 80 404 tokens: 33 941 for cap 1, 35 444 for cap 2, and 11 019 for one refused run kept as evidence. Wall clock: 25 270 / 26 186 / 27 053 ms at cap 1 against 17 427 / 17 735 / 17 998 ms at cap 2. The median saving is 8 451 ms against within-cell spreads of 1 783 and 571 ms, so it is about five times the larger spread and the saving is a rate, not one lucky pair. Everything the review asked for on this axis is now measured on one parent task with the same configuration. Tokens: the opposite. Cap 1's own token spread is 11 946, wider than its 7 789-token median gap to cap 2, and the cache-read share of tokens runs from 0.607 to 0.847 inside a single cell - so '80-84 % of tokens are cache reads' was a two-rep artefact and no token-direction claim survives at this rep count. Per-unit tokens across the thirty-two stored units span 7 105 to 19 163. Two facts from executing it. The command had to be guessed and one run was refused: without --session-runner the driver names the session it cannot continue, records it in incomplete and stops after the first unit, which is the guard working and is archived rather than deleted. And the fused cell's third rep ran with a newer chain prompt than its first two (the exclusivity and admitted-tools lines added earlier the same day), so those three reps are not one instrument version - the post-fix rep is the fastest and lowest-token of the three, which says the change did not hurt the cell, not that it helped it. aggregate-3rep.json recomputes every value from the stored reports; aggregate.json keeps the original two-rep reading. The fusion planning document, the decision record that quoted the two-rep medians, the ledger's F4 line and P6 of the arm plan all now say what the third rep settled and what it did not. * docs(execution): fusion's measured benefit is stated, its old readings are replaced, and the reps stay paired The review found the report still carrying three withdrawn readings, an aggregate whose columns could not be paired with the runs they came from, and a comparability claim that its own text denied. All of it is document work. The benefit is now stated where it is measured: the four-unit fine plan's cap 1 to cap 2 saves 8 451 ms against within-cell spreads of 1 783 and 571 ms, 4.2 s per avoided session, and the two-rep pair alone separates (26.2-27.1 s against 17.7-18.0 s) - so the effect is not something the third rep's newer prompt introduced, and the D arm's own weak sample is about that sample, not about fusion. That last sentence is also in the arms record now, so its D arm section cannot be read as 'fusion unproven'. The withdrawn readings are replaced, not annotated around: the front half of the fusion planning document no longer says the D arm measured ~1 900 ms of startup with tokens flat or that fusing costs about 15 % more fresh input, and the sentence that used the D arm's delta to estimate and then cited that same delta as the ceiling's credential is now labelled an arithmetic check, with the independent test beside it - the same tool predicts 3 800 ms on the fine plan and the runs measure 8 451, a factor-of-two disagreement that is the useful result. The implemented decision record keeps its old wording only inside a dated correction block, and the ledger's startup-term range is corrected to 3.2-4.2 s. Pairing: aggregate-3rep.json now carries one object per rep, with sorted arrays named as such and medians computed from the objects; both tables that quoted three sorted columns positionally are now one row per run. Provenance: the reports do not record an instrument commit, so the archive states what can be established from the outside - reps 1 and 2 at 06:54-06:56Z, before the chain-prompt fix dated 12:11:24Z; the third reps and the refused run at 12:32-12:33Z from a clean tree at 4b0ba09 - and P6 lists 'the driver writes its own commit and a prompt digest' as the free item that would remove the need for that prose. Two over-strong claims are gone with it: that the token columns settle nothing at any affordable rep count (three reps are not enough, and the reason is their spread) and that '1.2 s exceeds 1.9 s' meant anything beyond that sample. * feat(execution): a run records its own instrument and the provider's usage split The cap experiment's cost question could not be answered from its reports: they recorded a token total and cache reads, and a total cannot be taken apart again. The provider reports more, so the harness now sums input, output, cache read, cache write and the provider's own price, and the driver records them per unit beside the totals - a cost claim no longer has to be argued from arithmetic that cannot be done. Two things came with it. A unit's report carries a promptDigest of the input its session was given, and every run carries instrument.commit, read from git at run time, so two runs that agree on a spec can still be told apart by what they actually ran. That was the review's point that the version basis lived only in prose, and the cap reports from earlier the same day name no commit at all. Also fixed, both pre-existing: four unused imports that lint had been failing on since the harness-to-shared move and the chain-prompt fix - npm run check does not run eslint, so only verify:static saw them, and those two passes ran check alone; and one mutation anchor, stale since the bound comparison moved into nextSessionMove, now breaks the driver's hand-off of the declared bound instead, which is the same invariant pinned in a target that exists (11 of 11 caught, restored byte-identically). * measure(execution): the A-D cells on the plain path, and what the chain surface costs Reading the driver to plan these runs turned up what the earlier readings had missed: all three cap specs declare fusion, so every cap cell runs the chain surface - one session per slot, the loosened schema, the chain prompt - even at bound 1. cap 1 is therefore the fused arms' same-surface control, not the unfused B cell, and the granularity comparison had no plain-path arm at all. Three cells were run on the plain path with the same fixture, worker, envelope and parent check: A (coarse, 1 unit, slots 1) 9 726 ms / 9 018 tokens, B (fine, 4 units, slots 1) 21 885 ms / 29 962, and C (the same spec at 2 slots) 18 565 ms / 30 674. Every unit and every parent check accepted, slotsUsed as asked, 69 654 tokens for the three. They are the first reports that name their own commit and carry the usage split and a prompt digest; the totals decompose exactly (A: 2 377 + 1 521 + 5 120 = 9 018) and the provider's price comes with them. Two readings follow. Declaring fusion at a bound of one, which fuses nothing, costs 4 301 ms and 15 520 tokens more than the plain path for the same plan (26 186 / 45 482 against 21 885 / 29 962, most of the token difference cache reads), so the chain surface is not free. And fusion still wins on wall clock against both baselines: cap 2's 17 735 ms median is 4 150 ms below plain B and 8 451 ms below cap 1, while spending 7 731 tokens more than B. Pricing that difference is the one purchase left, because no cap report carries the split. * docs(execution): the arm programme gets one measurement plan, with an end The program was being run one purchase at a time, and every report ended by naming the next one, which is how a plan turns into a treadmill. What was missing was not a cell but the plan: which cells exist in total, what question each one answers, which decision that question informs, and the rule for comparing them. So P6 is now the plan, and it is the one home of the measurement rules - one declared field per pair, the grade a reading gets (single observation, pair, rate), the spread rule for calling a difference separated, same instrument or say so, cost from the provider rather than from tokens, pair aggregates per rep. The cells are enumerated with their questions, the deliberate non-cells are named with reasons, and the end state is written out: Q1, Q3 and Q4 answered, Q5 a hypothesis, Q6 and Q7 closed unmeasured with their reasons, and Q2/Q3's pair-grade confirmation priced as an option (about 60k) rather than left as a to-do. The tables are read from the stored reports by a script that refuses a missing report or one whose token total does not equal its four parts, and its computed reading is archived as matrix.json. The archive README and the design docs now point at the plan instead of restating its comparison rules. * measure(execution): the pair-grade reps land, and the plan's grades hold Bought the one cell pair the plan had priced, and only it: B's and C's second rep (30 178 and 30 472 tokens). Both accepted every unit and the parent check, C ran its two slots, and both reports name the commit they ran from. The readings move as the grade rule said they would. A vs B separates at single-observation grade (12 853 ms against a 6 940 ms threshold). B vs C separates at pair grade (3 628 ms against 2 776 ms) - and the second slot came out both faster and cheaper in this pair, C's median price 0.001826 against B's 0.002106, which the token column alone would have hidden (C spends 503 more tokens, on more cache reads and less fresh input). B vs cap1 separates by 41 ms, the narrowest reading in the matrix, and it is stated as marginal rather than rounded into confidence. cap2 vs cap4 does not separate at pair grade (1 090 ms against 1 142 ms), so the knee stays a hypothesis. D1 vs D2 still cannot resolve, at rate grade. With that, no cell of the plan is un-run: every question is answered at the grade its decision needs or closed with its reason on the record. The per-run tables, the pair table with its thresholds, and the stored matrix.json are all regenerated from the reports rather than edited by hand, and the old one-rep aggregate was replaced by the two-rep one. The plan also gains its rule 9: a measurement step that changes no code owes no gate - the clean tree and the recorded instrument commit are the guard - while editing the driver or the shared mechanism owes the route's blocking checks. * docs(decisions): when the chain path may be entered, and the session-identity note is accepted The arm programme closed its measurement phase, and what it had not decided is the rule that lets a run enter the chain path at all. Until now the only thing that entered it was a spec declaring fusion.unitsPerSession - a measurement artifact - plus a capability check inside the arms' driver. So the rule had no home in the product. The new record gives it one, and states the permission rather than the economics. Fusion is decided by the shared runtime; the adapter supplies session capability and decides nothing. Entry needs three conditions - the semantics allow it (sharedSessionLegal stays the only legality rule), a continuable task exists at the boundary, and the host supports session reuse - and a missing one means the run proceeds unfused rather than failing. The board carries claims, acceptance and facts; the decision and its move are a run fact, so they are replayable. Fusion may not relax correctness: each task is still checked on its own for permission, input version, cancellation and acceptance, and the parent still accepts jointly. What is deliberately not declared is economics. The record carries the programme's closing classification instead - observed (coarse fastest here, fine granularity costs, the second slot and fusion each recover part of it), replicated (bound 2 beats bound 1 at rate grade), undetermined (the chain path's own cost, the default bound, any general cost-against-benefit claim), closed (no further samples) - and the alternatives say why: the cap cells' token column did not survive three reps and their price cannot be recovered. The other half is a lifecycle move. "A unit's session comes from the board" was proposed; its mechanism is landed and tested, its field trial ran on the product path, and the ruling above endorses exactly its content, so it moves to implemented with its acceptance criteria answered one by one and its proposal-era headings retired. The arm plan's end state and the fusion planning document now point at the decision instead of restating it. * docs(decisions): the deferred item is an executor, not a call site Asked whether the permission this record announces is wired into the product, the honest answer needed the record to be exact about what is missing. It said "a board-side caller is not wired", which reads as one call site. It is not: the product has no loop that runs a unit at all. The shared module holds legality and selection as pure functions, the coordinator governs runs, the extension offers the worker's tool surface - and nothing decides "admit the next unit or close the session", obtains a runner and runs the unit. Every caller of decideSessionMove and openUnitSession is a test, and every caller of a session runner is the arms' driver or the extension's live path, both reached from evals/. Both language versions now say that, and say the consequence: the rule is enforced where the decision is made but the product cannot reach it yet, so wiring it is a product-side executor and a decision of its own rather than a call site. * refactor(execution): the dispatch loop is shared, and the board is a port The loop that runs a plan to completion through the board now lives with the other shared execution decisions (`src/integration/ooo-dispatch.ts`) instead of inside the arms' driver, which becomes its caller. What the driver keeps is its spec format, its worker, its parent check, its report and its session identity - its declarations and its measurement, not its copy of the ordering. The board is a port (`DispatchBoard`): candidates, accepted, claim, put, submit. The experiment's own board satisfies it structurally, and nothing in the shared loop names it, so a product path can provide the same operations without depending on a research instrument. Three defects a review named are fixed in the same change: - the session decision is recorded with the boundary it belongs to (task, attempt, entry). Without them every decision in a run collided on one store key and only the first was written, while the function still returned the move it had computed; - the session identity is the caller's (`SessionCapability.identity`), not a key the shared loop invents from a unit name; - the chain path is entered only when the caller declares that this host can carry several units in one session, and a declared slot count is no longer cut to one chain when it is. Also: agents' tooling and the repo's own anchors. `evals/**` has no tsconfig, so a type error there is invisible to `tsc` and to `build` - `lsp_diagnostics` is the gate that saw it. The mutation sweep gains a per-suite timeout, a `--mutant` filter and a name-filtered fast path: a mutant that livelocks now costs a bound instead of hanging the sweep, and a sweep command can be kept inside a caller's time budget. Decision: docs/decisions/implemented/2026-09-19-dispatch-loop-is-shared.md * fix(execution): one pass takes each unit at most once A unit the board refused or whose worker failed is reported, not re-asked: the same pass cannot change that answer, and asking again is how a run spins instead of ending. A retry belongs to the caller's next call, which is where a retry policy belongs. The sweep also bounds a run filtered to one case more tightly than a whole suite, so a mutant that livelocks costs a bound. * test(execution): the shared loop gets a fast suite of its own, and its teeth The loop is driven through a board that answers in memory: legal set, claims, entries, verdicts - no store, no git, no check runner. A case costs milliseconds instead of the seconds a store-opening suite costs, which is what makes the sweep below seconds instead of an afternoon, and it is also the product-side proof that the port is a port: the loop cannot tell this board from a real one. Eight teeth, every one of them caught by the case it names: - the board's answer decides what runs, not the declared plan order; - a declared slot count is reached, the claims overlap, and each unit runs once; - a unit's check is outstanding while an independent unit's worker runs; - a refused claim leaves the unit on offer and does not end the pass; - a failed unit is asked once in a pass (the once-only rule); - a fused chain stops at the declared bound; - a worker that starts its own session is not reported as fusion; - a failed worker is not reported as a run that finished. The run-fact case is the one that writes: it asserts that each boundary's session move names the unit, its attempt and the entry it belongs to, so the store records one fact per boundary instead of one per run. The sweep itself is now bounded, because an unbounded one is a sweep nobody can run: a case is filtered by name (`expect` already named it) with the whole suite as a fallback, a run that does not finish is killed and reported as such, `--mutant` selects one tooth, and the result carries where the time went. * test(execution): the arms' checks read data, and the host prepares nothing A unit's acceptance ran as `node --test <fixture>.test.ts` inside a throwaway git worktree built per candidate: mkdtemp, `git worktree add --detach`, a node_modules junction, and a cleanup. Measured: 580/79 ms add, 254 ms remove, ~0.94 s per unit, ~5.0 s per dispatching case, 82.5 s for the driver suite - all of it paid to give a test file three files to import. - `DataCheck` + `verifyDataChecks` in the shared candidate module: a check is a function over `{files, frozen}` that answers a verdict, so nothing is created and an interrupted run leaves nothing behind. The verdict rule is shared with the command kind, so the two cannot disagree by accident. - `verifyCandidate` takes a workspace its caller prepared, not a repository and a revision; it writes the candidate's files, runs the checks, and reports. It cleans nothing, because the working tree is the caller's business. - `evals/ooo-execution/data-check-runner.ts`: the arms' checks are their own fixture test files, transpiled and evaluated in process, with the fixture's relative imports resolved against the candidate's file set. The specs declare `{label, test}`. - `evals/ooo-execution/candidate.test.ts` retires with the machinery it tested; `mutation-probe` prepares and resets its own worktree, because a caller that needs a workspace owns it. - `tools/mutation-teeth.ts` no longer prints a "NOT caught" problem for a mutant it counts as caught because the case spun; the spin is shown as the reason instead. Measured after: the three suites pass 33 cases in 6.8 s (driver 82.5 -> 4.7 s, families 1.3 s, loop suite 0.6 s unchanged); 13 of 13 mutants are still caught by the case each one names, both targets restored byte-identically; `test:product` 1519 pass. Decision: docs/decisions/implemented/2026-09-20-tests-need-no-filesystem.md * feat(rtm): the traceability report is evidence, not a count `rtm:check` counted an assertion as `proven` when its `strength` was `decision` and its check id resolved - which only proves the check exists. Nothing said whether it ran, whether it passed, or whether the reading was about the revision in front of the reader, so a green run read as assurance over claims no execution had touched. - Inventory stays inventory: contracts, assertions, bound, documented-only, uncovered, orphans. No field of it decides anything, and the report ends by saying so. - Every assertion gets a standing from recorded execution: `agent:verify`'s `.nmg/verification/latest.json` names the commands it ran, their status and the revision. `executed` (passed, at this revision), `historical` (passed elsewhere, never counted as execution), `not-run` (skipped or failed, with the recorded reason - only a current *failure* fails the gate), `not-recorded` (no entry, no command carrying the check, or no evidence file), plus `documented-only` and `uncovered`. - Each risk class (strength/kind/stage) is judged on its own evidence, and a contract whose scope resolves to several routes is listed as a cross-module change with its own routes and counts. Assumptions and open counterexamples are listed. - `proven` is renamed `decision`: the old name claimed over the check's subject what binding an id cannot give. - The CLI prints one line per assertion, per risk class and per cross-module contract, and closes with "no overall verdict". Proof on the real repository: with the evidence left by a dry run, every assertion reads `not-run (skipped (dry run))` and the gate still exits 0; after a real `agent:verify`, seven assertions read `executed (build=passed; test:product=passed)` at that revision and the five whose carrying command the run never executed read `not-recorded`. Tests: tests/tools/rtm-check.test.ts 16 pass (was 11): inventory vs standing, current evidence making an assertion executed, another revision reading historical, a skipped command not failing the gate where a failed one does, and a cross-module contract judged on its own. Decision: docs/decisions/implemented/2026-09-20-rtm-evidence-aggregation.md * docs(execution): the product call path is audited against the extracted loop The dispatch-loop decision was written when no product caller existed for `dispatchPlan`, and two of its claims followed from that absence. The audit replaces the inference with observed facts, verified against `e3508a37` and re-run unchanged at `c01d3fe` without touching runtime code or buying model runs. - The product already has an Agent-driven execution path: the board wakes an existing host session (`deliverWake` in `.pi/extensions/nmg/index.ts`), the Agent claims, works and delivers through the existing `nmg_board` operations, and an independent judge binds the verdict to the artifact digest. The extracted `DispatchBoard` port leaves `BoardAdmission` as the research implementation, so the product does not open its `ooo_probe_*` store - which the decision now says instead of "the product cannot reach it". - The boundary is concrete rather than asserted: `task-run-surface.test.ts`'s `registerAndFreeze` freezes `J.dependencies = ["P"]`, and the case `a managed entry's lifecycle write goes through the run, and the run records it` then adopts and claims J without P being produced or accepted, with `entry-bound` and `board-claim` as the observed facts. That managed claim enforces registration, binding and cancellation; it does not enforce the dependency-acceptance predicate. - `claimTaskBoardEntry` and `resolveTaskBoardEntry` both call `promoteNextSerialPending` (conditionally on the claim in the first case), so promotion is a handoff becoming available after a claim or a closure, not a dependency becoming satisfied. The audit keeps those transitions distinct instead of describing them as missing. - Verification: 41 pass, 0 fail over the loop, fusion-plan, session-fact and task-run surface suites, run the repository's way. A live daemon observed at `2026-09-20T11:54Z` answered `compatible: true` while its advertised `methods` omitted `taskRun`, which the worktree's service implements: an installed-instance gap, not a missing source method. Two corrections to the incoming text: the verification command is written the way this repository runs tests (no wrapper), and the two readings that the data-check change invalidated are replaced rather than left standing - `families.test.ts` is 1.3 s now and was ~30 s while each acceptance built a candidate worktree, and the plan-driver mutation lane's clean run is 5.5 s against ~92 s, with the 2026-09-19 lane numbers marked as that instrument's. * docs(experiments): the trial's stored specs keep their day's check shape, and say so The fusion trial's two spec files are the recorded inputs of a completed run, and they declare checks the way the driver declared them then. The arms' checks are now the fixture test files themselves, so the stored pair is no longer runnable as it stands. The README now says which form they carry, which decision changed it, and that re-running the plan means regenerating the pair from the current fixture. * fix(lint): the trial's spec generator lives where the lint surface can see it CodeFactor reads the repository eslint config, where `no-console` is a warning except on the surfaces that report through stdout by design. The config's own rule is that every `files:` block must be anchored in a directory `npm run lint` scans (held by tests/tools/eslint-config-coverage.test.ts), so exempting docs/ would have produced exactly the silent intent that rule exists to prevent. The generator moves next to the fixture it reads, takes the harness's name, and the trial README points at it. * fix(lint): the generator's usage line and the trial README follow the move The previous commit carried the rename alone: the `git add` that should have staged the edits listed the generator's old path, which no longer exists, so git rejected the whole pathspec list. This commit is the rest of that change - the file says where it now lives and why, and the trial README points at its new path in both places it names it. * docs(decisions): the session AG runtime is implemented, not proposed The record sat in proposed/ while its blueprint has been the current design since 2026-09-01 and the runtime it describes is live code. It moves to implemented/ with what that folder requires: Problem / Decision / Alternatives considered / Consequences, plus a Deferred section. The eight criteria stay under Decision, so the blueprint's acceptance mapping still has the text it cites, and the three items the blueprint itself marks Partial or intentionally deferred are named as deferred instead of being smoothed over. `Approved: unrecorded`, because the acceptance predates the approval field: the debt is the missing act, not an unapproved decision. The decision's compatibility layers are gone rather than kept - no `SessionRuntimeAg` and no continuation map remain in the tree, the query-scoped AG is the projection revision, and the disclosure ledger is what the Pi extension, the Claude plugin, WorkBuddy and DSH write through `markDisclosed`. Five design files pointed at the old path; design.md also called the decision proposed. The blueprint's own `decision §Proposal` and `§Acceptance` references now name the Decision section, which is where the criteria live.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Migration step 2 of
docs/design/task-unit-semantics.md(the persistence bullet): a round's storage is namespaced by run, so one store can hold several rounds — which is what makes "two rounds with the same taskId must not collide" testable at all.What changed
BoardAdmissionOptions{runId};ooo_probe_runs(run_id PK, policy, cancel_reason, cancelled_at, created_at)replacesooo_probe_meta (id PK CHECK(id=1));ooo_probe_tasks/ooo_probe_checksgainrun_idwith composite keys; the board channel isooo-probe:<runId>instead of the constantooo-process-probe.migrateToRunScope()rebuilds the legacy tables in one transaction and throws rather than guessing if legacy rows exist without theirmetarow. A migrated store keeps its artifact and leaves it unaccepted: the old column was the round's own self-report, which the board's independent verdict replaced.checkIdstays run-free on purpose. A replay is a new run reproducing the same attempts; scoping it broke replay (2 !== 3) before the suites were green again.Evidence (all re-runnable)
npm test— 1452 passed, 0 failed.tests/integration/ooo-run-namespace.test.ts— 4 of 4: two runs in one store do not collide, do not write each other's rows, and cancel separately; a multi-run store refuses to guess; a one-run store is continued; a pre-namespace store is migrated in place, and the cancelled run it restored then refuses a claim.npm run mutation:teeth— 22 of 22 mutants caught by name across 6 targets, 6 of 6 restored byte-identically, 0 inapplicable. The newclaim-is-not-scoped-to-its-runtooth was not caught by the first version of the test (next()/accepted()do not observe a neighbouring run's row); the assertion that catches it reads the other run's rawowner.npm run docs:check188 files / 0 errors;npm run complexity:gateok (11 changed files, 0 methods above 15 vs baseline).Not in this PR
The table is still one table (split is next), and the round still owns its SQLite file (injection from the daemon is after that). Record with the honest limits:
docs/experiments/execution/ooo-run-namespace-2026-09-13.md.Commits (this branch accumulates the rest of the migration; one PR, one review of the whole thing):
a3124e62the store and the board channel are scoped by run (change + call sites)4428039cthe four acceptance tests for a multi-run store2831d890teeth retargeted, plus a tooth pinning the claim's run scoped9104bd7the recordThe tree at the last commit is byte-identical to the tree these numbers were measured on
(
git rev-parse HEAD^{tree}equal,git diffempty) — splitting the history changed no content.The intermediate commits were not each re-run; the guarantee is on the final tree.
Still to come here: B2 split
ooo_probe_tasks(manifest / candidate bytes / derived state) andB3 inject the daemon-owned store instead of the round owning a SQLite file.