@sema-agent/core 7.11.2 → 7.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CHANGELOG.md +86 -13
  2. package/dist/core/ask-origin.d.ts +55 -0
  3. package/dist/core/ask-origin.js +21 -0
  4. package/dist/core/auto-mode-arming.d.ts +10 -14
  5. package/dist/core/auto-mode-arming.js +3 -9
  6. package/dist/core/auto-mode-defaults.d.ts +0 -2
  7. package/dist/core/auto-mode-defaults.js +0 -1
  8. package/dist/core/auto-mode-rebuild.d.ts +6 -13
  9. package/dist/core/auto-mode-rebuild.js +0 -2
  10. package/dist/core/auto-mode.d.ts +30 -89
  11. package/dist/core/auto-mode.js +12 -59
  12. package/dist/core/checkpoint-store.d.ts +1 -3
  13. package/dist/core/engine-notice.d.ts +18 -0
  14. package/dist/core/gate-fold.js +1 -9
  15. package/dist/core/gate-lanes.d.ts +0 -40
  16. package/dist/core/gate-lanes.js +18 -26
  17. package/dist/core/governance-codes.d.ts +1 -1
  18. package/dist/core/governance-codes.js +2 -0
  19. package/dist/core/hooks.d.ts +7 -1
  20. package/dist/core/hooks.js +1 -1
  21. package/dist/core/runner/assemble-result.d.ts +45 -1
  22. package/dist/core/runner/assemble-result.js +4 -1
  23. package/dist/core/runner/compaction-seams.d.ts +42 -0
  24. package/dist/core/runner/compaction-seams.js +80 -0
  25. package/dist/core/runner/contracts.d.ts +85 -15
  26. package/dist/core/runner/denial-limit-arms.d.ts +10 -13
  27. package/dist/core/runner/denial-limit-arms.js +12 -9
  28. package/dist/core/runner/gate-exit.js +9 -1
  29. package/dist/core/runner/permission-rule-lanes.d.ts +2 -1
  30. package/dist/core/runner/permission-rule-lanes.js +2 -1
  31. package/dist/core/runner/prepare-caps-and-workflow.d.ts +1 -1
  32. package/dist/core/runner/prepare-caps-and-workflow.js +0 -5
  33. package/dist/core/runner/prepare-policy-chain.js +3 -2
  34. package/dist/core/runner/prepare-protocol-tools.js +5 -0
  35. package/dist/core/runner/prepare-safety-scan.js +11 -16
  36. package/dist/core/runner/prepare-suspend-saga.d.ts +0 -2
  37. package/dist/core/runner/prepare-suspend-saga.js +2 -10
  38. package/dist/core/runner/prepare-task.d.ts +9 -1
  39. package/dist/core/runner/prepare-task.js +7 -2
  40. package/dist/core/runner/prepare-wiring-manifest.d.ts +1 -1
  41. package/dist/core/runner/prepare-wiring-manifest.js +1 -8
  42. package/dist/core/runner/resume-claim.d.ts +2 -2
  43. package/dist/core/runner/resume-preflight.d.ts +2 -2
  44. package/dist/core/runner/run-attachment-seats.d.ts +6 -4
  45. package/dist/core/runner/run-attachment-seats.js +2 -2
  46. package/dist/core/runner/run-git-lane.d.ts +1 -1
  47. package/dist/core/runner/run-harness-handlers.js +2 -0
  48. package/dist/core/runner/run-identity-wiring.d.ts +21 -33
  49. package/dist/core/runner/run-identity-wiring.js +6 -3
  50. package/dist/core/runner/run-leg.d.ts +108 -0
  51. package/dist/core/runner/run-leg.js +713 -0
  52. package/dist/core/runner/run-notification-lane.d.ts +56 -0
  53. package/dist/core/runner/run-notification-lane.js +128 -0
  54. package/dist/core/runner/run-reasoning-seat.d.ts +5 -5
  55. package/dist/core/runner/run-reasoning-seat.js +7 -7
  56. package/dist/core/runner/run-recovery-lanes.d.ts +4 -17
  57. package/dist/core/runner/run-recovery-lanes.js +5 -4
  58. package/dist/core/runner/run-settle-and-teardown.d.ts +113 -0
  59. package/dist/core/runner/run-settle-and-teardown.js +432 -0
  60. package/dist/core/runner/run-stop-and-final-verify.d.ts +2 -2
  61. package/dist/core/runner/run-terminal-adoption.d.ts +99 -0
  62. package/dist/core/runner/run-terminal-adoption.js +200 -0
  63. package/dist/core/runner/run-turn-boundary.js +5 -4
  64. package/dist/core/runner/runtask.d.ts +10 -79
  65. package/dist/core/runner/runtask.js +83 -1557
  66. package/dist/core/runner/stream-lifecycle-verbs.js +10 -1
  67. package/dist/core/runner-deps.d.ts +4 -14
  68. package/dist/core/store-contracts/workflow-journal-store-contract.d.ts +7 -0
  69. package/dist/core/store-contracts/workflow-journal-store-contract.js +85 -0
  70. package/dist/core/task-stream.d.ts +10 -1
  71. package/dist/core/tool-face.d.ts +8 -0
  72. package/dist/core/tool-face.js +1 -0
  73. package/dist/core/tool-policy.d.ts +42 -96
  74. package/dist/core/tool-policy.js +1 -11
  75. package/dist/core/tool-registry.d.ts +11 -3
  76. package/dist/core/tool-registry.js +7 -1
  77. package/dist/core/tool-roster.d.ts +26 -0
  78. package/dist/core/tool-roster.js +38 -7
  79. package/dist/core/trace.d.ts +6 -7
  80. package/dist/core/wiring-manifest.d.ts +5 -22
  81. package/dist/core/wiring-manifest.js +3 -11
  82. package/dist/core/workflow-journal-store.d.ts +35 -4
  83. package/dist/core/workflow-journal-store.js +19 -2
  84. package/dist/index.d.ts +6 -5
  85. package/dist/index.js +6 -5
  86. package/dist/orchestration/workflow.js +2 -0
  87. package/dist/server/http.js +2 -1
  88. package/dist/stores/file/workflow-journal-store.d.ts +7 -10
  89. package/dist/stores/file/workflow-journal-store.js +2 -4
  90. package/package.json +1 -1
  91. package/test/export-surface.snapshot.json +21 -11
package/CHANGELOG.md CHANGED
@@ -1,5 +1,78 @@
1
1
  # Changelog
2
2
 
3
+ ## 7.13.0 — 2026-09-10
4
+
5
+ ### Narrowing — a run that has already given its answer is no longer cancellable (#677; ruling 「最后一次 `turn_end` 后 run 不可取消:三轴同律;窗内停机终态记 completed;`interrupt()` 改有明确回执」; sema form, no CC 2.1.250 mirror — DV-677-1; @server @cli @test)
6
+ - **The rule, in one sentence.** Once the run's last turn has ended `stopReason:"stop"` (a clean answer — not a `partialFinalized` salvage, not `length`, not a tool batch), a hard stop landing on the run's TAIL — `TaskStream.interrupt()`, the task's `signal`, the walltime limit, and the turns cap firing on that same answer turn — cuts no work, and the run says so: it ends `completed` with that answer as `result`; no `[Request interrupted by user]` marker is written; no `limits.max_walltime_exceeded` / `limits.max_turns_exceeded` is minted for the axis; the end-of-task compaction pass does not run (no second summary call after the stop, no hook seat to collapse — see below); and the stop's only trace is ONE `task.interrupt_unconsumed` notice. The stop still aborts what is left of the tail — a trailing-boundary compaction in flight is mooted (`compact()` resolves `"mooted"`; no second summary is burned for it), a slow `stop` hook's seat still collapses to the grace as every hook seat does on the task signal. Before: the same stop recorded `failed` / `"run aborted"` (walltime: `+ limits.max_walltime_exceeded`; the turns cap: `+ limits.max_turns_exceeded`), wrote the marker (the next run on the session then read a finished answer as an abandoned one), and collapsed the seats — for a window the bundled in-memory store closed (0–7 ms after `turn_end`, reachable only from the same microtask chain) but any slow await after the last turn opened wide: the whole final-boundary compaction call (seconds), a slow `stop` hook, a durable/remote session append.
7
+ - **One predicate, evaluated once, read by five sites, zero new branches.** `answerSettled(final, userInputLost)` := `final?.stopReason === "stop" && final.partialFinalized !== true && !userInputLost` (`src/core/runner/assemble-result.ts`; a module export, not a public name). The second fact is "a continuation was cut", from two owners: the harness's own agent_end account (`onUndrainedUserInputs`, which counts a frame drained at the final boundary but never served) — a steer / follow-up the person had already queued makes that "stop" work in progress; the loop's own trace steps (`Prepared.workCutRef.continuation`: a `continue` step then the `aborted_before_stream` terminal) — a thinking-only "stop" the loop had decided to nudge into a visible answer, a truncated-output or degenerate-output recovery, a drained frame — in both of these the loop exits on the abort BEFORE the next dispatch and `final` is still the previous clean answer, so the message alone cannot tell (adversarial r1 + r2, both reproduced: r2's thinking-only shape returned `completed` with `result:""`); and the `turn_end` handler's own reading (`Prepared.workCutRef.abortedBeforeTurnEnd`: the run's abort was already in force when the last turn ended, read before the turns cap fires) — the ruling's line is the last `turn_end`, so a brain that ignored its signal and delivered a full `stop` THROUGH the abort still answered a run somebody had stopped mid-work (`failed` / `"run aborted"`, the bytes in `result`; the existing rb506 cancel control pins it). The terminal-adoption lane evaluates it ONCE (`RunTerminalAdoptionResult.answerSettled`) and every reader takes the value: (a) the terminal arbitration's slot 9 via `ResultFlags.answerSettled` — the whole `abortedLive | stopReason aborted` disjunction stands down and the run takes the EXISTING completed floor (no terminal is newly minted; `TerminalCause` stays single-sourced); (b) the interruption-marker gate (the orphan reconcile and the held session-write drain still run on every live abort — only the marker is gated: with no open batch the reconcile is a session read, and the held writes have no other drain before the harness is discarded); (c) the end-of-task pass — skipped outright for a stop on the tail (below), so its `cancelled` option (the hook-seat grace, unchanged) only ever serves a stop on work; (d) the teardown's background-child attribution — a child of a run stopped on its tail is settled `stoppedBy:"parent"` as on any completion, never `"user"` under a parent that reports completed (adversarial r1); (e) the notice gate. And two passes that used to start a NEW model call after a live stop no longer do — they are part of the tail the stop aborted: the opt-in suggestion pass (`suggestNextPrompts`; it builds its own controller, so it used to start a fresh call after a person's interrupt — independent review) and the end-of-task compaction for a stop on the TAIL (`finish`'s `skipCompaction` now includes `abortedLive && answerSettled`: `maybeCompact` takes no cancellation signal there, so a trailing-boundary summary the stop had just mooted was burned a second time before `result()` resolved — adversarial r2, reproduced; the session's next run compacts at its own boundary). A stop on WORK keeps the documented between-task pass (design/64 §25 "after a max-turns abort the boundary does not compact — finish() does", pinned in `compaction-within-task-turn-boundaries`), with its hook seats on the post-abort grace as before — r2 proposed skipping it for every live abort; the documented contract wins. Rules: −2 (the sites used to answer "was this run stopped?" from `abortedLive` alone, each with its own reading of the tail; the answer question is asked once) +0 exceptions.
8
+ - **`interrupt()` answers, both sides of the loop's end.** After the loop's end (the teardown window: the reconcile, the ledger flush, the end-of-task compaction — or once the result is out) it REJECTS `steering.not_running` (`cannot interrupt: the task is no longer running` / `…has already finished` / `…is not running` on a prepare that never produced a handle) — the halt/steer liveness gate on the same two facts (`resultValue`, `loop.ended`); it used to resolve `void` and do nothing. Inside the window it is NOT refused (the run is alive) — it aborts the tail, and the run tail owns the account (the notice). **Consumers:** a fire-and-forget `void stream.interrupt()` after the result is now an unhandled rejection — attach a handler, as for `halt`/`steer`. The bundled HTTP server's disconnect handler (`src/server/http.ts`, the one in-tree fire-and-forget caller — adversarial r1 + independent review, both) handles it: a client that disconnects while the run is already tearing down is that race, not a fault.
9
+ - **Export surface (+1 closed-set member, additive).** `ENGINE_NOTICE_CODES` +`"task.interrupt_unconsumed"` (`NOTICE_AUDIENCE` row `user`; `detail: { sessionId, runId, taskId?, origin }`, `origin` ∈ `"interrupt"` | `"walltime"` | `"turns"` | `"signal"` — the last is every other run-level abort the leg cannot name (the task's own `signal`, a parent's cancel, a `destroy()`, a refused preempt); once per run, minted AFTER the terminal arbitration so it can never claim a completed ending the terminal did not give). No other public name added or removed (`handoff:diff` 0/0); internal lane seats: `RunSettleAndTeardownInput` +`userInterruptedLive` +`answerSettled`, `RunTerminalAdoptionInput` +`userInputLostAtEnd`, `RunIdentityWiringResult` +`userInputLostAtEnd` (read face), `RunTerminalAdoptionResult` +`answerSettled`; `gate:phase-api` floor 941 → 944.
10
+ - **Observable (double-sided; the probe car's readings, now pinned).** ⟨tail⟩ answer `"stop"` → trailing-boundary compaction 2.5 s → stop at +400 ms: `interrupt()` resolves, `result()` = `{ terminal: { kind: "completed" } }`, `result` = the answer, `errorCode` undefined, `compact()` → `"mooted"`, no new model turn, session markers 0, notices = exactly one `task.interrupt_unconsumed` (`origin:"interrupt"`); the task `signal` → same with `origin:"signal"`; `limits.maxWalltimeMs` firing inside the compaction → same with `origin:"walltime"` and NO `limits.max_walltime_exceeded`; `limits.maxTurns` = the answer's own turn → same with `origin:"turns"` and NO `limits.max_turns_exceeded`; with no widener at all, an `interrupt()` issued on the `turn_end` event lands inside the window → completed. End-of-task `preCompact` hook that never answers, `timeoutMs: 2500`, stop on the tail → `onError` says `did not answer within its 2500ms bound` (the declared deadline), run completed. `interrupt()` after `result()` → rejects `steering.not_running`; from inside the end-of-task `preCompact` hook (after the loop's end) → rejects the same; steer/halt/interrupt on a finished task → the same code, three verbs. ⟨work⟩ the same three stops landing while a tool is running → `failed` / `"run aborted"` (walltime `+ limits.max_walltime_exceeded`), the interruption marker written for `interrupt()`, no notice; the hanging `preCompact` seat under a mid-tool stop → `had not answered when the task was cancelled` (the grace) and settles under 2.4 s. A run with no stop at all → completed, compaction landed, zero notices.
11
+ - **Fail-open reverse check — which stop STILL records an answered run as failed (unchanged, pinned G6 / G1.e).** A steer or follow-up the person queued before the answer and the stop cut before it was served (`failed` / `"run aborted"` + the marker + `task.user_steer_undrained`); a budget ceiling crossed by the last turn's own spend (`limits.max_tokens_exceeded` / `limits.max_cost_exceeded` — slot 3, above the yield); a throw or a lifted platform / guardrail / gate-stop cause (slots 4–7, e.g. `usage.window_exhausted`); the suspend-loop cap (`suspend.loop`); an UNSETTLED final under any live abort — `length`, `toolUse`, a partial `"stop"`, an `aborted` cut, no final (`failed` / `"run aborted"`, the axis code intact). A durable pause is untouched (slot 8.5).
12
+ - **Downstream.** `@server`: a walltime / turns limit that fires after the answer no longer produces a `limits.*` terminal — a per-run count of `limits.max_walltime_exceeded` / `limits.max_turns_exceeded` drops by exactly the runs that had already answered; the `task.interrupt_unconsumed` notice (with `origin`) is the replacement signal — whitelist the code (closed-set addition), forward it on the wire as the other `task.*_unconsumed` rows are. `@cli`: `interrupt()` after the result now rejects `steering.not_running` — map it to the cancel prompt the way `halt`'s refusal is mapped (cli [6862] ③); a run cancelled after its answer renders as a completed run with its answer plus the notice text. `@client-core`: no shape change (`EngineNotice` is untyped on `code`). `@test`: G1–G6 in the release criteria.
13
+ - Pins: `test/backlog677-cancel-window.test.ts` — G1 (three axes + no-widener + the queued-continuation control G1.e + the thinking-only-nudge control G1.f with its unstopped twin G1.g + no-stop control; G1.a also pins that no second summary starts after the stop), G2 (marker both ways), G1.h (a brain that answers through the caller's abort ⇒ failed), G3 (no end-of-task pass after a stop on the tail — no summary call, no preCompact seat; a stop on work keeps the pass with its seat on the grace; the unstopped control runs the pass and its hanging seat expires on the declared bound; the tail arm asserts the notice so a timing slip cannot make it vacuous), G4 (refusal after the result / from the teardown window / three verbs one code / mid-tool positive control), G5 (work controls), G6 (the predicate + the arbitration as a unit matrix incl. the fail-open reverse list; the verdict as a value), G7 (background-shell child receipt `parent` on the tail / `user` on work), G8 (no suggestion pass after a stop / the pass runs on an unstopped completion). Re-pinned: `test/steer-interrupt-lifecycle-389.test.ts` "a LATE interrupt() … mints no marker" (the late stop is now refused typed — asserted — and still writes no marker); `test/steering.test.ts` "interrupt() after the task finished" (was the council #4 silent no-op; now the typed refusal).
14
+ - **Not this car (recorded for the ledger, not fixed):** a durable / remote `SessionStore` append widens the window the same way (inferred from the await class, not measured); a subagent / child run's tail (the same sites, not probed); a `preemptSignal` fourth axis (files under `origin:"signal"`); an abort that lands BEFORE an `interrupt()` in the same window records `failed` with no marker (the first-writer origin — inferred); a `spec.signal` abort that lands AFTER the leg's `abortedLive` snapshot (the teardown window) changes nothing and mints nothing — the caller fired its own signal and gets no receipt (independent review; no consumer known to care); a CC-style `stop` hook that answered "keep going" and was then cut by a stop before the nudged turn dispatched ends `completed` with the answer (the hook's continuation is not a person's queued input and is not folded in — same shape, different owner; independent review); the same-window boundary-bundle residue in `run-turn-boundary.ts`; the turns-cap arm of this rule (the cap firing on the answer turn ⇒ completed) is the whole-disjunction reading of the ruling's "three axes, one law" and is named here so it can be un-ruled if that reading is wrong.
15
+
16
+ ### Narrowing — a read-boundary MANDATE is never the auto-mode classifier's to answer (#681; B-059; cli [6870] U-2; B-057's third lane; @cli @server @test)
17
+ - **The hole.** `bashReversibilityProbe` mints `mandated` for a deny-listed / out-of-root / recursive read; the gate folds that ask under the origin word `shell_gate_tighten` — the classify tier's OWN hesitation — and `CLASSIFIER_MAY_ANSWER.shell_gate_tighten` is `true`. So with auto mode armed the classifier was asked, answered allow, and the read ran with ZERO asks (foreground and backgrounded alike; measured in-process: one classify call, `tool_end.gate` allowed, the sentinel on stdout). The rule lane (7.11.1/7.11.2, B-057) and the shell's sync-allow layer (cli B-058) had both been closed; this was the same boundary's third clearing lane.
18
+ - **The rule (one word-set, reused).** The live gate's classifier station and the inherited-lane station both now read the allow layer's mandated-family word (`persistedRuleMandateOf`) and exclude the PROBE half of it — `probe_mandate` / `probe_unanswered`, i.e. the deployment's own read boundary judged per call (`probeBoundaryMandate`). The other two words (`tool_marks`, the operator's "always" tier) keep binding the rule lane only: an egress / safety tighten is the classifier's question by the origin table's own design (`safety_tighten: true`, pinned since 7.4). No new origin word; the table keeps saying which QUESTIONS are the classifier's, the probe words say which asks are nobody's but a person's. CC 2.1.250 form: boundary-class decisions carry `classifierApprovable:false` and the classifier station skips them.
19
+ - **Observable.** auto armed + `readFace:"roots"` + `readDenyPatterns:["**/secrets/**"]` + `grep needle secrets/app.txt` (with or without `run_in_background:true`): the classifier is consulted ZERO times, the person is asked ONCE (`origin:"shell_gate_tighten"`), a refusal ⇒ denied and never read; a classify-tier write (`touch x`) with no boundary is still the classifier's question (one classify call, zero asks); an ancestor's `probeMandated` ask at the inherited station is resolved by the ancestor's chain, never classified.
20
+ - **The sandbox-admission legs (review round).** The same probe words are now read by the live gate's sandbox-admission leg and by the two ancestor sandbox-admission arms: containment holds EFFECTS, a read boundary is about what reaches the model, so an isolated-sandbox deployment (`sandbox_local` policy + `redecision.reopen` store + durable park) no longer auto-admits a probe-mandated read either — it parks for a person. `probeMandatedAsk(ask)` is the one spelling those three readers share.
21
+ - **Also observable.** A reversibility probe that THROWS or times out under auto mode now ASKS a person (`probe_unanswered` is the family's second probe word) — before, the classifier answered in its stead. A fail-closed change, disclosed so a flapping probe is not read as a regression.
22
+ - **Fail-open reverse check.** Which mandated ask can still reach an automatic yes? A blanket `onAsk:"allow"` deployment seat clears asks that carry no `requiresRealApproval` — the probe mandate does not mint that bit today (cli [6870] U-3); tracked as the next arm under #681, not folded into this narrowing.
23
+ - Pins: `test/backlog681-auto-mandate-not-classifiable.test.ts` (nine cells: five Runner cells, a probe_unanswered unit cell, the operator-always reverse control, the sandbox-leg unit cell, a lexical anchor over the two ancestor arms — their positive admission path is not unit-producible; mutations: drop either classifier conjunct ⇒ 2/1 red, drop the sandbox-leg conjunct ⇒ 2 red, drop the ancestor conjuncts ⇒ 1 red, narrow the family to one probe word ⇒ 1 red) + `test/f012-sandbox-admission-domain.test.ts` (probeMandated ⇒ not admitted, red before). Re-pinned with the A car's #626 ① door: `test/design378-mcp-content-class.test.ts` G11 keeps its hostile server name under the 128-char bound (the over-bound spelling is now refused at prepare, one new cell).
24
+
25
+ ### Narrowing — the roster row never truncates an identity member, and a containment fence refuses a bare request (#626 ①②)
26
+ - **① Identity members are bounded at the mint, never truncated.** `mintRosterEntry` used to `slice` four members onto the row (`contract.contractId` / `contract.implementationRevision` / `modelGate` to 64, `cardId` to 160) and carried an MCP/A2A `origin.peer` past the schema's own 128 bound. Now ONE judge — `rosterMemberBoundProblem` (exported) — is read by the declaration door (`spec.tools`, before session acquire) and by the row mint (every source: MCP/A2A peers included): a member over its bound refuses the row with the door's existing codes (`config.tool_name_too_long` for a name / alias / peer, `config.tool_contract_too_long` for a contract member) or the mint's own `config.tool_roster_bound` (`modelGate`, `cardId`). `TOOL_CARD_ID_MAX_CHARS` (exported) is now DERIVED from the member bounds (`mcp:` + 128 + `/` + 128 = 261) instead of the declared 160 — a row minted from in-bound members fits by construction; the `ToolRosterEntry` schema's `cardId.maxLength` reads the constant (a consumer validating against the exported schema sees 261).
27
+ - Observable: a `spec.tools` entry with `modelGate` over 64 characters now refuses prepare (`config.tool_roster_bound`) instead of mounting with a truncated class; an MCP server whose configured name is over 128 characters now refuses prepare (`config.tool_name_too_long`) instead of minting a row whose `origin.peer` violates the row schema and whose `cardId` was cut at 160; a definition that reaches the mint by a route the door does not cover (an `AgentTool` handed to `RosterBuilder` directly) with a contract member over 64 refuses (`config.tool_contract_too_long`) instead of minting a different identity.
28
+ - Pins: `test/backlog626-roster-identity-bounds.test.ts` (each member at the bound rides verbatim / one over refuses, per code; the derived `cardId` bound covers every source form; the schema reads the constants; every catalog row is inside the bounds); `test/tool-face-bounds.test.ts` unchanged (the door's codes are the same).
29
+ - Downstream: a consumer that validates roster rows with a private copy of the 160 `cardId` bound must read `TOOL_CARD_ID_MAX_CHARS` (or the exported schema); nothing else on the wire changes.
30
+ - The PEER's own door (adversarial round 1): an MCP server name over 128 characters refuses prepare BEFORE any server is connected (`config.tool_name_too_long`, `prepare-protocol-tools.ts`) — a server with an empty initial listing would otherwise be judged for the first time at a run-time refresh, where the harness is rebuilt before the roster republishes and a throw would leave the refreshed tools callable without a roster face. The mint's peer check is the backstop; this door is the one that fires. Pinned (a 129-character server name, zero tools, never started).
31
+ - `gate:tool-catalog` ②d (adversarial round 1): an INDIRECT reference to `defineTool` (a renamed import, a local alias, `.call`, a namespace property, a value passed along) is refused outright — the syntactic gate follows only direct calls, so what it cannot follow is red rather than silently missed (`typeof defineTool` in a type position stays green); round 2 extended it to a renamed RE-EXPORT (a barrel alias) and to string-key reaches (`mod["defineTool"]`, a quoted destructuring, `require(...)["defineTool"]`).
32
+ - **② A bare request is refused by a containment reader.** design/388 B6's migration seam — `declaredPathTargetOf(call)` answering a request with no `face` from the catalog's declaration for the NAME — is closed: it now throws a typed error, `policy.bare_request` (`BARE_REQUEST_CODE`, exported), and so does every fence behind it (`isDeclaredWrite`, the fs-write gate's accept/exempt dirs, the session `allowDirs` rule, the persisted path-rule lane's allow arm). A grant is a claim about the tool's OWN slot; only a prepared leg's roster can say which object a call dispatches to. The PROTECTIVE floor is unchanged: `protectivePathTargetOf` / `isProtectedWrite` / the sensitive-path guard / write protection keep reading the catalog for a bare request (tighten-only — a deny can only gain a slot).
33
+ - Observable: inside a Runner nothing changes — the gate entry, the three resume belts and the pre-CAS edit recheck stamp the leg's face on every request (audited: `hooks.ts` gate entry, `run-leg.ts` ×3, `resume-apply.ts`, `prepare-park-ask.ts`); the one engine producer that can still hand a containment fence a bare request is the pre-CAS resume-edit recheck of a park row written before 7.8.0 (no face members on the row) — that recheck already catches a throwing policy and DENIES the edit with the error text, so the outcome is a loud deny rather than a catalog reading. A policy driven OUTSIDE a Runner (a host's own `check()` call, a test fixture) on a covered write name (`Write` / `Edit` / `NotebookEdit` / a caller face) must now stamp `face` (`toolCallFaceOf(roster, name)`); a bare NON-covered name never reaches the door (the fs-write gate allows it as before; the session rule denies it as an unconfinable write as before).
34
+ - Pins: `test/backlog626-bare-request-hard-door.test.ts` (the two readers, the three fences and the lane refuse bare / answer faced; the protective floor still reads the catalog; a Runner-driven hands `Write` under the fs-write gate lands); `test/design388-write-fences.test.ts` re-pinned at the three bare readings.
35
+ - Downstream (@server): a self-written `ToolPolicy` that calls `declaredPathTargetOf` / `isDeclaredWrite`, or that drives `createFsWriteGatePolicy` / `createSessionRulePolicy` outside a Runner with a hand-built request, must stamp the face; a policy that only ever runs inside the Runner's gate needs nothing.
36
+ - **Rules: fewer.** ① two bound judges (door: name/contract; mint: four independent `slice`s) → one function read at both; ② the containment reader's bare arm (catalog fallback) is gone — one reading (the face) instead of two.
37
+
38
+ ### Internal — design/393 S7: the Runner's remaining private methods leave `runtask.ts`; the driver is one method (byte-invariant; #667 / #670 / #674 / #675 ruled with it)
39
+ - **What moved, verbatim.** `seamCCompactionOptions` / `compactionHookOptions` / `recordCompactionReuse` → `compaction-seams.ts` (layer 1: three lane callers reach DOWN; `runner: RunnerDepsSeat` reads the deps live where `this.deps` did). `finish` → `run-terminal-adoption.ts`; `suggestNextPrompts` (+ its four bounds) and `teardownOwnedEnv` → `run-settle-and-teardown.ts`; `applyResumeDecision` + `resolvePendingCall` (+ the org-disclosure cap) → `run-leg.ts` — each the lane's own module-private function, by the S6 legs' rule (one lane caller; a sibling file would be a lane-to-lane reach). `runtask.ts` 2 514 → 1 415 lines; `Runner` is its public API, the two façades (`runTaskStream`, `resumeStream` + ladder + handoff) and `runLocked`.
40
+ - **#675 — one contract for the orchestrator's entry.** The five delegate seats and the two (divergent) spellings of the compaction triple are gone; the one seat left (`RunNotificationLaneInput.prepareTask`) is typed by `PrepareTaskFn` (contracts.ts) and the orchestrator pins `typeof prepareTask` against it at its declaration (`PrepareTaskIsTheContract`, type-level).
41
+ - **#674 — the driver's terminal form.** `runLocked` alone: the S6 continuation methods (`runSeatLanes` / `runAssembliesAndLegs`) and the `RunSeatsHandoff` re-spelling are the `next` closures of the async lanes they follow; no method or closure is `async` (the S6 TICKS and rejection-depth pins hold unchanged).
42
+ - **#670 — the empty Results declare themselves.** `InstallingPhaseResult` (contracts.ts): the six lanes whose products are all installations on borrowed seats extend it (run-attachment-seats, run-stop-and-final-verify, run-recovery-lanes, run-settle-and-teardown, resume-preflight, resume-claim), and `gate:phase-api` reads the declaration (INSTALLING rule: empty ⇒ must extend it; members ⇒ must not; nothing else inherited).
43
+ - **#667 — `docs/INTEGRATION-CORE.md`** re-anchored: every `runtask.ts:<line>` coordinate now names today's file:line and the symbol (the resume resolver's arms are `run-leg.ts` coordinates).
44
+ - **Lane Inputs (fewer seats, one added).** `RunLegInput` −`applyResumeDecision`; `RunTerminalAdoptionInput` −`finish` +`sessions`; `RunSettleAndTeardownInput` −`suggestNextPrompts` −`teardownOwnedEnv`; `RunRecoveryLanesInput` −`compactionSeats`; `TurnBoundaryDeps` −3 compaction delegates +`runner` (the turn boundary spells the leg identity / seat bound the driver's adapter baked in, at its own call). `gate:phase-api` floor 945 → 941.
45
+ - **Export surface: unchanged** (no new public name; the lane modules and contracts additions are internal).
46
+
47
+ ## 7.12.0 — 2026-09-10
48
+
49
+ ### BREAKING — workflow journal stores refuse an oversize entry loudly (#672; server [6840]; @server SQL store result arm same rule; @test)
50
+ - **The rule, in one sentence.** `WorkflowJournalStore.append` REFUSES any entry whose payload (the `parked` arm when present, else the `result` arm) serializes to more than `MAX_JOURNAL_RESULT_BYTES` (5 MiB, UTF-8): it throws `WorkflowJournalOversizeError` (`code: "workflow.journal_oversize"` = the existing `JOURNAL_OVERSIZE_ERROR_CODE`, message carries the bytes, the cap and the callKey) and journals nothing — on BOTH arms, one rule (`assertJournalEntryFits`, `@contract workflow.journal_oversize.refused`). Before: both bundled stores answered an oversize entry with a silent `return` (RB-168 "every backend degrades identically"). That skip was written for the `result` arm, where it was a dead spare (the engine measures the same serialization first and journals a tombstone, RB-243); on the `parked` arm (#642) the engine hands the store the child's WHOLE paused `TaskResult`, which can exceed the cap — the park then vanished from the journal, and a resume of a journal-only deployment ran the ordinal live beside the pinned child (server [6840], codex high, verified).
51
+ - **Engine, result arm — unchanged.** `journalAppend` still measures the durable copy, logs, counts `run.journalSkips` and journals the RB-243 tombstone; a completed result never reaches the store's refusal (pinned: every row the store is asked to append fits; the tombstone itself serializes under 4 KiB).
52
+ - **Engine, park arm — the refusal is disclosed, the park stands.** `journalAppendParked` throws the oversize refusal at once instead of retrying it (a transient store fault still gets three attempts; the oversize one is deterministic, and retrying it was measured — adversarial r2 — to spend the terminal's deadline so that a LATER, fitting park's carry was skipped); the caller's existing disclosure fires: the run log names the ordinal, the label and the store's message (bytes + cap) and says `start a fresh run instead of resuming this one`. With a `WorkflowRunStore` wired, a resume of that run is REFUSED by the #642 parked-row cross-check (`WorkflowJournalIncompatibleError`, "its journal entry is missing") — nothing runs live. **Known limit (journal-only deployment, no run store):** the refused park leaves no durable record anywhere but the run log, so a resume of that run still runs the ordinal live — the write-time disclosure is the whole gain there; wire a run store for the structural refusal. **Known limit (carry path, pre-existing, #673):** when a resumed run's terminal CARRIES a prior run's park into its own journal and that carry is refused (oversize or any other store fault), the refusal is logged ("resume from the prior run, not from this one") but the carrying run's own row store has no parked row for the cross-check to find — a resume from the carrying run can still spawn the ordinal live; before #672 the same carry was dropped silently. Filed as #673 (a durable carry-failed mark on the run record; wire key ⇒ @server co-review).
53
+ - **Observable.** `store.append(runId, scope, {callKey, result: >5MiB})` and `store.append(runId, scope, {callKey, parked: >5MiB})` both reject with an `Error` whose `code === "workflow.journal_oversize"`; `load` afterwards shows no row at that ordinal; the store accepts the next fitting entry; a payload of EXACTLY the cap's byte count is accepted (inclusive). The file store leaves no ledger file behind for a refused first entry.
54
+ - **Export surface (+3, additive).** `WorkflowJournalOversizeError` (class: `code`, `callKey`, `bytes`, `cap`), `assertJournalEntryFits(entry)` (the one rule a third-party backend calls from its own `append`), `workflowJournalStoreContract(make, runner?)` (the vitest-free kit: round-trip on both arms + scope wall + oversize refusal on both arms + inclusive cap; `docs/sdk/10-extension-points.md` §7 table + §7.4 semantic-since row). No removals. The **store contract** is what breaks: a backend that skips silently now fails the kit's two oversize cases.
55
+ - **Downstream.** `@server` — the SQL journal twins (`pg`/`tidb`) apply the same rule on BOTH arms: call `assertJournalEntryFits(entry)` (or throw `WorkflowJournalOversizeError`) before the INSERT instead of skip-journaling, on the `result` arm too (the engine's tombstone means the SQL store never sees an oversize completed result either — the refusal there is the same dead-spare-made-honest); bind `workflowJournalStoreContract` to each twin. `@cli` / `@client-core`: zero surface (the refusal lands on the run log line the shell already renders). `@test`: G grid = two stores × two arms + the park-arm disclosure + the run-store-wired resume refusal.
56
+ - Pins: `test/backlog672-journal-oversize-refusal.test.ts` (kit bound to both bundled stores — round-trip whole entries in numeric ordinal order, scope wall before/after the owner's read and after refused cross-scope writes on both arms, oversize ⇒ throw on both arms, inclusive cap; error shape; result arm never hands the store an oversize row + tombstone fits; park arm ONE attempt + disclosure with bytes/cap; run store wired ⇒ resume refused, zero live spawns); `test/file-workflow-journal-store.test.ts` (oversize ⇒ refused on both arms, no ledger file, store usable; RB-243 layer invariant re-pinned as a refusal); `test/defectscan-store-class-fix-matrix.test.ts` §4.1 (both stores answer both arms with the same code, same row count). Mutations: restoring either store's `return` reds that store's two oversize kit cases + §4.1; dropping `code` off the error reds the kit's four oversize cases (both stores) + the file-store pin.
57
+
58
+ ### BREAKING — the auto-mode classifier has no local breaker: an unavailable classifier DENIES the call and says so; every ask is classified on its own (#661 ②③; B-056; ruling: 「分类器不设本地永久熔断,不可用时 fail-closed 拒绝并明说」; CC 2.1.250 form; @cli @client-core @server @test)
59
+ - **The ruling, in one sentence.** CC 2.1.250 has no counting breaker (`unavailableOuterRetries` is a retry count; `tengu_auto_mode_config.enabled` is a remote killswitch), and its `Ze.unavailable` arm DENIES with `decisionReason:{type:"classifier", reason:"Classifier unavailable"}` and the `x1t` sentence. This engine's session latch (three consecutive failures ⇒ every later ask of the session went to a person, no half-open) and its "unavailable ⇒ the ask flows the original chain" arm were both sema-minted and unregistered as divergences; both are gone.
60
+ - **② No breaker.** `createAutoModeDecider` keeps nothing between rounds: a classifier that failed the last call is asked again on the next. Retired — `AutoModeDeciderOptions.failureThreshold` / `onBreakerOpen`, `AutoModeDecider.breakerOpen()` / `consecutiveFailures()`, `RunnerDeps.autoMode.failureThreshold` / `onBreakerOpen`, `AUTO_MODE_DEFAULT_FAILURE_THRESHOLD`; the closed set `AUTO_MODE_BREAKER_CAUSES` / `AutoModeBreakerCause` / `isAutoModeBreakerCause`; the read face `WiringManifest.autoMode.breaker` with `AutoModeBreakerTrip` and the Runner-lived `AutoModeBreakerLedger` (`RunInternals.autoModeBreakerLedger`); `AUTO_MODE_UNAVAILABLE_CAUSES` loses `breaker_open` (now `error | timeout`; the `auto_mode.classified` trace frame's `cause` narrows the same way); `AUTO_MODE_ARM_REASONS` loses `latch_open` (it named a mid-leg re-read face nothing ever minted — a consumer switching exhaustively over the set drops the arm). The persisted arming recipe (`AutoModeArmingRecipe`) loses `failureThreshold` and moves to `AUTO_MODE_ARMING_RECIPE_VERSION = 2`: a version-1 row is refused as unknown (the redemption keeps its honest refusal), never read as version 2 — no compatibility read. The #503 write-side gate "no recipe while the ancestor's breaker is tripped or its streak non-zero" is gone with the latch: an armed layer's recipe is recorded whenever the deployment opted in (`constraintChainEntryOfLayer` no longer reads the decider; `ConstraintChainLayerView.autoMode` is `{ arming?: unknown }`); the persisted auto-mode intent bit is written whenever the leg was armed; the peer-referral tighten no longer carries a breaker conjunct.
61
+ - **③ Unavailable ⇒ deny that says so (narrowing).** At the gate's classifier station and the inherited-lane stations an `unavailable` verdict (model leg threw / rejected / ran past `timeoutMs`; a cancelled round reads `error`) is now a DENY: `decisionReason:"classifier"`, `deniedBy:"classifier"` on `tool_end.gate`, the model-facing sentence `classifierUnavailableDenyMessage(tool, cause)` (CC `x1t`/`EIt`: "The auto-mode classifier is temporarily unavailable (timed out), so auto mode cannot determine the safety of <tool> right now. Wait a moment and then try this action again. If it keeps failing, continue with other tasks that don't require this action and come back to it later. Note: reading files, searching code, and other read-only operations do not require the classifier and can still be used."), and the fact `classifierUnavailable: { cause }` on the deny decision (`PermissionResult` deny arm, additive) and on the deny observer's payload (`PermissionDeniedPayload.classifierUnavailable`, additive). Nobody is asked: `onAsk` is not consulted and nothing parks. The ask-side faces of #616 (`AskRequest.classifierUnavailable`, the park row's `PendingAction.tool_approval.classifierUnavailable`, the checkpoint summary echo) stay as additive display metadata a policy may self-declare, but the ENGINE no longer writes them — retirement candidate for the next wire window (@server: keep reading them as optional).
62
+ - **③ parse_error ⇒ the CC block.** A reply with no verdict (zero `<block>` hits, or a yes/no contradiction, after the one re-ask) is handled as CC 2.1.250 does — a BLOCK (`shouldBlock`, reason `Ure`/R3t): deny with `CLASSIFIER_PARSE_FAILURE_DENY_MESSAGE` ("Auto mode could not evaluate this action and is blocking it for safety — the classifier's reply carried no verdict."), counted by the denial limit like any block (the third consecutive one falls back to a person carrying `denialLimitFallback`), no `classifierUnavailable` fact (the classifier ran). It no longer flows to a person as an unmarked ask.
63
+ - **Derived narrowing — the #503 redemption WITHOUT a recipe (@server S-185).** A cross-process redemption of a row that recorded `autoModeArmed: true` with no recipe (`persistArming` off, the default) must hand back a decider to match the digest, and the only honest one answers `unavailable`. Before: that answer let the ancestor's asks flow to the frozen approver / park for a person again. Now: those asks are DENIED with the classifier-unavailable sentence and `classifierUnavailable: { cause: "error" }` on the deny (attributed, as every inherited-station deny is, to the frozen policy layer: `deniedBy: "policy"`); the approved parked call itself still executes. A deployment that wants the ancestor's classifier to keep deciding on the redeemed leg turns on `persistArming` (the recipe rebuilds it); no step-aside verdict is added for this path (it would be a word CC does not have). Pinned in `test/design503-parked-automode-arming.test.ts` (section E 负控).
64
+ - **Fail-open reverse check (the one arm that still reaches a person).** The question tool: `CLASSIFIER_MAY_ANSWER.content_question === false`, so `AskUserQuestion` never reaches the classifier and has no unavailable arm to fall into — which is also why CC's `vt` exception (fall back to the question dialog when the classifier is unavailable) needs no port: in 250 that branch is dead code (`l2()` is the constant `true`, so `vt` is always `false`), and here the exclusion table already keeps the content ask on its own channel. The denial-limit fallback at the bound (a BLOCK reaching 3 consecutive / 20 total) still asks a person — unchanged, and by design (a bounded fallback, not an outage).
65
+ - **Export surface.** Removed (BREAKING): `AUTO_MODE_BREAKER_CAUSES`, `AutoModeBreakerCause`, `isAutoModeBreakerCause`, `AutoModeBreakerTrip`, `AutoModeBreakerLedger`; the `WiringManifest.autoMode.breaker` key; `AutoModeDecider.breakerOpen/consecutiveFailures`; `AutoModeDeciderOptions.failureThreshold/onBreakerOpen`; `RunnerDeps.autoMode.failureThreshold/onBreakerOpen`; `AutoModeArmingRecipe.failureThreshold`, `AutoModeArmingFace.failureThreshold`, `AutoModeRebuildOptions.onBreakerOpen`; `RunInternals.autoModeBreakerLedger`; the `latch_open` and `breaker_open` words. Added: `classifierUnavailableDenyMessage`, `CLASSIFIER_PARSE_FAILURE_DENY_MESSAGE`; `PermissionResult` deny arm `classifierUnavailable?`; `PermissionDeniedPayload.classifierUnavailable?`.
66
+ - **Downstream.** `@cli` L-147 already CLOSED (1.0.106 never rendered `autoMode.breaker`) — zero cost; render a `deniedBy:"classifier"` `tool_end` whose reason carries "temporarily unavailable" as the classifier-outage card rather than as a policy refusal, and drop the `latch_open` arm from any exhaustive switch over `AUTO_MODE_ARM_REASONS`. `@client-core`: mirror the two closed sets (`AUTO_MODE_UNAVAILABLE_CAUSES` = `error | timeout`; `AUTO_MODE_ARM_REASONS` five words) and the `WiringManifest.autoMode` shape without `breaker`. `@server` S-165 CLOSED (7.69.0 never projected the breaker face) — zero surface; `PermissionDeniedPayload.classifierUnavailable` is additive; the park row's `classifierUnavailable` stays optional and is now never engine-written; `auto_mode.classified` frames never read `cause:"breaker_open"` again. `@test`: black-box criteria G-cells below.
67
+ - Pins: `test/backlog661-unavailable-cc-form.test.ts` (decider: 4 throws ⇒ 4 model calls, no breaker members, timeout independent per round; gate: unavailable(error|timeout) ⇒ deny by `classifier`, approver never consulted, CC sentence with "(timed out)" iff timeout, observer payload carries the fact; parse_error ⇒ block sentence, no fact, counted to the bound; AskUserQuestion never classified; inherited station deny + fact; e2e Runner: four throws ⇒ four classifier requests / four denies / zero executions / zero asks, run 2 classifies afresh, manifest has no breaker key and the fingerprint holds); re-pinned: `test/auto-mode.test.ts`, `test/backlog616-classifier-unavailable.test.ts`, `test/design503-parked-automode-arming.test.ts`, `test/backlog529-manifest-auto-mode.test.ts`, `test/backlog618-classifier-request-shape.test.ts`, `test/backlog661-classifier-cap-retry.test.ts`, `test/design276-peer-referral.test.ts`, `test/rb201-workflow-spawn-review.test.ts`, `test/backlog556-arming-denial-limit-bounds.test.ts`.
68
+
69
+ ### Internal — design/393 S6: `runLocked`'s notification lane and its three legs leave `runtask.ts` as four `run-*.ts` lanes; the S5 views over the driver's `let`s are gone (#671); the S5 attachment-seats tick is closed (byte-invariant but for that one tick)
70
+ - **What moved.** `Runner.runLocked`'s R0 — the notification lane (the resume plan's prepare-side reading, the task-notification queue and detach hub, the park destination with its delegated-terminal escalation, the tier-routing subscription, the ONE injection entry, the peer refs, the model-catalog pin) and the `prepareTask` await — moved whole to `run-notification-lane.ts`; R13 — the leg: the one `harness.prompt` and everything that decides whether it is issued, the RESUME leg (entrance claims, the gated decision, the exhaustion arms, the continuation's assembly under its trust framings and redelivery screens) and the OBJECTIVE leg (the prompt screen, the git frame, the first-frame listings, the pre-call gates, the objective's prompt), the tail's abort facts, the catch's bounded rethrow teardown, the finally's releases — to `run-leg.ts` (one lane, two module-private leg functions: a sibling file would be a lane-to-lane reach); R14 — the terminal adoption (the typed causes lifted over the leg's throw, the final charge, the orphan reconcile and the interruption marker, the durable park's carried input and the held account's settle, the end-of-task compaction) — to `run-terminal-adoption.ts`; R15 — the settle and teardown (the result assembly and publication, the reopen compensation, the seal and workspace settle, the StopFailure observer, the lifecycle terminal, the reap-stash and unpin, the memory harvest, `task.end`, the sub-agent terminal tick, the mooted manual compaction, the forwarded-frame drain + `done` + close, the file-history settle, the suggestion kickoff, and the four teardown legs in their order) — to `run-settle-and-teardown.ts` (layer 3, run lanes), each behind ONE Input (`Run…Input` / `Run…Result`; 89 seats in all: 13 / 25 / 21 / 30). The four async lanes (the notification lane, the attachment seats, the leg, the terminal adoption) take a `next` seat and enter their successor in their own last continuation — the resume ladder's rung shape — so the driver is `runLocked`'s signature, two continuation methods (`runSeatLanes` for R1–R3, `runAssembliesAndLegs` for R4–R12, the two assemblies and the leg chain) and the lane order. `Runner.finish`'s return type is named once (`EndOfTaskCompaction`, contracts.ts); the Runner's four private methods the legs call (`applyResumeDecision`, `finish`, `suggestNextPrompts`, `teardownOwnedEnv`) reach the lanes as delegates on their Inputs; `prepareTask` reaches the notification lane through its Input (a lane may not name the orchestrator). `runtask.ts` 4 587 → 2_514.
71
+ - **#671 — the three views are gone.** Each `let` now lives in the lane that writes it: the notification lane's four bindings (harness, session anchor, identity mint, liveness) are ONE seat on its Result (`NotificationLaneBindings`, contracts.ts) that the identity lane binds and the leg's finally flips; the identity lane's held agent_end account and the reasoning seat's resolution are read faces (`{ readonly current }`) on their Results, read by the terminal adoption and by the result assembly. The moved sink / mint text returns to its bare-variable spelling; the two view seats leave the R1 / R5 Inputs.
72
+ - **The one behavior-surface change: the S5 tick is closed.** S5's attachment-seats lane was awaited by the driver, which put the tool-mount / clock lanes' first host read (`spec.limits.maxWalltimeMs`) ONE microtask after the one-function body's (the S5 收货's (a) ruling, banked for this slice). The lane now takes the same `next` seat and the tick is gone — the 7.11.1 tick structure, since S5 never shipped. Pinned in `test/design393-s6-run-leg-lanes.test.ts`: each successor is entered on the tick its lane's LAST underlying await resolved on — a microtask ladder anchored on that await (`prepareTask` for R0, the session reads for R3, the leg's `flushGitMirror` for R13, `finish` for R14; review r1 showed a ladder on the lane's own promise cannot see an await inserted inside `next`) reads 0 hops at the successor's call for all four lanes (an `await Promise.resolve()` at the head of any `next` reads 1 — four mutants measured), and a `maxWalltimeMs = 0` write inside the attachment lane's settle reaction never reaches the clock lane (the run completes; a driver `await` between R3 and R4 turns it into `failed` / `limits.max_walltime_exceeded`). `runLocked` itself is not `async` either (review r1): a prepare that throws reaches the run IIFE's failure backstop and lock release on the tick it did. What the `next` chain costs instead is at the OTHER end: `runLocked`'s own settlement — the session lock's release and `result()` — lands a few microtasks (one per `return await` frame of the four chained lanes; the driver's two continuation methods are not `async` and add none) after the one-function body's, the same class the resume ladder recorded (order is contract, absolute depth is not); no consumer-visible frame moves — the `done` depth ratchet holds at 58 — and no lane's first host read does.
73
+ - **Byte-invariant otherwise.** dist `runtask.js` differs from the previous state by import lines, the four lane calls, the two continuation methods' frames and the handoff destructures (−1 008 / +55); the four lane files are the only additions; the attachment-seats / identity-wiring / reasoning-seat lanes differ by the seat lines above; no other runtime file changed; the export surface is unchanged. Every await inside a moved region sits on the tick it did (the move proof: each moved region ≡ the base range after the declared renames — `this.deps` → the live deps seat, the four `this.*` methods → delegates, the three view reads → `.current` / `.live`).
74
+ - **Doors that moved with it.** module-size ratchet (runtask banked; four lane entries; the three seat lanes re-banked), `gate:phase-api` floor 859 → 947 and the run- row's residents pin (+4, the async lanes' generic Inputs), `gate:layering` `legLanesNote`, the run-lane residents pin (+4), the rb466 / fail-open / retired-key per-file lists (+4 each), INTEGRATION-CORE's leg anchors re-read (the wake replay and the tool-label map in the driver, the orphan reconcile in the adoption lane, the terminal tick in the settle lane), #670 gains the settle lane as its sixth empty-Result file. `@server @cli @test`: nothing to pick up — no wire key, no closed set, no export moved.
75
+
3
76
  ## 7.11.2 — 2026-09-10
4
77
 
5
78
  ### Fixes — B-057 second half: the `run_in_background:true` arm of the shell probe now mints the read-boundary mandate too (test [6825]; security axis; @server @cli @test)
@@ -8,9 +81,21 @@
8
81
 
9
82
  ### Internal — design/393 S5: `runLocked`'s seat lanes R1–R12 leave `runtask.ts` as eleven `run-*.ts` lanes, and its fourteen parameters become one Input (byte-invariant; zero behavior surface)
10
83
  - **What moved.** `Runner.runLocked`'s R1–R12 — the identity wiring (post-prepare bindings, `ident`, the manifest frame, the harness sinks, the bridges, the idle redelivery, the loop latch, `onReady`), the telemetry and budget seats (pricing / degrade / limits / budget writers, the run record), the attachment seats (the counters / attach groups and the resume re-derivations — the one async lane), the tool-mount facts (mount gates, the write-family resolver, the three task-start trace frames), the reasoning seat, the clock and content seats (the walltime window, the hard timer, `pushContent`, `emitCommitted`), the brain sinks, the compaction machinery (the accounted brain, the breaker, the window-safety builder), the stop gate + final-verification seat, the recovery lanes (the shared forced pass, the loop recovery chain, the guard chain's arm-B seat) and the git lane — moved whole to `run-identity-wiring.ts`, `run-telemetry-and-budget-seats.ts`, `run-attachment-seats.ts`, `run-tool-mount-facts.ts`, `run-reasoning-seat.ts`, `run-clock-and-content.ts`, `run-brain-sinks.ts`, `run-compaction-machinery.ts`, `run-stop-and-final-verify.ts`, `run-recovery-lanes.ts`, `run-git-lane.ts` (layer 3, run lanes), each a factory behind ONE Input (`Run…Input` / `Run…Result`, 88 seats in all) that hands back the values the leg reads under the same names. The driver keeps R0 (the notification lane + prepare), the two assemblies (the harness handlers — R8 — and the turn-boundary call, each the S2 lane's call site; the design's 18-seat R11 is cut there, leaving the recovery lane at 10 seats) and the legs; it mints the run state (`createRunState`, a pure zero-value, ~50 lines earlier than before) and three views over its own `let`s (the notification lane's bindings, the held agent_end account, the reasoning resolution) so a lane writes the driver's variable, never a copy. `runLocked(spec, queue, …, entryTracer)` — fourteen positional parameters — is now `runLocked(input: RunLockedInput)` (driver-private; the seat types are the contracts.ts seats the stream lanes already spell). The three compaction knobs (`MAX_CONSECUTIVE_COMPACTION_FAILURES`, `COMPACTION_REGROWTH_FACTOR`, `COMPACTION_FREED_EPSILON`) sank from the turn-boundary lane to `compaction-knobs.ts` (layer 1): the recovery lane reads them too, and a lane may not name a sibling. `STOP_HOOK_BLOCK_CAP` moved with the stop gate.
11
- - **Byte-invariant.** dist `runtask.js` differs from the previous release by import lines, the call-site object literal, the `RunLockedInput` destructure, the three views and the eleven lane calls (−1 117 / +≈85 lines, of which the run IIFE's `setResult` callback is re-indented under its key, not changed); `run-turn-boundary.js` by three constant declarations out and one import in; the eleven lane files and `compaction-knobs.js` are the only additions; no other runtime file changed; the export surface is unchanged. Every lane whose top-level await count is 0 (all but one) is a synchronous factory; the attachment-seats lane is `async` and is awaited where its session reads always sat — inside it every read sits on the tick it did, and the driver's continuation after it is ONE microtask later than the one-function body's (the 390 v2.0 ruling ① class the design named for R3; nothing between the lane's return and the driver's next read can write what that read reads). `runtask.ts` 6 594 → 4 587.
84
+ - **Byte-invariant.** dist `runtask.js` differs from the previous release by import lines, the call-site object literal, the `RunLockedInput` destructure, the three views and the eleven lane calls (−1 117 / +≈85 lines, of which the run IIFE's `setResult` callback is re-indented under its key, not changed); `run-turn-boundary.js` by three constant declarations out and one import in; the eleven lane files and `compaction-knobs.js` are the only additions; no other runtime file changed; the export surface is unchanged. Every lane whose top-level await count is 0 (all but one) is a synchronous factory; the attachment-seats lane is `async` and is awaited where its session reads always sat — inside it every read sits on the tick it did, and the driver's continuation after it is ONE microtask later than the one-function body's (the 390 v2.0 ruling ① class the design named for R3; nothing between the lane's return and the driver's next read can write what that read reads — CORRECTED by S6 below: a host that writes `spec.limits.*` in a microtask can, and the S6 segment closes the tick). `runtask.ts` 6 594 → 4 587.
12
85
  - **Doors that moved with it.** module-size ratchet (runtask and run-turn-boundary banked; eleven lane entries), `gate:phase-api` floor 771 → 859 and the run- row's residents pin, `gate:layering` `seatLanesNote` + `compaction-knobs` on layer 1, the run-lane residents pin, the rb466 / fail-open / retired-key per-file lists (+11 each), the reminder-literal rows (`fvMarked` / the bare final-verification probe → `run-attachment-seats.ts`), the nuia baseline regenerated, INTEGRATION-CORE's two driver-assembly anchors re-read (the rest stay #667's), the RunState writer note in contracts.ts. `@server @cli @test`: nothing to pick up — no wire key, no closed set, no export moved.
13
86
 
87
+ ## 7.11.1 — 2026-09-09
88
+
89
+ ### Fixes — a read the DEPLOYMENT'S READ BOUNDARY demoted is a mandated ask; the read-only shell arm cannot clear it (B-057; security axis; @server 7.69.0 six cells @test @cli)
90
+ - **The regression (7.11.0 #619).** `bashReversibilityProbe` minted `mandated` only for an operand OUTSIDE the roots; a deny-listed operand (`grep needle secrets/app.txt` under `readDenyPatterns:["**/secrets/**"]`) and a recursive walk under a wired deny judge (`grep -r needle sub`, `du`) came back as a bare `reversible:false` — an "ordinary" classify-tier ask — and the new read-only arm (a reader of command text, blind to paths) retired it and the command ran, with `decisionReason:"read_only"`. Fail-open; measured red first.
91
+ - **The rule, ONE arm (`boundaryGate`):** a demotion the read boundary raised is structural — outside the roots, on the deny judge (new structured `CompoundReadonlyVerdict.readDenied`, stamped where the deny arm demotes), or a recursive walk under the deny judge — and neither a stored allow rule nor the read-only arm may retire it (`probe_mandate`, the same word #502 minted). Rules do not grow: the mandate's trigger widens from "outside the roots" to "the boundary spoke". No wire change; `ReversibilityVerdict.mandated` unchanged in shape.
92
+ - The same rule covers a recursion the classifier cannot bound even with NO deny pattern: `find . -name x && pwd` under `shellGate:"classify"` asks once again (the 7.11.0 reading that let the read-only arm clear it was the same fail-open, pinned in `test/backlog482-compound-readonly-e2e.test.ts`).
93
+ - Observable: with `readFace:"roots"`, `shellGate:"classify"` and a deny pattern, those three commands ask ONCE (`origin:"shell_gate_tighten"`), no `permission.read_only_allowed` trace, and run only after the person answers; with no pattern (or one that matches nothing) the same readers are reversible at the fold and ask nothing. Pins: `test/backlog-b057-read-boundary-mandate.test.ts`.
94
+
95
+ ### Fixes — a `spec.tools` entry the ToolSpec arm cannot rebuild is refused by name with a stable code (#666 ③; test [6794] G4.b; @server @test)
96
+ - The caller mount rebuilds a raw ToolSpec by spreading it, so an entry whose `name` / `execute` live on a prototype (a class instance, `Object.create(spec)`, `Object.create(product)`), a function carrying the brand, or a Proxy lying about its own keys mounted with `name: undefined` and failed on an unrelated roster read (`undefined.startsWith`). ONE predicate — can the spec arm construct this entry (own enumerable string `name` + own function `execute`)? — now refuses the whole family with terminal code **`config.tool_mount_denied`** (registered, terminal) and a remedy sentence shared with the rebind seal (plain-object ToolSpec / the product itself / the supported wrapper form via `stampDefineToolBrand`). The 7.11.0-era brand-inheritance arm (`inheritsDefineToolBrand`) is retired — it closed only the branded subset. Pins: the four shapes through a real Runner + the own-property control.
97
+
98
+
14
99
  ### Internal — design/393 S4: the TaskStream façade leaves `runtask.ts` as five `stream-*.ts` lanes (byte-invariant; zero behavior surface)
15
100
  - **What moved.** `Runner.runTaskStream`'s T3–T7 — the settle backstop (the `run.catch` handler: checkpoint reopen compensation, terminal resume-failure unpin/destroy, the `failed` terminal mint, the backstop `task.end`, the owed delegation terminal, the drained `done` push), the suspended-run reap (`reapSuspended`), and the eight verb closures (`steer`; `notify` / `optOutMemoryCapture` / `compact` / `detach` / `interrupt`; `halt` / `destroy`) — moved whole to `stream-settle-backstop.ts`, `stream-reap.ts`, `stream-steer-verb.ts`, `stream-lifecycle-verbs.ts`, `stream-halt-verbs.ts` (layer 3, run lanes), each a factory behind ONE Input (`Stream…Input` / `Stream…Result`, 43 seats in all) that hands back the closure(s) the stream object carries under the same names. The driver keeps the seats (T1), the run IIFE (T2) and the eleven-name stream object; it mints the lanes' live view (`TaskStreamLiveSeat` — getters over `resultValue` / `handle` / `reapHandle`, one setter for the backstop's mint) and threads the Runner's registries and live deps seat as borrowed seats. Six seat types (`LiveHandle`, `TaskIdRef`, `ManualCompactRef`, `NotifyRef`, `CaptureOptOutRef`, `TaskStreamLiveSeat`) went down to `contracts.ts` from the driver's inline annotations. `steerChain` and `destroyOnce` moved with the only verb that read each.
16
101
  - **Byte-invariant.** dist `runtask.js` differs from the previous release by import lines, the live-seat mint, five factory calls and eight property names only (−510 / +34); the five new files are the only additions; no other runtime file changed; the export surface is unchanged. Every await inside a moved closure sits on the tick it did: the lanes return the same functions the driver still installs in the same positions (`run.catch(onRunRejected)`, the stream object's properties), and every pre-await host read (`live.resultValue`, `live.handle`, the options bags) is the same read on the same tick. `runtask.ts` 7 565 → 6 594.
@@ -26,18 +111,6 @@
26
111
  - `@cli` 1.0.106: pick up this version with 7.11.0 — a thinking-locked classifier model is now asked for `low` inside a 2304-token cap instead of being sent an off it ignores (where the catalog maps `low` onto the wire, it thinks at that tier; where it does not, at its default — see the residual); a model that exhausts the cap still fails once per ask; nothing to render. `@test`: known cannot-off (a catalog entry `reasoning:true` on the openai wire with no `thinkingLevelMap.off`, or anthropic adaptive) ⇒ the FIRST classifier request carries `reasoning:"low"` and `max_tokens` 2304, one request; an unknown entry (no `reasoning`) answering empty `finish_reason:"length"` then `<block>no</block>` ⇒ two requests, the second `low` at 2304; a can-off entry (deepseek format / declared off spelling) ⇒ `off` at 256, one request. `@server`: zero surface.
27
112
  - Pins: `test/backlog661-classifier-cap-retry.test.ts` (the seat predicate — cannot-off adaptive and stock-openai first request `low` + 2304; can-off `off` + 256 ×3; the re-ask's second request `low`; cannot-off empty ⇒ one request); `test/backlog618-classifier-request-shape.test.ts` re-pinned (the no-cap seat is gone: cannot-off ⇒ `low` + 2304).
28
113
 
29
- ## 7.11.1 — 2026-09-09
30
-
31
- ### Fixes — a read the DEPLOYMENT'S READ BOUNDARY demoted is a mandated ask; the read-only shell arm cannot clear it (B-057; security axis; @server 7.69.0 six cells @test @cli)
32
- - **The regression (7.11.0 #619).** `bashReversibilityProbe` minted `mandated` only for an operand OUTSIDE the roots; a deny-listed operand (`grep needle secrets/app.txt` under `readDenyPatterns:["**/secrets/**"]`) and a recursive walk under a wired deny judge (`grep -r needle sub`, `du`) came back as a bare `reversible:false` — an "ordinary" classify-tier ask — and the new read-only arm (a reader of command text, blind to paths) retired it and the command ran, with `decisionReason:"read_only"`. Fail-open; measured red first.
33
- - **The rule, ONE arm (`boundaryGate`):** a demotion the read boundary raised is structural — outside the roots, on the deny judge (new structured `CompoundReadonlyVerdict.readDenied`, stamped where the deny arm demotes), or a recursive walk under the deny judge — and neither a stored allow rule nor the read-only arm may retire it (`probe_mandate`, the same word #502 minted). Rules do not grow: the mandate's trigger widens from "outside the roots" to "the boundary spoke". No wire change; `ReversibilityVerdict.mandated` unchanged in shape.
34
- - The same rule covers a recursion the classifier cannot bound even with NO deny pattern: `find . -name x && pwd` under `shellGate:"classify"` asks once again (the 7.11.0 reading that let the read-only arm clear it was the same fail-open, pinned in `test/backlog482-compound-readonly-e2e.test.ts`).
35
- - Observable: with `readFace:"roots"`, `shellGate:"classify"` and a deny pattern, those three commands ask ONCE (`origin:"shell_gate_tighten"`), no `permission.read_only_allowed` trace, and run only after the person answers; with no pattern (or one that matches nothing) the same readers are reversible at the fold and ask nothing. Pins: `test/backlog-b057-read-boundary-mandate.test.ts`.
36
-
37
- ### Fixes — a `spec.tools` entry the ToolSpec arm cannot rebuild is refused by name with a stable code (#666 ③; test [6794] G4.b; @server @test)
38
- - The caller mount rebuilds a raw ToolSpec by spreading it, so an entry whose `name` / `execute` live on a prototype (a class instance, `Object.create(spec)`, `Object.create(product)`), a function carrying the brand, or a Proxy lying about its own keys mounted with `name: undefined` and failed on an unrelated roster read (`undefined.startsWith`). ONE predicate — can the spec arm construct this entry (own enumerable string `name` + own function `execute`)? — now refuses the whole family with terminal code **`config.tool_mount_denied`** (registered, terminal) and a remedy sentence shared with the rebind seal (plain-object ToolSpec / the product itself / the supported wrapper form via `stampDefineToolBrand`). The 7.11.0-era brand-inheritance arm (`inheritsDefineToolBrand`) is retired — it closed only the branded subset. Pins: the four shapes through a real Runner + the own-property control.
39
-
40
-
41
114
  ## 7.11.0 — 2026-09-09
42
115
 
43
116
  ### Fixes — the auto-mode classifier reads its verdict the way CC 2.1.250 does, and re-asks once when a capped reply came back empty (#661 ① ⑤; B-056; @cli 1.0.106 @test @server)
@@ -172,6 +172,11 @@ export type RealApprovalTableCoversEveryOrigin = AssertAllKeysHandled<Exclude<As
172
172
  * deployment armed the classifier to resolve (the classify doctrine's whole point for the shell
173
173
  * tier; the safety marks make the ask NON-BUDGETABLE at the durable park, a different axis from who
174
174
  * may answer it live).
175
+ * A PROBE mandate riding the same `shell_gate_tighten` word (a read-boundary / out-of-root /
176
+ * recursive-read probe verdict, or a probe that did not answer) is NOT this table's to admit: the
177
+ * gate's `probeBoundaryMandate` excludes it at both classifier stations, the way the allow layer
178
+ * excludes it from a standing rule. This table says which QUESTIONS are the classifier's; the probe
179
+ * words say which asks are nobody's but a person's.
175
180
  * - `policy`: YES — including a safety policy's `requiresRealApproval` ask (a live verdict is
176
181
  * judgment, and arming auto mode is the deployment's explicit choice to let the classifier be the
177
182
  * explicit ask-resolution for those classes).
@@ -195,4 +200,54 @@ declare const CLASSIFIER_MAY_ANSWER: {
195
200
  export type ClassifierTableCoversEveryOrigin = AssertAllKeysHandled<Exclude<AskOrigin, keyof typeof CLASSIFIER_MAY_ANSWER>>;
196
201
  /** Whether the auto-mode classifier may resolve an ask of this origin (see the table's contract). */
197
202
  export declare function classifierMayAnswer(origin: AskOrigin): boolean;
203
+ /**
204
+ * The mandate provenance of one call, judged from the SAME mark inputs the gate is driven with —
205
+ * the single source for "could a persisted allow rule clear this ask?". Allow rules silence the
206
+ * classifier's questions, never a mandated one, and this predicate is the mandated-family half of
207
+ * that boundary (the real-approval/governance half rides the decision's own `requiresRealApproval`
208
+ * bit, which the org layer stamps):
209
+ * · `probeMandated` — this CALL's own reversibility probe declared its demotion STRUCTURAL
210
+ * (`"probe_mandate"`), judged FIRST because it is the only per-call member here: the three below
211
+ * are properties of the TOOL and are true of every call on the seat, so a mandate that is true of
212
+ * this one call must not be shadowed by the tier that happens to carry it (#502: the built-in
213
+ * shell probe raises it for a listed reader naming a path outside the session's roots — a boundary
214
+ * the deployment declared, which is exactly what the classify tier alone cannot say);
215
+ * · `egress` — the tool's own external-write mark, judged next: it is the tool's declaration even
216
+ * when the coarse doctrine also installed a shell tier on the same seat;
217
+ * · `shellGated` + tier `"always"` — the operator's per-call confirmation doctrine
218
+ * (`"operator_always"`); the classify doctrine installs `"maybe"`, and THOSE asks stay the rule
219
+ * lane's home turf (`undefined`) — that is the don't-ask-again main case, and the per-call member
220
+ * above is deliberately the ONLY thing that carves a mandate out of it;
221
+ * · a tool's OWN `"always"`/`"maybe"` irreversibility tier without the doctrine (`"tool_marks"`).
222
+ *
223
+ * Two consumers, one derivation: the gate's silencing arm (a matching rule is disclosed as shadowed
224
+ * instead of clearing the ask) and the runner's suggestion factory (a mandated ask offers no
225
+ * "stop asking me this" option — a rule minted from it would never clear it). A drift between the
226
+ * two would let a card offer a rule the lane then refuses to honor. The per-call member reaches both
227
+ * the same way every other per-call fact does: the gate stamps it on the surviving ask, the
228
+ * synchronous sites read it off the decision they spread, and the park leg threads its own parameter.
229
+ */
230
+ export declare function persistedRuleMandateOf(marks: {
231
+ egress?: boolean;
232
+ shellGated?: boolean;
233
+ irreversibility?: "never" | "maybe" | "always";
234
+ /** #502: the surviving ask's engine-stamped `probeMandated` — see
235
+ * {@link import("./types.js").ReversibilityVerdict.mandated}. */
236
+ probeMandated?: boolean;
237
+ /** The pass's own record that the probe was consulted and did not answer (`GatePass.probeUnanswered`):
238
+ * the boundary is unknown, which no standing yes may stand in for. The runner's suggestion factory does
239
+ * not see the pass and never passes it — a card may offer a rule the lane then discloses as shadowed
240
+ * (loud, never a silent override), the same drift window the `matchedAskRule` conjunct already has. */
241
+ probeUnanswered?: boolean;
242
+ }): "operator_always" | "tool_marks" | "probe_mandate" | "probe_unanswered" | undefined;
243
+ /** The PROBE half of the mandated family (the deployment's read boundary, judged per call): neither a standing rule
244
+ * nor the classifier clears these two words; `tool_marks`/`operator_always` bind the rule lane only (`safety_tighten`). */
245
+ export declare function probeBoundaryMandate(word: ReturnType<typeof persistedRuleMandateOf>): boolean;
246
+ /** An ASK that carries its probe's mandate — the per-call member the gate stamped (`probeMandated`), read
247
+ * through the same family word so every consumer (the inherited classifier station, the two ancestor
248
+ * sandbox-admission arms) is one predicate, not three spellings. Adding a probe word to the family
249
+ * changes all of them at once. */
250
+ export declare function probeMandatedAsk(ask: {
251
+ probeMandated?: boolean;
252
+ }): boolean;
198
253
  export {};
@@ -62,3 +62,24 @@ const CLASSIFIER_MAY_ANSWER = {
62
62
  export function classifierMayAnswer(origin) {
63
63
  return CLASSIFIER_MAY_ANSWER[origin];
64
64
  }
65
+ export function persistedRuleMandateOf(marks) {
66
+ return marks.probeMandated === true
67
+ ? "probe_mandate"
68
+ : marks.probeUnanswered === true
69
+ ? "probe_unanswered"
70
+ : marks.egress === true
71
+ ? "tool_marks"
72
+ : marks.shellGated === true
73
+ ? marks.irreversibility === "always"
74
+ ? "operator_always"
75
+ : undefined
76
+ : marks.irreversibility === "always" || marks.irreversibility === "maybe"
77
+ ? "tool_marks"
78
+ : undefined;
79
+ }
80
+ export function probeBoundaryMandate(word) {
81
+ return word === "probe_mandate" || word === "probe_unanswered";
82
+ }
83
+ export function probeMandatedAsk(ask) {
84
+ return probeBoundaryMandate(persistedRuleMandateOf({ probeMandated: ask.probeMandated === true }));
85
+ }
@@ -2,18 +2,18 @@ import type { AutoModeRules, AutoModeWindowOptions } from "./auto-mode-prompt.js
2
2
  import type { AutoModeDenialLimitOptions } from "./auto-mode.js";
3
3
  /** The recipe format's version. A reader that does not know a version REFUSES it (never guesses): the
4
4
  * recipe names the criteria a classifier enforces, and a partially-understood criteria set is the one
5
- * thing a permission gate may not improvise. */
6
- export declare const AUTO_MODE_ARMING_RECIPE_VERSION = 1;
5
+ * thing a permission gate may not improvise. Version 2: the breaker threshold (`failureThreshold`) left the
6
+ * criteria set with the breaker itself; a version-1 recipe named a criterion no decider enforces any more and is
7
+ * refused as unknown (the redemption keeps its honest refusal), never read as a version-2 recipe. */
8
+ export declare const AUTO_MODE_ARMING_RECIPE_VERSION = 2;
7
9
  /**
8
10
  * The SERIALIZABLE half of an auto-mode arming — everything `createAutoModeDecider` +
9
11
  * `buildAutoModePrompt` need except the model leg itself. Plain data by construction (JSON /
10
12
  * `structuredClone` round-trips), because it rides a durable checkpoint row and is bound by the
11
13
  * constraint-chain digest.
12
14
  *
13
- * Deliberately NOT carried: `onBreakerOpen` (a closure the redeeming deployment wires its own alarm
14
- * seat, which is where the operator watching THAT process can see it) and the model/roster selection
15
- * (the redeeming deployment routes its own classifier leg; a recorded model id would name a catalog
16
- * entry that need not exist in the redeeming fleet).
15
+ * Deliberately NOT carried: the model/roster selection (the redeeming deployment routes its own classifier
16
+ * leg; a recorded model id would name a catalog entry that need not exist in the redeeming fleet).
17
17
  */
18
18
  export interface AutoModeArmingRecipe {
19
19
  /** {@link AUTO_MODE_ARMING_RECIPE_VERSION}. An unknown version is refused, never partially read. */
@@ -28,15 +28,13 @@ export interface AutoModeArmingRecipe {
28
28
  window?: AutoModeWindowOptions;
29
29
  /** Classify round-trip cap, ms. */
30
30
  timeoutMs?: number;
31
- /** Consecutive-failure threshold opening the one-way breaker (floored, as the decider itself floors it). */
32
- failureThreshold?: number;
33
31
  /**
34
32
  * #556 — the classifier DENIAL-LIMIT bounds (`RunnerDeps.autoMode.denialLimit`, CC 2.1.250 `FO`/`AKe`):
35
33
  * how many blocks the ancestor's classifier was allowed before a person had to look, and the window the
36
34
  * fallback ask ran under. A KNOB triple, not prompt body — every member is orderable, so the fold takes
37
35
  * the strictest of the two sides rather than refusing on a difference.
38
36
  *
39
- * It belongs on the recipe because the tracker CANNOT travel (it is live state, like the breaker) while
37
+ * It belongs on the recipe because the tracker CANNOT travel (it is live state) while
40
38
  * its BOUNDS are exactly the kind of criteria the recipe exists to carry: without them a cross-process
41
39
  * redemption rebuilt the ancestor's classifier and counted its blocks against the REDEEMING
42
40
  * deployment's own bounds, which may be looser — the ancestor's "three strikes and a person looks"
@@ -82,7 +80,6 @@ export interface AutoModeArmingFace {
82
80
  sessionContext?: readonly string[];
83
81
  window?: AutoModeWindowOptions;
84
82
  timeoutMs?: number;
85
- failureThreshold?: number;
86
83
  denialLimit?: AutoModeDenialLimitOptions;
87
84
  settingsEpoch?: string;
88
85
  /** `true` when the cross-session lane's classifier rule is spliced into this deployment's classifier
@@ -174,10 +171,9 @@ export type AutoModeArmingFold = {
174
171
  * ANCESTOR recorded, which is what its digest authenticates), so every later redemption is bounded by
175
172
  * ITS OWN declared settings rather than by whatever the previous redeemer happened to run under. A
176
173
  * deployment that wants a tightening to be durable tightens its own settings, where it is auditable.
177
- * · bodies EQUAL ⇒ rebuild, with `timeoutMs`/`failureThreshold` taking the MINIMUM of the two. Both
178
- * directions of that minimum are fail-closed: a shorter timeout turns a slow classifier into
179
- * `unavailable` (the original chain, i.e. a human), and a lower threshold opens the one-way breaker
180
- * sooner (the session falls back to non-auto). Neither can widen a verdict. The denial-limit triple
174
+ * · bodies EQUAL ⇒ rebuild, with `timeoutMs` taking the MINIMUM of the two — fail-closed: a shorter
175
+ * timeout turns a slow classifier into `unavailable` (a deny that says so) sooner, and can never widen
176
+ * a verdict. The denial-limit triple
181
177
  * (#556) folds the same way through {@link tightenDenialLimit} — fewer blocks before a person looks,
182
178
  * and no window ever lengthened or removed.
183
179
  * · bodies DIFFER ⇒ refuse. `settings_moved` normally; `epoch_inconsistent` when the two sides
@@ -1,5 +1,5 @@
1
- import { AUTO_MODE_DEFAULT_FAILURE_THRESHOLD, AUTO_MODE_DEFAULT_TIMEOUT_MS, AUTO_MODE_DEFAULT_WINDOW_MAX_CHARS, AUTO_MODE_DEFAULT_WINDOW_MAX_ENTRIES, AUTO_MODE_DEFAULTS_SENTINEL, AUTO_MODE_DENIAL_AUTO_DENY_DEFAULT_MS, AUTO_MODE_DENIAL_LIMIT_DEFAULTS, } from "./auto-mode-defaults.js";
2
- export const AUTO_MODE_ARMING_RECIPE_VERSION = 1;
1
+ import { AUTO_MODE_DEFAULT_TIMEOUT_MS, AUTO_MODE_DEFAULT_WINDOW_MAX_CHARS, AUTO_MODE_DEFAULT_WINDOW_MAX_ENTRIES, AUTO_MODE_DEFAULTS_SENTINEL, AUTO_MODE_DENIAL_AUTO_DENY_DEFAULT_MS, AUTO_MODE_DENIAL_LIMIT_DEFAULTS, } from "./auto-mode-defaults.js";
2
+ export const AUTO_MODE_ARMING_RECIPE_VERSION = 2;
3
3
  const isPlainRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
4
4
  function canonicalStrings(v) {
5
5
  if (v === undefined)
@@ -76,9 +76,8 @@ export function sanitizeAutoModeArmingRecipe(value) {
76
76
  const settingsDenyRules = canonicalStrings(value.settingsDenyRules);
77
77
  const sessionContext = canonicalStrings(value.sessionContext);
78
78
  const timeoutMs = canonicalNumber(value.timeoutMs, 1, false);
79
- const failureThreshold = canonicalNumber(value.failureThreshold, 1, true);
80
79
  const denialLimit = canonicalDenialLimit(value.denialLimit);
81
- if (settingsDenyRules === null || sessionContext === null || timeoutMs === null || failureThreshold === null || denialLimit === null)
80
+ if (settingsDenyRules === null || sessionContext === null || timeoutMs === null || denialLimit === null)
82
81
  return undefined;
83
82
  let rules;
84
83
  if (value.rules !== undefined) {
@@ -132,7 +131,6 @@ export function sanitizeAutoModeArmingRecipe(value) {
132
131
  ...(sessionContext !== undefined ? { sessionContext } : {}),
133
132
  ...(window !== undefined ? { window } : {}),
134
133
  ...(timeoutMs !== undefined ? { timeoutMs } : {}),
135
- ...(failureThreshold !== undefined ? { failureThreshold } : {}),
136
134
  ...(denialLimit !== undefined ? { denialLimit } : {}),
137
135
  ...(settingsEpoch !== undefined && settingsEpoch !== "" ? { settingsEpoch } : {}),
138
136
  };
@@ -146,7 +144,6 @@ export function autoModeArmingRecipeOf(face, bind) {
146
144
  ...(face.sessionContext !== undefined ? { sessionContext: face.sessionContext } : {}),
147
145
  ...(face.window !== undefined ? { window: face.window } : {}),
148
146
  ...(face.timeoutMs !== undefined ? { timeoutMs: face.timeoutMs } : {}),
149
- ...(face.failureThreshold !== undefined ? { failureThreshold: face.failureThreshold } : {}),
150
147
  ...(face.denialLimit !== undefined ? { denialLimit: face.denialLimit } : {}),
151
148
  ...(face.settingsEpoch !== undefined ? { settingsEpoch: face.settingsEpoch } : {}),
152
149
  ...(face.crossSessionMessagesRule !== undefined ? { crossSessionMessagesRule: face.crossSessionMessagesRule } : {}),
@@ -213,7 +210,6 @@ export function foldAutoModeArming(recorded, current) {
213
210
  }
214
211
  const minKnob = (a, b, fallback) => Math.min(a ?? fallback, b ?? fallback);
215
212
  const timeoutMs = minKnob(rec.timeoutMs, cur.timeoutMs, AUTO_MODE_DEFAULT_TIMEOUT_MS);
216
- const failureThreshold = minKnob(rec.failureThreshold, cur.failureThreshold, AUTO_MODE_DEFAULT_FAILURE_THRESHOLD);
217
213
  const denialLimit = tightenDenialLimit(rec.denialLimit, cur.denialLimit);
218
214
  const recordedDenial = tightenDenialLimit(rec.denialLimit, rec.denialLimit);
219
215
  return {
@@ -222,12 +218,10 @@ export function foldAutoModeArming(recorded, current) {
222
218
  effective: {
223
219
  ...rec,
224
220
  timeoutMs,
225
- failureThreshold,
226
221
  denialLimit,
227
222
  ...(cur.settingsEpoch !== undefined ? { settingsEpoch: cur.settingsEpoch } : {}),
228
223
  },
229
224
  tightened: timeoutMs < (rec.timeoutMs ?? AUTO_MODE_DEFAULT_TIMEOUT_MS) ||
230
- failureThreshold < (rec.failureThreshold ?? AUTO_MODE_DEFAULT_FAILURE_THRESHOLD) ||
231
225
  denialLimit.maxConsecutive < recordedDenial.maxConsecutive ||
232
226
  denialLimit.maxTotal < recordedDenial.maxTotal ||
233
227
  denialLimit.autoDenyAfterMs !== recordedDenial.autoDenyAfterMs,
@@ -1,7 +1,5 @@
1
1
  /** Hard cap on one classification round-trip when `AutoModeDeciderOptions.timeoutMs` is omitted. */
2
2
  export declare const AUTO_MODE_DEFAULT_TIMEOUT_MS = 15000;
3
- /** Consecutive-failure streak that opens the session breaker when `failureThreshold` is omitted. */
4
- export declare const AUTO_MODE_DEFAULT_FAILURE_THRESHOLD = 3;
5
3
  /** Newest transcript entries in the classify window when `AutoModeWindowOptions.maxEntries` is omitted. */
6
4
  export declare const AUTO_MODE_DEFAULT_WINDOW_MAX_ENTRIES = 40;
7
5
  /** Per-entry excerpt cap when `AutoModeWindowOptions.maxCharsPerEntry` is omitted. */
@@ -1,5 +1,4 @@
1
1
  export const AUTO_MODE_DEFAULT_TIMEOUT_MS = 15_000;
2
- export const AUTO_MODE_DEFAULT_FAILURE_THRESHOLD = 3;
3
2
  export const AUTO_MODE_DEFAULT_WINDOW_MAX_ENTRIES = 40;
4
3
  export const AUTO_MODE_DEFAULT_WINDOW_MAX_CHARS = 2_000;
5
4
  export const AUTO_MODE_CLASSIFIER_MAX_TOKENS = 256;
@@ -1,4 +1,4 @@
1
- import { type AutoModeDecider, type AutoModeDeciderOptions } from "./auto-mode.js";
1
+ import { type AutoModeDecider } from "./auto-mode.js";
2
2
  import { type AutoModeArmingRecipe, type AutoModeRebuildRefusal } from "./auto-mode-arming.js";
3
3
  import type { Message } from "../internal/llm.js";
4
4
  /** The redeeming deployment's FRESH model leg: one tool-less completion over a prompt CORE assembles.
@@ -28,11 +28,9 @@ export interface AutoModeRebuildOptions {
28
28
  * an affirmative claim, "this is the run's first gated action", that would simply be false. A provider
29
29
  * that returns `[]` is DECLARING the transcript empty, which is a different statement from an engine
30
30
  * that silently had none to show. A provider that throws fails the classification closed
31
- * (`unavailable` ⇒ the original chain), never into a blind verdict.
31
+ * (`unavailable` ⇒ a deny that says so), never into a blind verdict.
32
32
  */
33
33
  transcript: () => readonly Message[] | Promise<readonly Message[]>;
34
- /** The redeeming process's breaker alarm (the recorded arming cannot carry the ancestor's closure). */
35
- onBreakerOpen?: AutoModeDeciderOptions["onBreakerOpen"];
36
34
  }
37
35
  export type AutoModeRebuildResult = {
38
36
  ok: true;
@@ -59,16 +57,11 @@ export type AutoModeRebuildResult = {
59
57
  *
60
58
  * What is reproduced: the assembled system prompt (`buildAutoModePrompt` over the recorded rule
61
59
  * overrides / settings-deny rules / session context, plus the engine's cross-session lane rule when
62
- * the recipe says the arming leg spliced it), the transcript-window bounds, the round-trip
63
- * timeout and the breaker threshold — i.e. every input the ancestor's own `createAutoModeDecider` call
64
- * had except the model leg and the alarm closure.
60
+ * the recipe says the arming leg spliced it), the transcript-window bounds and the round-trip
61
+ * timeout — i.e. every input the ancestor's own `createAutoModeDecider` call had except the model leg.
62
+ * A decider carries no state between rounds, so there is no ancestor state to reproduce or to withhold.
65
63
  *
66
- * What is NOT reproduced, deliberately: the ancestor's BREAKER STATE. The rebuilt decider starts closed,
67
- * and that is sound only because a recipe is never recorded from a decider whose breaker is open OR whose
68
- * failure streak is non-zero (bound ①, at the persistence point) — either state means the ancestor's own
69
- * budget was spent or spending, and the row then carries no recipe at all.
70
- *
71
- * Also NOT reproduced: this build's PROMPT ASSETS are its own. The recipe records the deployment's
64
+ * NOT reproduced: this build's PROMPT ASSETS are its own. The recipe records the deployment's
72
65
  * overrides, so the assembled prompt is checked against the digest the arming recorded — a fleet running
73
66
  * two asset versions refuses to rebuild rather than enforce a different set of default rules under the
74
67
  * ancestor's name.
@@ -31,8 +31,6 @@ export function rebuildAutoModeDecider(opts) {
31
31
  }
32
32
  const decider = createAutoModeDecider({
33
33
  ...(effective.timeoutMs !== undefined ? { timeoutMs: effective.timeoutMs } : {}),
34
- ...(effective.failureThreshold !== undefined ? { failureThreshold: effective.failureThreshold } : {}),
35
- ...(opts.onBreakerOpen !== undefined ? { onBreakerOpen: opts.onBreakerOpen } : {}),
36
34
  classify: async (input, signal) => {
37
35
  const messages = await opts.transcript();
38
36
  const userPrompt = renderAutoModeWindow(messages, effective.window) + renderAutoModeAction(input);