@sema-agent/core 7.11.2 → 7.12.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 (57) hide show
  1. package/CHANGELOG.md +42 -13
  2. package/dist/core/auto-mode-arming.d.ts +10 -14
  3. package/dist/core/auto-mode-arming.js +3 -9
  4. package/dist/core/auto-mode-defaults.d.ts +0 -2
  5. package/dist/core/auto-mode-defaults.js +0 -1
  6. package/dist/core/auto-mode-rebuild.d.ts +6 -13
  7. package/dist/core/auto-mode-rebuild.js +0 -2
  8. package/dist/core/auto-mode.d.ts +30 -89
  9. package/dist/core/auto-mode.js +12 -59
  10. package/dist/core/checkpoint-store.d.ts +1 -3
  11. package/dist/core/gate-fold.js +1 -9
  12. package/dist/core/gate-lanes.js +15 -9
  13. package/dist/core/hooks.d.ts +6 -0
  14. package/dist/core/runner/contracts.d.ts +41 -9
  15. package/dist/core/runner/denial-limit-arms.d.ts +10 -13
  16. package/dist/core/runner/denial-limit-arms.js +9 -7
  17. package/dist/core/runner/gate-exit.js +9 -1
  18. package/dist/core/runner/prepare-caps-and-workflow.d.ts +1 -1
  19. package/dist/core/runner/prepare-caps-and-workflow.js +0 -5
  20. package/dist/core/runner/prepare-suspend-saga.d.ts +0 -2
  21. package/dist/core/runner/prepare-suspend-saga.js +2 -10
  22. package/dist/core/runner/prepare-task.js +1 -1
  23. package/dist/core/runner/prepare-wiring-manifest.d.ts +1 -1
  24. package/dist/core/runner/prepare-wiring-manifest.js +1 -8
  25. package/dist/core/runner/run-attachment-seats.d.ts +4 -2
  26. package/dist/core/runner/run-attachment-seats.js +2 -2
  27. package/dist/core/runner/run-identity-wiring.d.ts +14 -33
  28. package/dist/core/runner/run-identity-wiring.js +4 -3
  29. package/dist/core/runner/run-leg.d.ts +106 -0
  30. package/dist/core/runner/run-leg.js +462 -0
  31. package/dist/core/runner/run-notification-lane.d.ts +55 -0
  32. package/dist/core/runner/run-notification-lane.js +128 -0
  33. package/dist/core/runner/run-reasoning-seat.d.ts +5 -5
  34. package/dist/core/runner/run-reasoning-seat.js +7 -7
  35. package/dist/core/runner/run-settle-and-teardown.d.ts +109 -0
  36. package/dist/core/runner/run-settle-and-teardown.js +324 -0
  37. package/dist/core/runner/run-terminal-adoption.d.ts +99 -0
  38. package/dist/core/runner/run-terminal-adoption.js +120 -0
  39. package/dist/core/runner/runtask.d.ts +14 -3
  40. package/dist/core/runner/runtask.js +55 -1010
  41. package/dist/core/runner-deps.d.ts +4 -14
  42. package/dist/core/store-contracts/workflow-journal-store-contract.d.ts +7 -0
  43. package/dist/core/store-contracts/workflow-journal-store-contract.js +85 -0
  44. package/dist/core/tool-policy.d.ts +37 -93
  45. package/dist/core/tool-policy.js +1 -11
  46. package/dist/core/trace.d.ts +6 -7
  47. package/dist/core/wiring-manifest.d.ts +5 -22
  48. package/dist/core/wiring-manifest.js +3 -11
  49. package/dist/core/workflow-journal-store.d.ts +35 -4
  50. package/dist/core/workflow-journal-store.js +19 -2
  51. package/dist/index.d.ts +3 -2
  52. package/dist/index.js +3 -2
  53. package/dist/orchestration/workflow.js +2 -0
  54. package/dist/stores/file/workflow-journal-store.d.ts +7 -10
  55. package/dist/stores/file/workflow-journal-store.js +2 -4
  56. package/package.json +1 -1
  57. package/test/export-surface.snapshot.json +10 -10
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 7.12.0 — 2026-09-10
4
+
5
+ ### BREAKING — workflow journal stores refuse an oversize entry loudly (#672; server [6840]; @server SQL store result arm same rule; @test)
6
+ - **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).
7
+ - **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).
8
+ - **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).
9
+ - **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.
10
+ - **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.
11
+ - **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.
12
+ - 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.
13
+
14
+ ### 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)
15
+ - **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.
16
+ - **② 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.
17
+ - **③ 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).
18
+ - **③ 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.
19
+ - **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 负控).
20
+ - **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).
21
+ - **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?`.
22
+ - **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.
23
+ - 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`.
24
+
25
+ ### 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)
26
+ - **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.
27
+ - **#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.
28
+ - **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.
29
+ - **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`).
30
+ - **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.
31
+
3
32
  ## 7.11.2 — 2026-09-10
4
33
 
5
34
  ### 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 +37,21 @@
8
37
 
9
38
  ### 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
39
  - **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.
40
+ - **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
41
  - **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
42
 
43
+ ## 7.11.1 — 2026-09-09
44
+
45
+ ### 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)
46
+ - **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.
47
+ - **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.
48
+ - 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`).
49
+ - 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`.
50
+
51
+ ### 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)
52
+ - 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.
53
+
54
+
14
55
  ### Internal — design/393 S4: the TaskStream façade leaves `runtask.ts` as five `stream-*.ts` lanes (byte-invariant; zero behavior surface)
15
56
  - **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
57
  - **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 +67,6 @@
26
67
  - `@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
68
  - 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
69
 
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
70
  ## 7.11.0 — 2026-09-09
42
71
 
43
72
  ### 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)
@@ -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);
@@ -1,13 +1,12 @@
1
1
  import type { ToolCallRequest } from "./tool-policy.js";
2
2
  /**
3
- * WHY an `unavailable` verdict could not run — the closed set behind the verdict arm, the ask's
4
- * `classifierUnavailable` fact (the card's "asked because the classifier could not run (timeout)" sentence)
5
- * and the `auto_mode.classified` trace frame's `cause`, so the three spell one word. `error` = the model leg
3
+ * WHY an `unavailable` verdict could not run — the closed set behind the verdict arm, the deny's
4
+ * `classifierUnavailable` fact (the deny observer's word and the model-facing sentence's parenthetical) and
5
+ * the `auto_mode.classified` trace frame's `cause`, so the three spell one word. `error` = the model leg
6
6
  * threw or rejected (a route failure at classify time reads here too — the derived-route pre-flight fell
7
- * back BEFORE any decide, so there is no separate word for it); `timeout` = the round-trip cap;
8
- * `breaker_open` = the session latch already tripped and the round was short-circuited.
7
+ * back BEFORE any decide, so there is no separate word for it); `timeout` = the round-trip cap.
9
8
  */
10
- export declare const AUTO_MODE_UNAVAILABLE_CAUSES: readonly ["error", "timeout", "breaker_open"];
9
+ export declare const AUTO_MODE_UNAVAILABLE_CAUSES: readonly ["error", "timeout"];
11
10
  export type AutoModeUnavailableCause = (typeof AUTO_MODE_UNAVAILABLE_CAUSES)[number];
12
11
  /** Whether a value is a member of the unavailable-cause set — the screen a row reader applies to a stored word. */
13
12
  export declare function isAutoModeUnavailableCause(v: unknown): v is AutoModeUnavailableCause;
@@ -21,26 +20,30 @@ export type AutoModeVerdict = {
21
20
  category: string;
22
21
  reason: string;
23
22
  }
24
- /** The classifier could not run: model error/timeout, or the session breaker is open. NOT a decision. */
23
+ /** The classifier could not run: the model leg threw / rejected, or ran past the cap. NOT a decision — the
24
+ * gate denies and says so ({@link classifierUnavailableDenyMessage}). */
25
25
  | {
26
26
  kind: "unavailable";
27
27
  cause: AutoModeUnavailableCause;
28
28
  }
29
- /** The model responded but not in the `<block>…` contract shape. NOT a decision. */
29
+ /** The model responded but not in the `<block>…` contract shape. Handled as a BLOCK with the CC
30
+ * parse-failure sentence ({@link CLASSIFIER_PARSE_FAILURE_DENY_MESSAGE}), counted by the denial limit. */
30
31
  | {
31
32
  kind: "parse_error";
32
33
  raw: string;
33
34
  };
34
35
  /**
35
- * The failure kinds that COUNT toward the one-way breaker and the word a trip records as its `lastCause`:
36
- * the two health failures of the unavailable arm plus the contract failure. `breaker_open` is deliberately
37
- * not one: it is what the latch ANSWERS once tripped, never what trips it; a cancelled round (the run's own
38
- * abort) counts nothing (see the decider's catch arm).
36
+ * The model-facing deny text for an `unavailable` verdictCC 2.1.250 `x1t`, with the parenthetical `EIt`
37
+ * spells for the causes this engine distinguishes (a wall-clock / connection timeout reads " (timed out)";
38
+ * an error of unknown kind reads nothing CC spells HTTP statuses it has, and this decider does not). The
39
+ * subject is the classifier itself rather than a model id: the decider does not know which model answered,
40
+ * and the sentence must not name one it cannot vouch for. One writer for every deny site (the gate's own
41
+ * station and the inherited-lane arms).
39
42
  */
40
- export declare const AUTO_MODE_BREAKER_CAUSES: readonly ["error", "timeout", "parse_error"];
41
- export type AutoModeBreakerCause = (typeof AUTO_MODE_BREAKER_CAUSES)[number];
42
- /** Whether a value is a member of the breaker-cause set (the manifest's value screen over a recorded trip). */
43
- export declare function isAutoModeBreakerCause(v: unknown): v is AutoModeBreakerCause;
43
+ export declare function classifierUnavailableDenyMessage(toolName: string, cause: AutoModeUnavailableCause): string;
44
+ /** The model-facing deny text for a `parse_error` verdict — CC 2.1.250 `Ure` over the R3t stem: the classifier
45
+ * answered, but not with a verdict, and auto mode blocks what it could not evaluate. */
46
+ export declare const CLASSIFIER_PARSE_FAILURE_DENY_MESSAGE = "Auto mode could not evaluate this action and is blocking it for safety \u2014 the classifier's reply carried no verdict.";
44
47
  /**
45
48
  * Parse the classifier's output contract — the CC 2.1.250 verdict reader (`wCe`, its thinking strip
46
49
  * `xCe`, its reason reader `yCe`) ported as is:
@@ -65,11 +68,12 @@ export declare function isAutoModeBreakerCause(v: unknown): v is AutoModeBreaker
65
68
  * stripped text, the tag spelled as the prompt spells it, trimmed) are carried when present and `""`
66
69
  * when absent; the consumers render their
67
70
  * own fallback line from the tool name in that case. The block intent is unambiguous either way, and
68
- * downgrading a stated block to `parse_error` ( the ask may later auto-deny OR a person may approve)
69
- * would weaken the classifier's explicit verdict on the exact calls it flagged.
71
+ * downgrading a stated block to `parse_error` (a block with the generic parse-failure sentence instead
72
+ * of the classifier's own reason) would lose the classifier's explicit verdict on the exact calls it flagged.
70
73
  *
71
- * `parse_error` is fail-closed: the ask flows the original chain (a person, a durable park, a headless
72
- * auto-deny) exactly as if auto mode were absent — never a silent allow.
74
+ * `parse_error` is fail-closed: the gate handles it as a BLOCK (CC 2.1.250's own outcome for a reply with no
75
+ * verdict) that the denial limit counts like any other — never a silent allow, and never a question handed
76
+ * to a person on the classifier's behalf.
73
77
  */
74
78
  export declare function parseAutoModeResponse(text: string): AutoModeVerdict;
75
79
  /** What the decider hands the deployment's `classify` hook (the assembled prompt is the hook's job
@@ -88,21 +92,11 @@ export interface AutoModeDeciderOptions {
88
92
  /** Hard cap on one classification round-trip. Default 15_000 ms (sema 裁量 — CC's constant is not
89
93
  * established; a permission gate must not stall the whole run on a slow classifier). */
90
94
  timeoutMs?: number;
91
- /** Consecutive-failure threshold that opens the session breaker (default 3 "连续 N 失败").
92
- * Failures = unavailable(error|timeout) + parse_error. A successful round (allow/block) resets it. */
93
- failureThreshold?: number;
94
- /** Fired ONCE when the breaker opens ("本 session 退回非 auto + 一次性告警"). `lastCause` is the failure that tripped it
95
- * ({@link AutoModeBreakerCause}). */
96
- onBreakerOpen?: (info: {
97
- consecutiveFailures: number;
98
- lastCause: AutoModeBreakerCause;
99
- }) => void;
100
95
  /**
101
96
  * Fired once per `decide` call with the verdict the gate is about to act on and the wall time it
102
- * waited (`ms`, integer ≥ 0; a timed-out round reads the deadline; a breaker-open short-circuit
103
- * reads ~0 with `cause:"breaker_open"`). `cause` is present iff the verdict is `unavailable`. The
104
- * engine's own wiring turns this into the `auto_mode.classified` trace frame; a hand-built decider
105
- * need not implement it. Like the breaker alarm, a throwing hook never breaks the gate.
97
+ * waited (`ms`, integer ≥ 0; a timed-out round reads the deadline). `cause` is present iff the verdict
98
+ * is `unavailable`. The engine's own wiring turns this into the `auto_mode.classified` trace frame; a
99
+ * hand-built decider need not implement it. A throwing hook never breaks the gate.
106
100
  */
107
101
  onClassified?: (info: AutoModeClassified) => void;
108
102
  }
@@ -120,66 +114,13 @@ export interface AutoModeClassified {
120
114
  export interface AutoModeDecider {
121
115
  /** Never rejects. Any internal failure surfaces as `unavailable`/`parse_error` (fail-closed). */
122
116
  decide(input: AutoModeClassifyInput, signal?: AbortSignal): Promise<AutoModeVerdict>;
123
- /** True once the session breaker has opened (it never half-opens: is a SESSION fallback
124
- * to non-auto, not a retry window — a flapping classifier must not oscillate the permission mode). */
125
- breakerOpen(): boolean;
126
- /**
127
- * The CURRENT consecutive-failure streak — the part of the breaker's state that `breakerOpen()` alone
128
- * cannot express. A latch that has not tripped yet still carries how much budget is left before it
129
- * does, and #503 needs exactly that: an arming recorded for cross-process rebuild starts a FRESH
130
- * decider, so recording one from a decider that is already 2 failures into a threshold of 3 would hand
131
- * the redemption more tolerance for a failing classifier than the ancestor had left — the redeemed leg
132
- * stays auto-classified through failures that would have dropped the ancestor back to non-auto.
133
- * {@link import("./tool-policy.js").constraintChainEntryOfLayer} therefore records an arming only at a
134
- * ZERO streak, and reads a decider that does not implement this member as unknown (⇒ records nothing).
135
- *
136
- * OPTIONAL so that hand-built deciders keep type-checking; the engine's own
137
- * {@link createAutoModeDecider} always implements it. A deployment that hand-rolls a decider and wants
138
- * the persisted-arming path must implement it too — the conservative reading of absence is deliberate.
139
- */
140
- consecutiveFailures?(): number;
141
117
  }
142
118
  /**
143
- * Session-scoped decider: timeout + fail-closed error mapping + a one-way circuit breaker.
144
- * One instance per run/session the breaker state is the session's "退回非 auto" latch.
119
+ * The per-run decider: timeout + fail-closed error mapping, and nothing carried between rounds. One instance
120
+ * per run (it binds the run's model leg and assembled prompt); a round that failed says nothing about the
121
+ * next one, which asks the model again.
145
122
  */
146
123
  export declare function createAutoModeDecider(opts: AutoModeDeciderOptions): AutoModeDecider;
147
- /**
148
- * ONE breaker trip as the session-level read face records it (`WiringManifest.autoMode.breaker`): when the
149
- * latch opened, what tripped it, how many consecutive failures it took, and which run's decider it was.
150
- * A decider is minted per run (its latch is a RUN fact), so a trip names one leg; the ledger below carries
151
- * the most recent one forward per session, which is how the session's NEXT leg can say "auto mode fell
152
- * back to asking on this session, at T, because the classifier timed out three times" — the sentence a
153
- * shell's doctor line and a capabilities read face owe a person who wonders why auto mode is asking.
154
- */
155
- export interface AutoModeBreakerTrip {
156
- /** Epoch ms at which the latch opened. */
157
- readonly openedAtMs: number;
158
- /** The failure that tripped it — the streak's last failure ({@link AutoModeBreakerCause}). */
159
- readonly lastCause: AutoModeBreakerCause;
160
- /** The consecutive-failure count at the trip (the threshold, or more under concurrent rounds). */
161
- readonly failures: number;
162
- /** The run whose decider tripped. */
163
- readonly runId: string;
164
- }
165
- /**
166
- * The Runner-lived, per-session record of the most recent breaker trip — the ONE writer is the engine's
167
- * own `onBreakerOpen` wrap at the arming site (the deployment's alarm hook, when wired, is called after
168
- * the record lands), the ONE reader the wiring-manifest phase of a later leg. Never persisted (a trip is
169
- * process-local, like the decider it describes); bounded FIFO so a long-lived Runner cannot grow it
170
- * without limit — an evicted session simply reads as "no trip recorded", the same as a session that
171
- * never tripped. Threaded through the trusted `RunInternals` channel exactly as the per-session read-file
172
- * seats are (the Runner sets it on every prepare it drives; a standalone prepare has none).
173
- */
174
- export declare class AutoModeBreakerLedger {
175
- private readonly cap;
176
- private readonly trips;
177
- constructor(cap?: number);
178
- /** Record THIS session's most recent trip (replacing an earlier one). */
179
- record(sessionId: string, trip: AutoModeBreakerTrip): void;
180
- /** The most recent trip recorded for this session, or undefined (never tripped, or evicted). */
181
- lastTrip(sessionId: string): AutoModeBreakerTrip | undefined;
182
- }
183
124
  /** The deployment's bounds for the denial limit (`RunnerDeps.autoMode.denialLimit`). Every member
184
125
  * optional; an omitted member takes its CC default. A present member with a bad value is REFUSED
185
126
  * loudly at construction (never clamped, never read as the default). */