@sema-agent/core 5.23.0 → 5.25.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 (35) hide show
  1. package/CHANGELOG.md +192 -1
  2. package/dist/core/checkpoint-store.d.ts +84 -10
  3. package/dist/core/checkpoint-store.js +3 -1
  4. package/dist/core/hooks.d.ts +18 -5
  5. package/dist/core/hooks.js +28 -4
  6. package/dist/core/memory-engine/engine.js +18 -3
  7. package/dist/core/permission-rule-org.d.ts +9 -0
  8. package/dist/core/permission-rule-org.js +12 -5
  9. package/dist/core/permission-rule-store.d.ts +25 -14
  10. package/dist/core/permission-rule-store.js +5 -1
  11. package/dist/core/runner/prepare-task.d.ts +2 -0
  12. package/dist/core/runner/prepare-task.js +40 -9
  13. package/dist/core/runner/runtask.js +49 -6
  14. package/dist/core/runner/session-file-state-replay.js +3 -0
  15. package/dist/core/tool-policy.d.ts +11 -0
  16. package/dist/core/tool-policy.js +17 -15
  17. package/dist/core/tool-result-store.d.ts +8 -0
  18. package/dist/core/tool-result-store.js +77 -3
  19. package/dist/core/types.d.ts +7 -5
  20. package/dist/index.d.ts +13 -7
  21. package/dist/index.js +2 -2
  22. package/dist/stores/file/adoption/adopt.d.ts +23 -3
  23. package/dist/stores/file/adoption/adopt.js +4 -8
  24. package/dist/stores/file/adoption/marker.d.ts +41 -18
  25. package/dist/stores/file/adoption/marker.js +14 -7
  26. package/dist/stores/file/permission-rule-store.d.ts +15 -1
  27. package/dist/stores/file/permission-rule-store.js +4 -1
  28. package/dist/stores/file/session-policy-store.d.ts +11 -1
  29. package/dist/stores/file/session-policy-store.js +8 -3
  30. package/dist/stores/file/task-list-store.d.ts +15 -1
  31. package/dist/stores/file/task-list-store.js +2 -2
  32. package/dist/tools/fs/fs-bash.js +7 -4
  33. package/dist/tools/fs/fs-shared.js +23 -7
  34. package/dist/tools/monitor.js +3 -3
  35. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,175 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.25.0 — 2026-08-10
4
+
5
+ No API-BREAKING changes (exports grow only; every new member is optional/additive). One
6
+ behavior-surface narrowing, called out below.
7
+
8
+ ### Narrowed (behavior, ruled)
9
+
10
+ - **The persisted-rule lane gains its mandate boundary (#144, dual-source measured).** *Allow rules
11
+ silence the classifier's questions, never a mandated one.* A persisted allow rule used to
12
+ short-circuit every surviving non-governance ask — including an operator's `shellGate:"always"`
13
+ (per-call confirmation mandated by deployment config) and a tool's own egress/irreversibility
14
+ marks (the non-budgetable family). The boundary is a single-source provenance predicate: the
15
+ classify-doctrine bash ask (coarse tier `"maybe"`) stays the rule lane's home turf (the
16
+ don't-ask-again main case is deliberately preserved); doctrine-installed `"always"` and
17
+ tool-declared marks are not clearable by rule. When a rule MATCHES but cannot clear, the surviving
18
+ ask discloses it on both channels: a message note naming the rule and the mandate, and the new
19
+ additive `PermissionResult.ask.persistedRuleShadowed` member (the matched rule text) — a consumer
20
+ renders "your rule is alive, just outranked". **Consumer note**: deployments under
21
+ `shellGate:"always"` or with marked tools will see asks their users' rules used to clear.
22
+
23
+ ### Added
24
+
25
+ - **`CheckpointError.detail.reason`** (closed set, additive; downstream-requested): discriminates
26
+ the pre-CAS refusal arms one code used to cover — `version_newer` / `env_factory_missing` /
27
+ `governed_unwired` on `unsupported_version` (each retryable on a differently-capable worker), and
28
+ `real_approval_damaged` / `real_approval_forged` / `constraint_chain_missing` on the
29
+ `invalid_outcome` row-integrity arms (terminal for the row's bytes). A deployment retry policy can
30
+ now tell "a capable worker can redeem this" from "no worker ever will".
31
+ - **`REAL_APPROVAL_CHECKPOINT_VERSION` exported** from the package root (joins the other five ladder
32
+ constants).
33
+ - **The I6 adoption boot gate reaches two more store faces** via an optional `dataRoot` anchor on
34
+ `FilePermissionRuleStoreProvider` and `createFileTaskListStore` (additive; the adoption arc's own
35
+ nested rule leg keeps its exemption, pinned). The freeze header states its real coverage — two
36
+ doors and the seams between them — instead of over-claiming.
37
+
38
+ ### Fixed
39
+
40
+ - **The disclosure channel is real end-to-end (pre-release rescan on this very batch).**
41
+ `persistedRuleShadowed` was write-only as first landed: `AskRequest` gains the member (all four
42
+ mint sites thread it), and the durable park mint carries it as `RiskDescriptor.shadowedRule`
43
+ (`inlineUntrusted`-capped) — the mandated population's normal route now discloses like the
44
+ synchronous one. The mandate predicate judges egress FIRST (the tool's own declaration is not
45
+ shadowed by the coarse doctrine sharing the seat). The secret-scrub quarantine capture stops
46
+ treating a name collision as a receipt (family suffix form; the polluted-index warning claims a
47
+ capture only when its write landed). The last silent timeout discard (a legal cap below the
48
+ resolved default) is announced; an empty env string is a written silence exception (the unset
49
+ idiom). The materialize-strategy announcements gain a per-value process ledger and a reset seam.
50
+ - **The loud-bad-value law lands on its two founding cases (#123, ruled).** `BASH_*_TIMEOUT_MS`
51
+ discards stop exempting the garbage/0/negative classes (every discarded value names the knob, what
52
+ arrived — env legs show the original string — and the value in force);
53
+ `SEMA_TOOL_MATERIALIZE_STRATEGY`'s refusal now matches the seat the bad value occupies: shadowed
54
+ by an explicit legal spec ⇒ loud discard (the documented "spec wins" precedence finally holds),
55
+ would-be-in-force ⇒ the closed-set refusal stands, dormant (no deferred tools) ⇒ announced once
56
+ per process instead of lying in wait.
57
+ - **A v7+ checkpoint declaring parent constraints must carry both the frozen chain and its digest**
58
+ (they are one write with the version stamp) — a row carrying neither is refused pre-CAS as damaged
59
+ instead of silently falling back to the count-only contract. Rows v6 and below keep the historic
60
+ contract.
61
+ - **The org resume belt's refusal carries the overlay's disclosure lines** (the resume path has no
62
+ `onUnavailable` seat) and tells a cancelled wait apart from unreadable governance. The verdict is
63
+ unchanged; the account is not.
64
+ - The sandbox-admission registry/decision domain split is documented and pinned (the two
65
+ un-instrumented fold families are safe for structural reasons — a family that gains an ask arm
66
+ reds instead of arriving unrecorded); frozen projections' fold position is disclosed; the adoption
67
+ report declares the memory-engine tree `action:"none"` explicitly; two engine quarantine captures
68
+ go `wx`-exclusive (no symlink at the final name, no overwrite of earlier evidence under a coarse
69
+ clock); the abort-races-timeout test pin gets real load headroom; the v8 stamp note carries its
70
+ erratum (v8 rows appear on ANY durable deployment via the always-mounted integrity policies — what
71
+ stays true: rows without the bit keep their historic stamps).
72
+
73
+ ## 5.24.0 — 2026-08-10
74
+
75
+ No API-BREAKING changes (exports grow only; `suspendAsk` gains an optional fifth parameter;
76
+ `removePersistedRule` widens its `principal` input to a union). Behavior-surface narrowings and two
77
+ deliberate loosenings are called out below; checkpoint **v8** is a compatibility disclosure, not a
78
+ breaking change (rows without the new gate bit keep their historic stamps).
79
+
80
+ ### Narrowed (behavior)
81
+
82
+ - **The real-approval bit survives the durability boundary (#130/#120).** An ask carrying
83
+ `requiresRealApproval` (an org ASK rule, the org-unavailable tighten, or a policy that minted the
84
+ bit) now parks as the NON-BUDGETABLE `irreversible_ask` gate kind — it used to park as plain
85
+ `human`, the one kind a network budget resolver may auto-approve. The gate carries
86
+ `RealApprovalGateBit` (`origin: "org_rule" | "org_unavailable" | "policy"`), the row stamps
87
+ `REAL_APPROVAL_CHECKPOINT_VERSION = 8` (`MAX_SUPPORTED` raised to 8 — a pre-5.24 worker rejects a
88
+ v8 row pre-CAS as `unsupported_version`, the row stays pending). Resume adds three guards: a
89
+ governed row (org origin) is refused PRE-CAS on a worker with no `permissionRuleOrg` wiring (the
90
+ human decision stays unspent, redeemable on an org-wired worker; a post-CAS belt remains as
91
+ defense in depth); a v8 row whose bit was stripped in storage, and a sub-v8 row carrying a bit no
92
+ release minted, are both refused pre-CAS (corruption/forgery guards).
93
+ - **`run_in_background: true` is judged by the backgrounding doctrine (#125, P0).** On a
94
+ `shellGate:"classify"` deployment, `bashReversibilityProbe` used to read only the command text, so
95
+ `ls` + the parameter form auto-admitted exactly what `ls &` asks for. The parameter spelling now
96
+ gets the textual spelling's verdict (not reversible ⇒ ask).
97
+ - **Oversized strings inside `details` are offloaded (#116).** `withToolResultOffload` used to
98
+ replace only `res.content`; a Bash call's 140k-char `details.stdout` sailed into session
99
+ persistence and every wire projection. Every oversized string inside `details` (deep walk, plain
100
+ objects/arrays, cycle-cut with a completed-transform memo, identity-preserving when untouched) is
101
+ now offloaded to the same store and replaced by a bounded head naming its ref; judged
102
+ independently of content size. Sequenced after the server tool-results read face (7.11.0) so full
103
+ payloads always have a host-side egress. `Write` joins `Read`'s structural offload exemption: its
104
+ `details.content` is the session-continuation replay's authorization source, and the replay
105
+ additionally refuses to seed a content string carrying the offload-replacement notice
106
+ (`isOffloadedDetailReplacement`, exported). An own `__proto__` key in `details` survives the
107
+ rebuild as data (never installed as the rebuilt object's prototype), and below head+notice size a
108
+ string rides untouched (no replacement that grows).
109
+ - **A local-owner adoption rebinds the anonymous session-policy estate (#132).** The
110
+ session-policy leg now runs for BOTH adoption shapes: the local-owner shape rewrites `[sid, null]`
111
+ rows to the adopted principal (a foreign principal's row stays), and the terminal report says
112
+ `row-rewrite` with the real count. On such a root the anonymous lane ALIASES to the adopted
113
+ principal (reads and writes), so the window between terminal adoption and the operator landing the
114
+ principal wiring cannot drop tighten-only deny rules; unadopted and principal-adopted roots are
115
+ untouched. Narrow disclosed window: a pre-5.24 in-flight local-owner marker (a stage-3 claim with
116
+ no session-policy evidence) reads as corrupt — recovery is an operator action (remove the root's
117
+ adoption.json and adopt afresh).
118
+
119
+ ### Loosened (deliberate, ruled)
120
+
121
+ - **A park minted under org UNAVAILABILITY honors the approval it solicited (#131).** The gate's
122
+ own message promises "every allow tightens to a real approval until [the org] can [be read]" — but
123
+ the resume belt refused that very approval and burned it, livelocking a park-only deployment
124
+ (ask → park → approved → still unavailable → refused, forever). A row with
125
+ `origin:"org_unavailable"` now EXECUTES on an approved resume even while the org stays unreadable
126
+ (the durable row — origin plus the human outcome — is the audit record); `org_rule` rows and
127
+ pre-v8 rows keep the strict fail-closed posture, and a published org deny blocks regardless of
128
+ origin.
129
+ - **The adoption terminal record's read validator checks shape, not this build's constants
130
+ (#133).** The stored report snapshots the by-design-not-migrated set and the config table AS RULED
131
+ AT ADOPTION TIME; both may grow by ruling, and the old exact-equality read marked every
132
+ already-adopted root corrupt on the first release after any growth (and again on rollback), across
133
+ 13 store constructors at once. Read time now enforces non-empty/well-formed/no-duplicate-identity;
134
+ the writer-emits-the-constant drift guard moves to the test grid, where a change is a deliberate,
135
+ reviewable edit. Empty, duplicate-carrying and mangled reports are refused exactly as before.
136
+
137
+ ### Added
138
+
139
+ - **The permission-rule BACKEND CONTRACT is exported** (ruled 2026-08-10): `PERMISSION_RULE_WRITER`,
140
+ `writerOf`, `foldDelta`, `addDotsOf`, the delta/writer/store types and the two consent-boundary
141
+ assertions — an out-of-repo store twin builds against the same definitions the file backend
142
+ implements instead of mirroring them. The consent boundary is unchanged and now PINNED: the engine
143
+ reaches `writer.apply` only on the consent redemption, tighten-delete and sync-join lanes
144
+ (registered-caller scan + a second net over every file naming the writer handle).
145
+ - **`removePersistedRule` accepts a structural `RuleOwner`** — the string shorthand is unchanged;
146
+ `{ kind: "local-owner" }` resolves the local-owner bucket (previously unrevokable through this
147
+ entry), and a provider without `forLocalOwner` fails loudly instead of no-oping against the
148
+ zero-rule store.
149
+ - `RealApprovalGateBit` exported (the supervisor-inbox / SQL-twin read of the new gate member).
150
+
151
+ ### Fixed
152
+
153
+ - **The background time wall is stated as a budget, never a kill threat (#121).** Under deadline
154
+ pressure a model read "auto-terminates if still running after 600s" as "better TaskStop it myself
155
+ first" (a measured 6.3s self-stop where waiting was optimal). All three Bash minting arms and both
156
+ Monitor siblings now say "may run up to Ns"; the kill explanation stays with the terminal
157
+ timed-out wording, which fires only when a timeout actually happens. Retired-dialect tripwires on
158
+ both faces.
159
+ - **`createAllowDenyPolicy`'s invalid-name refusal teaches each lesson once.** A CC-migrated
160
+ settings file with 39 content-form entries repeated the same three-line lesson 39 times; the
161
+ thrown string now groups by lesson (entries listed under it). The structured `issues` face stays
162
+ per-entry and gains a `lesson` member (additive).
163
+ - Three public comment surfaces stopped recommending prefix recomposition over `buildToolResultRef`
164
+ (#134 — the encoding is not injective; ownership is exact-equality against a recomposed ref), the
165
+ `RunnerDeps.permissionRuleStore` docstring states the ruled backend-contract boundary instead of
166
+ "the write face is core-private", and dated errata landed for two past releases (5.22.0: the
167
+ design/182 rule-sync core half shipped unlisted; 5.13.0: safety asks going sync-first under a live
168
+ approver shipped unlisted and superseded a written park promise).
169
+ - Docs: `docs/sdk/09` gains the sandbox-admission section (every exclusion conjunct, from source)
170
+ and an org-rule-layer account stated in terms of the mechanism that actually holds the line
171
+ (`requiresRealApproval` excluded at every loosening seam, now including the durability boundary).
172
+
3
173
  ## 5.23.0 — 2026-08-10
4
174
 
5
175
  No BREAKING changes.
@@ -92,7 +262,10 @@ No BREAKING changes.
92
262
  delegation-provenance aggregate rides the durable checkpoint; a resumed leg missing the state
93
263
  reads as `unknown` (static floor), never as `clean`.
94
264
  - **`buildToolResultRef` exported from the package root** — a host wiring an HTTP tool-result read
95
- face binds an incoming ref to its owning task by prefix recomputation.
265
+ face composes refs through the single source instead of reimplementing the segment escaping.
266
+ (Erratum 2026-08-10: this entry originally said a read face binds a ref to its owning task "by
267
+ prefix recomputation". It must not — the encoding is not injective over the two segments, so
268
+ ownership is an exact-equality test against a recomposed ref, never a prefix match.)
96
269
  - **Content mandate split from the approval mandate (#94).** A delegated child's question routes on
97
270
  the QUESTION seat, not the approver seat. Behavior widening, called out explicitly: under
98
271
  `durableApproval` with a live `onQuestion`, an absent or string approver seat used to leave child
@@ -103,6 +276,13 @@ No BREAKING changes.
103
276
  `TaskRegistry.reviveBackgroundAgent`'s refusal union gains the `recycling` member — exhaustive
104
277
  consumers add one arm.
105
278
 
279
+ _Addendum (2026-08-10, #135 erratum): the design/182 rule-sync **core half also shipped in this
280
+ release** and was not listed. It added the sync client and its wire contract to the public surface:
281
+ `syncPermissionRules` (full-state join, screened in both directions), `PERMISSION_RULE_SYNC_PATH`,
282
+ `RuleSyncRequestBody` / `RuleSyncResponseBody` / `PermissionRuleSyncResult`, `parseRuleSyncResponse`,
283
+ and `LOCAL_OWNER_UNSYNCABLE_CODE` (a local-owner bucket refuses to sync, loudly) — the definitions a
284
+ server-side rule store builds against. Recorded as a dated erratum rather than a silent rewrite._
285
+
106
286
  ## 5.21.1 — 2026-08-09
107
287
 
108
288
  - **Fix: every `hands: none` deployment failed at the door on 5.21.0** (P0). `HAND_TOOL_EFFECTS`
@@ -1105,6 +1285,17 @@ mysterious runtime.
1105
1285
 
1106
1286
  - **`finalVerification` mechanism hardening (four of the seven field failure modes).** ① The injection headroom guard read only the turn axis — it now also reads the tightest caller-armed budget axis (tokens/cost/walltime, same axes as the limit-approach frames); ≥90% full skips the injection, so the gate can no longer convert an externally-passing run into `limits.*_exceeded`. ② Both injections emit the standard `steering_injected` echo (**new closed-set member: `source: "final_verification"`** — switches over the source union add an arm), closing the stats-vs-stream observability gap. ③ The nudge licenses cleaning up residue the model's own testing created (the state-harmless clause no longer reads as "leave your test residue in place"). ④ The nudge forbids laundering pre-existing uncertainty into "confirmed". The remaining modes are design-bounded: the engine never judges check semantics, and self-grading is the deployment's verifier-role wiring.
1107
1287
 
1288
+ _Addendum to 5.13.0 (2026-08-10, erratum — the entry below was missing at release): **SAFETY asks go
1289
+ sync-first when a live approver is wired.** With a FUNCTION-valued `onAsk` present, an
1290
+ egress/irreversibility-tightened ask now resolves in-stream through that approver (same turn, no
1291
+ checkpoint) instead of always parking as `irreversible_ask`; the durable park remains the headless /
1292
+ no-live-approver / `forceDurableGate` / live-face-answered-unavailable path, where the gate kind,
1293
+ `safetyAxis` and risk descriptor are unchanged. This superseded the earlier written promise that
1294
+ "safety asks … still park" with a live approver (the 1.37x sync-approval entry). The change shipped
1295
+ in 5.13.0 with no changelog entry — recorded here as a dated erratum rather than a silent rewrite;
1296
+ consumer flips: a probe pinning "an `irreversibility:'always'` tool always suspends" reds whenever a
1297
+ live `onAsk` is wired — drop the approver (or arm `forceDurableGate`) to test the park leg._
1298
+
1108
1299
  ## 5.12.0 — 2026-08-05
1109
1300
 
1110
1301
  ### BREAKING
@@ -135,6 +135,11 @@ export interface RiskDescriptor {
135
135
  * attempted — over-reaching a shell parse risks a wrong/forgeable path). Each path `inlineUntrusted`-capped.
136
136
  * Omitted when none derivable. */
137
137
  touchedPaths?: string[];
138
+ /** #144 (additive): a persisted allow rule MATCHED this call but could not clear the mandated ask —
139
+ * the matched rule text (`inlineUntrusted`-capped), threaded to the mint so the durable-park route
140
+ * carries the same disclosure the synchronous ask does. The inbox renders "their rule is alive,
141
+ * just outranked" instead of the person concluding their rule silently broke. */
142
+ shadowedRule?: string;
138
143
  }
139
144
  /**
140
145
  * design/80 §D-E: the DETERMINISTIC severity tier (1..5) for an escalation checkpoint, a PURE function of the
@@ -266,7 +271,31 @@ export declare function buildRiskDescriptor(input: {
266
271
  shellGated?: boolean;
267
272
  /** The resolved doctrine to persist when `shellGated` (see {@link RiskDescriptor.shellGateDoctrine}). */
268
273
  shellGateDoctrine?: "classify" | "always";
274
+ /** #144: the matched-but-outranked persisted rule to persist (see {@link RiskDescriptor.shadowedRule}). */
275
+ shadowedRule?: string;
269
276
  }): RiskDescriptor;
277
+ /**
278
+ * #130/#131/#120 (2026-08-10) — the durable record of an ask's `requiresRealApproval` bit, which used
279
+ * to DIE at the park: the mint keyed the gate kind on the static tool marks only, so an org-governed
280
+ * ask on an unmarked tool minted a plain `{kind:"human"}` — the one kind a network budget resolver may
281
+ * auto-approve — and the org's "only judgment clears this" demand was silently budgetable after the
282
+ * durability boundary. Present ⇒ the mint escalates to `irreversible_ask` (non-budgetable) and stamps
283
+ * {@link REAL_APPROVAL_CHECKPOINT_VERSION}. `origin` additionally records WHY, for the resume belts:
284
+ * · `"org_rule"` — an org ASK rule fired (governance was READABLE at mint). Resume keeps the strict
285
+ * posture: org unavailable at resume still refuses (newly-blind ⇒ fail-closed).
286
+ * · `"org_unavailable"` — governance could NOT be read at mint and the gate's own message promised
287
+ * "every allow tightens to a real approval until it can". A person approving THIS park IS that real
288
+ * approval — so a resume that finds org still unavailable executes instead of refusing and burning
289
+ * the approval (the park-only livelock #131 closed). The durable row itself — this origin plus the
290
+ * recorded human outcome — is the audit record of that passage; the resume emits no extra notice.
291
+ * · `"policy"` — a policy/hook minted the bit (e.g. the always-on classifier-parity rule); no org
292
+ * semantics, the resume belts treat it as a plain non-budgetable approval.
293
+ * Both org origins double as the #120 governed mark: a resuming worker with NO org adjudication wiring
294
+ * refuses to redeem such a row (the governed boundary must not vanish with a deployment's wiring).
295
+ */
296
+ export interface RealApprovalGateBit {
297
+ origin: "org_rule" | "org_unavailable" | "policy";
298
+ }
270
299
  export type CheckpointGate =
271
300
  /** F4: a human (or any external authority) must allow/deny a pending tool call. design/80 §D-E:
272
301
  * carries an OPTIONAL display-only {@link RiskDescriptor} (severity/axes/summary) for the supervisor
@@ -292,6 +321,7 @@ export type CheckpointGate =
292
321
  reason: string;
293
322
  toolName: string;
294
323
  safetyAxis?: SafetyAxis;
324
+ realApproval?: RealApprovalGateBit;
295
325
  riskDescriptor?: RiskDescriptor;
296
326
  }
297
327
  /** design/74: a resource slice limit (budget/walltime/turns) was reached — suspend (resumable) instead of
@@ -782,8 +812,11 @@ export interface CheckpointState {
782
812
  * (extra deny-narrowing layers + the pre-CAS edit re-adjudication) rather than trusting a
783
813
  * re-supplied policy's execution half for the frozen deny data — a re-supplied closure that
784
814
  * matches the digest but behaves differently cannot un-deny what the suspend froze. Present iff
785
- * `requiresParentConstraint` on new mints; absent on older checkpoints (⇒ no frozen deny data —
786
- * the count-only contract governs, and an EDIT resume is refused for want of a validatable chain). */
815
+ * `requiresParentConstraint` on new mints; absent on PRE-F-012 checkpoints (⇒ no frozen deny data —
816
+ * the count-only contract governs, and an EDIT resume is refused for want of a validatable chain).
817
+ * "Older" is version-decidable, not guessed: chain, digest and the {@link F012_CHECKPOINT_VERSION}
818
+ * stamp are one write, so on a v7+ row the pair is REQUIRED — its absence there is a damaged row and
819
+ * the resume ladder refuses it pre-CAS rather than falling back to the count-only contract. */
787
820
  constraintChain?: ReadonlyArray<import("./tool-policy.js").ConstraintChainEntry>;
788
821
  /**
789
822
  * F-012 L1 — the content digest over `constraintChain` ({@link import("./tool-policy.js").constraintChainDigest}):
@@ -791,7 +824,8 @@ export interface CheckpointState {
791
824
  * content. The resume pre-CAS ladder recomputes the digest over the RE-SUPPLIED chain's
792
825
  * projections and rejects a mismatch (`resume.parent_constraint_mismatch`) — replacing the
793
826
  * count-only shape check with a content contract (a same-length chain of different frozen deny
794
- * data no longer passes). Present iff `constraintChain` is. */
827
+ * data no longer passes). Present iff `constraintChain` is — and on a v7+ row both are required
828
+ * (see {@link constraintChain}: the pair and the version stamp are minted together). */
795
829
  constraintDigest?: string;
796
830
  };
797
831
  /**
@@ -909,13 +943,29 @@ export declare const ORG_ADMISSION_CHECKPOINT_VERSION = 6;
909
943
  * state keeps the historic stamps — deployments not delegating see zero version movement.
910
944
  */
911
945
  export declare const F012_CHECKPOINT_VERSION = 7;
946
+ /**
947
+ * #130/#120 (2026-08-10 — the F012_CHECKPOINT_VERSION precedent replayed a sixth time): the schema
948
+ * version a suspend stamps when its gate carries {@link RealApprovalGateBit}. The enforcement lives
949
+ * ENTIRELY in the resuming worker (the no-org-wiring refusal, the org_unavailable resume-belt
950
+ * distinction): a pre-131 worker (MAX_SUPPORTED=7) would accept the row, ignore `realApproval`, and
951
+ * either redeem a governed row with no org wiring at all (#120's exact hole, replayed through version
952
+ * skew) or burn the approval on the unavailable belt the bit exists to soften. Stamping v8 forces it
953
+ * to reject PRE-CAS (`unsupported_version`, stays `pending`, retried on an enforcing worker). A gate
954
+ * with NO `realApproval` keeps the historic stamps. NOTE (erratum 2026-08-10, downstream-measured):
955
+ * "zero version movement for ungoverned deployments" is TOO WIDE — the two always-mounted integrity
956
+ * policies (transcript-integrity, unverifiable-delete) mint `requiresRealApproval` asks, so a park on
957
+ * one of THOSE stamps v8 with `origin:"policy"` on ANY durable deployment, org-governed or not. What
958
+ * stays true: rows whose gate carries no realApproval bit keep their historic stamps everywhere.
959
+ */
960
+ export declare const REAL_APPROVAL_CHECKPOINT_VERSION = 8;
912
961
  /** The highest {@link Checkpoint.version} `runner.resume` will act on; a higher one is rejected pre-CAS with
913
962
  * {@link CheckpointError} `unsupported_version` (the checkpoint stays `pending`, retryable on a newer worker).
914
- * Raised to 7 for F-012-bearing checkpoints — this worker reads v1 (legacy human), v2 (resource), v3
963
+ * Raised to 8 for realApproval-bearing gates — this worker reads v1 (legacy human), v2 (resource), v3
915
964
  * (binding human/irreversible_ask), v4 (a pre-164 row, refused only when it carries the retired
916
965
  * wall-clock allocation), v5 (token-allocation-bearing resource/approval), v6 (org-admission freeze
917
- * state) and v7 (F-012 constraint-chain / delegation-provenance enforcement state). */
918
- export declare const MAX_SUPPORTED_CHECKPOINT_VERSION = 7;
966
+ * state), v7 (F-012 constraint-chain / delegation-provenance enforcement state) and v8 (a
967
+ * non-budgetable `realApproval` gate bit with its org-origin resume semantics). */
968
+ export declare const MAX_SUPPORTED_CHECKPOINT_VERSION = 8;
919
969
  /**
920
970
  * Read a checkpoint's schema version, defaulting an absent field to **legacy `0`** (a 1.67-era checkpoint
921
971
  * written before the field existed — it carries no `workspaceHandle`, so resuming it the v1 way is safe).
@@ -1428,8 +1478,8 @@ export declare class CheckpointError extends Error {
1428
1478
  * computable from a self-reported closure, so the edited resume is refused typed pre-CAS (the
1429
1479
  * checkpoint stays `pending`, still decidable); an UNEDITED approval is untouched. */
1430
1480
  | "resume.constraint_unprojectable";
1431
- /** Structured discrimination a caller can branch on where the code alone is ambiguous. Today one
1432
- * member: `field` names WHICH part of the decision payload was rejected on a
1481
+ /** Structured discrimination a caller can branch on where the code alone is ambiguous. Two
1482
+ * members. `field` names WHICH part of the decision payload was rejected on a
1433
1483
  * `checkpoint.invalid_outcome` — `"boundCallId"` (the action you decided on has been replaced:
1434
1484
  * re-fetch the pending list), `"boundInputHash"` (the input you reviewed has changed under the same
1435
1485
  * action: re-review), or `"answer"` (the content-ask answer is missing, or was attached to a
@@ -1441,6 +1491,18 @@ export declare class CheckpointError extends Error {
1441
1491
  * discriminant. */
1442
1492
  readonly detail?: {
1443
1493
  field?: "boundCallId" | "boundInputHash" | "answer" | "settledBy";
1494
+ /** WHICH pre-CAS refusal arm fired, where one `code` covers several (requested 2026-08-10: a
1495
+ * deployment retry policy needs to tell "a newer worker can redeem this row" from "this row is
1496
+ * damaged/forged and no worker ever will" — blanket-retriable and gate-shape heuristics were
1497
+ * both refuted downstream, so the throw site carries the fact it always knew). Closed set;
1498
+ * additive and optional like `field` — `code` remains the only REQUIRED discriminant.
1499
+ * · `unsupported_version` arms: `"version_newer"` (retryable on a newer worker),
1500
+ * `"env_factory_missing"` (retryable on a factory-wired worker), `"governed_unwired"`
1501
+ * (retryable on an org-wired worker).
1502
+ * · `invalid_outcome` pre-CAS row-integrity arms: `"real_approval_damaged"`,
1503
+ * `"real_approval_forged"`, `"constraint_chain_missing"` (all terminal for the row's
1504
+ * current bytes — no worker version redeems a damaged row). */
1505
+ reason?: "version_newer" | "env_factory_missing" | "governed_unwired" | "real_approval_damaged" | "real_approval_forged" | "constraint_chain_missing";
1444
1506
  } | undefined;
1445
1507
  constructor(code: "checkpoint.already_exists" | "checkpoint.already_resolved" | "checkpoint.not_found"
1446
1508
  /** `runner.resume` was handed an {@link ResumeOutcome} whose `gate` arm does not match the
@@ -1537,8 +1599,8 @@ export declare class CheckpointError extends Error {
1537
1599
  * computable from a self-reported closure, so the edited resume is refused typed pre-CAS (the
1538
1600
  * checkpoint stays `pending`, still decidable); an UNEDITED approval is untouched. */
1539
1601
  | "resume.constraint_unprojectable", message: string,
1540
- /** Structured discrimination a caller can branch on where the code alone is ambiguous. Today one
1541
- * member: `field` names WHICH part of the decision payload was rejected on a
1602
+ /** Structured discrimination a caller can branch on where the code alone is ambiguous. Two
1603
+ * members. `field` names WHICH part of the decision payload was rejected on a
1542
1604
  * `checkpoint.invalid_outcome` — `"boundCallId"` (the action you decided on has been replaced:
1543
1605
  * re-fetch the pending list), `"boundInputHash"` (the input you reviewed has changed under the same
1544
1606
  * action: re-review), or `"answer"` (the content-ask answer is missing, or was attached to a
@@ -1550,6 +1612,18 @@ export declare class CheckpointError extends Error {
1550
1612
  * discriminant. */
1551
1613
  detail?: {
1552
1614
  field?: "boundCallId" | "boundInputHash" | "answer" | "settledBy";
1615
+ /** WHICH pre-CAS refusal arm fired, where one `code` covers several (requested 2026-08-10: a
1616
+ * deployment retry policy needs to tell "a newer worker can redeem this row" from "this row is
1617
+ * damaged/forged and no worker ever will" — blanket-retriable and gate-shape heuristics were
1618
+ * both refuted downstream, so the throw site carries the fact it always knew). Closed set;
1619
+ * additive and optional like `field` — `code` remains the only REQUIRED discriminant.
1620
+ * · `unsupported_version` arms: `"version_newer"` (retryable on a newer worker),
1621
+ * `"env_factory_missing"` (retryable on a factory-wired worker), `"governed_unwired"`
1622
+ * (retryable on an org-wired worker).
1623
+ * · `invalid_outcome` pre-CAS row-integrity arms: `"real_approval_damaged"`,
1624
+ * `"real_approval_forged"`, `"constraint_chain_missing"` (all terminal for the row's
1625
+ * current bytes — no worker version redeems a damaged row). */
1626
+ reason?: "version_newer" | "env_factory_missing" | "governed_unwired" | "real_approval_damaged" | "real_approval_forged" | "constraint_chain_missing";
1553
1627
  } | undefined);
1554
1628
  }
1555
1629
  /**
@@ -120,6 +120,7 @@ export function buildRiskDescriptor(input) {
120
120
  axes,
121
121
  toolName,
122
122
  ...(input.shellGated && input.shellGateDoctrine !== undefined ? { shellGateDoctrine: input.shellGateDoctrine } : {}),
123
+ ...(input.shadowedRule !== undefined ? { shadowedRule: inlineUntrusted(input.shadowedRule, 200) } : {}),
123
124
  ...(summary !== undefined ? { summary } : {}),
124
125
  ...(touchedPaths !== undefined ? { touchedPaths } : {}),
125
126
  };
@@ -134,7 +135,8 @@ export const BINDING_CHECKPOINT_VERSION = 3;
134
135
  export const TOKEN_CHECKPOINT_VERSION = 5;
135
136
  export const ORG_ADMISSION_CHECKPOINT_VERSION = 6;
136
137
  export const F012_CHECKPOINT_VERSION = 7;
137
- export const MAX_SUPPORTED_CHECKPOINT_VERSION = 7;
138
+ export const REAL_APPROVAL_CHECKPOINT_VERSION = 8;
139
+ export const MAX_SUPPORTED_CHECKPOINT_VERSION = 8;
138
140
  export function checkpointVersionOf(cp) {
139
141
  return cp.version ?? 0;
140
142
  }
@@ -602,7 +602,16 @@ export interface ToolGateInput {
602
602
  * prefer, so the closure must NOT take its sync-first decline and should park durably. All other
603
603
  * decline/pre-commit-failure paths keep their existing `undefined` fallbacks (the gate then keeps the
604
604
  * fail-closed deny / typed refusal the caller carries). */
605
- liveFaceUnavailable?: boolean) => Promise<ToolGateResult["suspend"] | ParkAttemptFailed | undefined>;
605
+ liveFaceUnavailable?: boolean,
606
+ /** #130: present ⇒ the surviving ask carries `requiresRealApproval` and the mint must escalate to
607
+ * the non-budgetable `irreversible_ask` kind carrying this bit (a budget resolver must never
608
+ * auto-approve what only judgment may clear). `origin` records whether the bit came from an org
609
+ * ASK rule, from the org-unavailable tighten (whose resume semantics differ — see
610
+ * {@link import("./checkpoint-store.js").RealApprovalGateBit}), or from a policy/hook. */
611
+ realApproval?: import("./checkpoint-store.js").RealApprovalGateBit,
612
+ /** #144: the matched-but-outranked persisted rule (the surviving ask's `persistedRuleShadowed`)
613
+ * — threaded so the park mint's risk descriptor carries the disclosure on the durable route. */
614
+ shadowedRule?: string) => Promise<ToolGateResult["suspend"] | ParkAttemptFailed | undefined>;
606
615
  /**
607
616
  * design/174 — route a policy `ask` on the reserved question tool to this run's CONTENT-ask channel
608
617
  * before it can become a park or a refusal. Called in the `ask` branch with the FINAL post-hook,
@@ -722,10 +731,14 @@ export interface ToolGateInput {
722
731
  * · the reserved question tool and a call MARKED unresolvable are excluded for the same reason the
723
732
  * classifier excludes them: both contracts require that no synchronous decision-maker stands between
724
733
  * the ask and the park / content route, and this lane is one.
725
- * What it DOES consume is the egress and irreversibility/shellGate tightens and unmarked bare asks —
726
- * deliberately, because a deployment forcing shell classification is exactly where the feature is for.
727
- * Consuming such an ask also skips the park it would have minted; that IS what a standing approval
728
- * means, and the first bullet is what keeps an integrity ask out of that set.
734
+ * · #144 (ruled): a MANDATED ask is never consumed an operator's shellGate:"always" tier and a
735
+ * tool's own egress/irreversibility marks are structural requirements, not classifier hesitation
736
+ * ("allow rules silence the classifier's questions, never a mandated one"). When a rule matches
737
+ * but cannot clear, the surviving ask discloses it (message + `persistedRuleShadowed`).
738
+ * What it DOES consume is the classify-DOCTRINE shell ask (the coarse tier "maybe" — the
739
+ * don't-ask-again main case this feature exists for) and unmarked bare asks. Consuming such an ask
740
+ * also skips the park it would have minted; that IS what a standing approval means, and the
741
+ * bullets above are what keep integrity/hook/mandated asks out of that set.
729
742
  */
730
743
  persistedRules?: {
731
744
  /** The canonical text of the rule that admits this call, or `undefined`. Must not throw: a store that
@@ -248,6 +248,7 @@ export async function runToolGate(input) {
248
248
  }
249
249
  }
250
250
  let orgRealApprovalRequired = false;
251
+ let orgAskOrigin;
251
252
  let orgTightenCount = 0;
252
253
  const applyOrgLayer = async (current, args) => {
253
254
  if (input.orgRules === undefined)
@@ -266,6 +267,7 @@ export async function runToolGate(input) {
266
267
  }, { ...(input.abortSignal !== undefined ? { signal: input.abortSignal } : {}), timeoutMs: ORG_ADJUDICATION_TIMEOUT_MS });
267
268
  if (answer.status === "unavailable") {
268
269
  orgRealApprovalRequired = true;
270
+ orgAskOrigin = "unavailable";
269
271
  await notifier.notifyAsync(() => input.orgRules?.onUnavailable?.({ toolName, toolCallId, message: answer.disclosures.join("; ") }), "toolGate.orgSnapshotUnavailable");
270
272
  if (decided.action === "allow") {
271
273
  decided = {
@@ -297,6 +299,7 @@ export async function runToolGate(input) {
297
299
  orgTightenCount += 1;
298
300
  denySource = "org";
299
301
  orgRealApprovalRequired = true;
302
+ orgAskOrigin = "rule";
300
303
  return decided.action === "ask"
301
304
  ? { ...decided, requiresRealApproval: true }
302
305
  : {
@@ -312,6 +315,15 @@ export async function runToolGate(input) {
312
315
  currentInput = policyRewrite;
313
316
  req.args = policyRewrite;
314
317
  }
318
+ const persistedRuleMandate = input.egress === true
319
+ ? "tool_marks"
320
+ : input.shellGated === true
321
+ ? input.irreversibility === "always"
322
+ ? "operator_always"
323
+ : undefined
324
+ : input.irreversibility === "always" || input.irreversibility === "maybe"
325
+ ? "tool_marks"
326
+ : undefined;
315
327
  if (input.persistedRules &&
316
328
  !orgRealApprovalRequired &&
317
329
  decision.action === "ask" &&
@@ -320,7 +332,7 @@ export async function runToolGate(input) {
320
332
  req.toolName !== ASK_USER_QUESTION_TOOL_NAME &&
321
333
  input.isMarkedUnresolvable?.(input.event.toolCallId) !== true) {
322
334
  const hit = await input.persistedRules.admits(req).catch(() => undefined);
323
- if (hit !== undefined) {
335
+ if (hit !== undefined && persistedRuleMandate === undefined) {
324
336
  decision = {
325
337
  action: "allow",
326
338
  message: `a persisted allow rule (${hit}) covers this call`,
@@ -329,6 +341,14 @@ export async function runToolGate(input) {
329
341
  };
330
342
  await notifier.notifyAsync(() => input.persistedRules?.onResolved?.({ toolName: req.toolName, toolCallId, rule: hit }), "toolGate.persistedRuleResolved");
331
343
  }
344
+ else if (hit !== undefined) {
345
+ const mandateNoun = persistedRuleMandate === "operator_always" ? "this deployment mandates per-call confirmation for shell commands (shellGate: always)" : "this tool carries egress/irreversibility marks (a mandated confirmation a rule cannot clear)";
346
+ decision = {
347
+ ...decision,
348
+ persistedRuleShadowed: hit,
349
+ message: `${decision.message !== undefined ? `${decision.message} ` : ""}(a persisted allow rule (${hit}) matches this call but does not clear the ask — ${mandateNoun})`,
350
+ };
351
+ }
332
352
  }
333
353
  if (input.autoMode &&
334
354
  !orgRealApprovalRequired &&
@@ -383,8 +403,11 @@ export async function runToolGate(input) {
383
403
  const egressTool = input.egress === true;
384
404
  const irreversibleTool = input.irreversibility === "always" || input.irreversibility === "maybe";
385
405
  const safety = egressTool || irreversibleTool ? { egress: egressTool, irreversible: irreversibleTool } : undefined;
406
+ const realApprovalOf = (d) => d.action === "ask" && d.requiresRealApproval === true
407
+ ? { origin: orgAskOrigin !== undefined ? `org_${orgAskOrigin}` : "policy" }
408
+ : undefined;
386
409
  if (suspendAsk && decision.action === "ask") {
387
- const suspended = await suspendAsk(req, currentInput, safety);
410
+ const suspended = await suspendAsk(req, currentInput, safety, undefined, realApprovalOf(decision), decision.action === "ask" ? decision.persistedRuleShadowed : undefined);
388
411
  if (suspended) {
389
412
  if ("parkFailed" in suspended)
390
413
  parkFailed = suspended.parkFailed;
@@ -403,7 +426,7 @@ export async function runToolGate(input) {
403
426
  req.args = outcome.presentedInput;
404
427
  }
405
428
  if (suspendAsk && outcome.parkDeclined && parkFailed === undefined) {
406
- const suspended = await suspendAsk(req, currentInput, safety, true);
429
+ const suspended = await suspendAsk(req, currentInput, safety, true, realApprovalOf(decision), decision.action === "ask" ? decision.persistedRuleShadowed : undefined);
407
430
  if (suspended) {
408
431
  if ("parkFailed" in suspended)
409
432
  parkFailed = suspended.parkFailed;
@@ -427,10 +450,11 @@ export async function runToolGate(input) {
427
450
  }
428
451
  }
429
452
  if (decision.action === "ask") {
453
+ const askBeforeResolve = decision;
430
454
  const resolved = await resolveAsk(decision, req);
431
455
  decision = resolved;
432
456
  if (resolved.action === "deny" && resolved.approverUnavailable === true && suspendAsk && parkFailed === undefined) {
433
- const suspended = await suspendAsk(req, currentInput, safety, true);
457
+ const suspended = await suspendAsk(req, currentInput, safety, true, realApprovalOf(askBeforeResolve), askBeforeResolve.action === "ask" ? askBeforeResolve.persistedRuleShadowed : undefined);
434
458
  if (suspended) {
435
459
  if ("parkFailed" in suspended)
436
460
  parkFailed = suspended.parkFailed;
@@ -656,16 +656,18 @@ export class MemoryEngine {
656
656
  const indexNow = readSafe(pollutedIndexPath);
657
657
  if (indexNow === undefined || indexNow === handle.indexText)
658
658
  return;
659
+ let indexCaptureLanded = false;
659
660
  try {
660
661
  const dest = join(this.controlDir, QUARANTINE_DIR, `${this.now()}-polluted-${MEMORY_INDEX_FILENAME}`);
661
662
  ensureDirExists(dirname(dest));
662
- writeFileSync(dest, indexNow, "utf8");
663
+ writeFileSync(dest, indexNow, { encoding: "utf8", flag: "wx" });
664
+ indexCaptureLanded = true;
663
665
  }
664
666
  catch {
665
667
  }
666
668
  try {
667
669
  writeFileNoFollow(pollutedIndexPath, handle.indexText);
668
- report.warnings.push("memory index restored to its pre-session state — this session's index additions were not retained (session polluted; the removed text was captured to quarantine)");
670
+ report.warnings.push(`memory index restored to its pre-session state — this session's index additions were not retained (session polluted; ${indexCaptureLanded ? "the removed text was captured to quarantine" : "the quarantine capture did NOT land — the removed text is gone"})`);
669
671
  }
670
672
  catch (err) {
671
673
  report.warnings.push(`memory index could NOT be restored to its pre-session state: ${err instanceof Error ? err.message : String(err)}`);
@@ -1225,7 +1227,20 @@ export class MemoryEngine {
1225
1227
  try {
1226
1228
  const dest = join(this.controlDir, QUARANTINE_DIR, `${this.now()}-${MEMORY_INDEX_FILENAME}`);
1227
1229
  ensureDirExists(dirname(dest));
1228
- writeFileSync(dest, text, "utf8");
1230
+ let landed = false;
1231
+ for (let attempt = 0; !landed && attempt < 10; attempt++) {
1232
+ const candidate = attempt === 0 ? dest : `${dest}.${attempt}`;
1233
+ try {
1234
+ writeFileSync(candidate, text, { encoding: "utf8", flag: "wx" });
1235
+ landed = true;
1236
+ }
1237
+ catch (err) {
1238
+ if (!(err instanceof Error && "code" in err && err.code === "EEXIST"))
1239
+ throw err;
1240
+ }
1241
+ }
1242
+ if (!landed)
1243
+ writeFileSync(`${dest}.${process.pid}`, text, { encoding: "utf8", flag: "wx" });
1229
1244
  captured = true;
1230
1245
  }
1231
1246
  catch (err) {
@@ -112,6 +112,15 @@ export declare const ORG_ADJUDICATION_TIMEOUT_MS = 15000;
112
112
  export declare function settleOrgVerdictWithin<T>(p: Promise<T>, fallback: T, opts: {
113
113
  signal?: AbortSignal;
114
114
  timeoutMs: number;
115
+ /**
116
+ * backlog #136④ — which arm WON, reported at the moment it won. A consumer that wants to tell a
117
+ * cancelled wait from an unreadable provider cannot get that by sampling `signal.aborted` after
118
+ * the await: a cancellation queued between the settlement and the continuation reads identically
119
+ * to one that actually ended the wait, and the operator-facing account would name the wrong
120
+ * cause. Called at most once, before `p` resolves, and only for the two non-provider arms —
121
+ * absent call ⇒ the awaited promise itself settled.
122
+ */
123
+ onFallback?: (cause: "aborted" | "timeout") => void;
115
124
  }): Promise<T>;
116
125
  /** The `decisionReason` of a decision an ORG RULE produced (a deny, or a non-dismissable ask). Same
117
126
  * single-spelling contract as {@link ORG_UNAVAILABLE_DECISION_REASON}. */