@sema-agent/core 5.65.0 → 7.0.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 (165) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dist/agents/subagent.d.ts +2 -2
  3. package/dist/agents/subagent.js +11 -0
  4. package/dist/agents/verify.d.ts +1 -1
  5. package/dist/brain/anthropic.js +1 -1
  6. package/dist/brain/errors.d.ts +29 -0
  7. package/dist/brain/errors.js +20 -0
  8. package/dist/brain/open-responses.js +2 -2
  9. package/dist/brain/route-adjudicator.d.ts +8 -1
  10. package/dist/brain/route-adjudicator.js +1 -0
  11. package/dist/brain/status-sink.js +12 -1
  12. package/dist/brain/stream-engine.js +17 -6
  13. package/dist/core/auto-compaction.d.ts +26 -0
  14. package/dist/core/auto-compaction.js +7 -2
  15. package/dist/core/auto-mode-arming.d.ts +138 -0
  16. package/dist/core/auto-mode-arming.js +181 -0
  17. package/dist/core/auto-mode-defaults.d.ts +13 -0
  18. package/dist/core/auto-mode-defaults.js +5 -0
  19. package/dist/core/auto-mode-prompt.d.ts +14 -3
  20. package/dist/core/auto-mode-prompt.js +10 -7
  21. package/dist/core/auto-mode-rebuild.d.ts +75 -0
  22. package/dist/core/auto-mode-rebuild.js +41 -0
  23. package/dist/core/auto-mode.d.ts +15 -0
  24. package/dist/core/auto-mode.js +4 -2
  25. package/dist/core/checkpoint-store.d.ts +18 -0
  26. package/dist/core/context-edit.d.ts +47 -5
  27. package/dist/core/context-guard.d.ts +1 -1
  28. package/dist/core/file-history-retention.d.ts +106 -0
  29. package/dist/core/file-history-retention.js +36 -0
  30. package/dist/core/file-history-store.d.ts +768 -0
  31. package/dist/core/file-history-store.js +880 -0
  32. package/dist/core/governance-codes.d.ts +13 -3
  33. package/dist/core/governance-codes.js +15 -0
  34. package/dist/core/hooks.d.ts +39 -7
  35. package/dist/core/hooks.js +38 -21
  36. package/dist/core/lsp.d.ts +2 -2
  37. package/dist/core/mcp.d.ts +29 -7
  38. package/dist/core/memory-engine/consolidation-driver.d.ts +11 -0
  39. package/dist/core/memory-engine/consolidation-driver.js +71 -4
  40. package/dist/core/memory-engine/consolidation.d.ts +25 -2
  41. package/dist/core/memory-engine/consolidation.js +4 -1
  42. package/dist/core/memory-engine/distiller.d.ts +84 -1
  43. package/dist/core/memory-engine/distiller.js +68 -0
  44. package/dist/core/memory-engine/dual-root.js +3 -0
  45. package/dist/core/memory-engine/engine.d.ts +328 -15
  46. package/dist/core/memory-engine/engine.js +355 -29
  47. package/dist/core/memory-engine/file-backend.d.ts +30 -0
  48. package/dist/core/memory-engine/file-backend.js +14 -13
  49. package/dist/core/memory-engine/frontmatter.d.ts +22 -1
  50. package/dist/core/memory-engine/frontmatter.js +3 -0
  51. package/dist/core/memory-engine/header-hints.d.ts +5 -0
  52. package/dist/core/memory-engine/index.d.ts +5 -4
  53. package/dist/core/memory-engine/index.js +5 -4
  54. package/dist/core/memory-engine/layout.d.ts +88 -2
  55. package/dist/core/memory-engine/layout.js +112 -3
  56. package/dist/core/memory-engine/provenance-wording.d.ts +7 -0
  57. package/dist/core/memory-engine/provenance-wording.js +3 -0
  58. package/dist/core/memory-engine/tools.d.ts +89 -8
  59. package/dist/core/memory-engine/tools.js +263 -22
  60. package/dist/core/memory-engine/types.d.ts +64 -1
  61. package/dist/core/memory-recall.d.ts +6 -0
  62. package/dist/core/memory.d.ts +27 -1
  63. package/dist/core/memory.js +16 -2
  64. package/dist/core/permission-rule-consent.d.ts +62 -2
  65. package/dist/core/permission-rule-consent.js +105 -14
  66. package/dist/core/permission-rule-model.d.ts +118 -16
  67. package/dist/core/permission-rule-model.js +57 -9
  68. package/dist/core/permission-rule-session.d.ts +124 -0
  69. package/dist/core/permission-rule-session.js +121 -0
  70. package/dist/core/permission-rule-store.d.ts +65 -2
  71. package/dist/core/permission-rule-store.js +75 -16
  72. package/dist/core/permission-rule-sync.d.ts +9 -0
  73. package/dist/core/permission-rule-sync.js +52 -19
  74. package/dist/core/retention-policy.d.ts +9 -0
  75. package/dist/core/retention-policy.js +5 -2
  76. package/dist/core/retention.d.ts +13 -2
  77. package/dist/core/runner/assemble-result.d.ts +19 -1
  78. package/dist/core/runner/assemble-result.js +17 -2
  79. package/dist/core/runner/compaction-call-options.d.ts +93 -0
  80. package/dist/core/runner/compaction-call-options.js +3 -0
  81. package/dist/core/runner/memory-capture-optout.d.ts +80 -0
  82. package/dist/core/runner/memory-capture-optout.js +53 -0
  83. package/dist/core/runner/prepare-config-doors.d.ts +5 -0
  84. package/dist/core/runner/prepare-config-doors.js +16 -0
  85. package/dist/core/runner/prepare-hands-readface.d.ts +110 -5
  86. package/dist/core/runner/prepare-hands-readface.js +99 -7
  87. package/dist/core/runner/prepare-memory.d.ts +88 -0
  88. package/dist/core/runner/prepare-memory.js +305 -24
  89. package/dist/core/runner/prepare-task.d.ts +141 -1
  90. package/dist/core/runner/prepare-task.js +478 -81
  91. package/dist/core/runner/runtask.d.ts +9 -20
  92. package/dist/core/runner/runtask.js +133 -96
  93. package/dist/core/runner/session-file-state-replay.d.ts +18 -10
  94. package/dist/core/runner/session-file-state-replay.js +52 -1
  95. package/dist/core/runner/tool-disclosure.js +2 -1
  96. package/dist/core/runner/turn-attachments.d.ts +22 -12
  97. package/dist/core/session-store.d.ts +1 -1
  98. package/dist/core/session-store.js +6 -1
  99. package/dist/core/session.d.ts +34 -1
  100. package/dist/core/store-contracts/file-history-store-contract.d.ts +3 -0
  101. package/dist/core/store-contracts/file-history-store-contract.js +720 -0
  102. package/dist/core/store-contracts/permission-rule-sync-contract.js +15 -1
  103. package/dist/core/task-registry-shared.js +11 -1
  104. package/dist/core/tool-errors.js +1 -0
  105. package/dist/core/tool-policy.d.ts +172 -1
  106. package/dist/core/tool-policy.js +32 -1
  107. package/dist/core/tool-result-store.js +2 -1
  108. package/dist/core/trace.d.ts +31 -2
  109. package/dist/core/types.d.ts +795 -89
  110. package/dist/core/types.js +4 -3
  111. package/dist/core/untrusted-text.d.ts +1 -1
  112. package/dist/core/untrusted-text.js +8 -0
  113. package/dist/core/workflow-run-store-contract.js +6 -2
  114. package/dist/core/workflow-run-store.d.ts +4 -1
  115. package/dist/engine/compaction/compaction.d.ts +88 -10
  116. package/dist/engine/compaction/compaction.js +109 -30
  117. package/dist/engine/execution-env/node-execution-env.d.ts +9 -1
  118. package/dist/engine/execution-env/node-execution-env.js +28 -0
  119. package/dist/engine/harness/agent-harness.d.ts +52 -1
  120. package/dist/engine/harness/agent-harness.js +36 -1
  121. package/dist/engine/harness/types.d.ts +26 -1
  122. package/dist/engine/llm/types.d.ts +50 -4
  123. package/dist/engine/loop/agent-loop.d.ts +5 -1
  124. package/dist/engine/loop/agent-loop.js +25 -0
  125. package/dist/engine/loop/types.d.ts +19 -0
  126. package/dist/engine/lsp/node-lsp-manager.d.ts +1 -1
  127. package/dist/engine/session/session.js +1 -1
  128. package/dist/index.d.ts +20 -9
  129. package/dist/index.js +16 -7
  130. package/dist/orchestration/run-workflow-tool.d.ts +20 -2
  131. package/dist/orchestration/run-workflow-tool.js +22 -3
  132. package/dist/orchestration/workflow-governance.d.ts +59 -1
  133. package/dist/orchestration/workflow-governance.js +61 -8
  134. package/dist/orchestration/workflow-meta.d.ts +4 -2
  135. package/dist/orchestration/workflow-primitives.js +56 -13
  136. package/dist/orchestration/workflow-types.d.ts +78 -2
  137. package/dist/orchestration/workflow.d.ts +20 -0
  138. package/dist/orchestration/workflow.js +163 -14
  139. package/dist/prompt-assembly/event-registry.js +1 -1
  140. package/dist/prompts/default.d.ts +7 -7
  141. package/dist/stores/file/file-history-store.d.ts +368 -0
  142. package/dist/stores/file/file-history-store.js +1248 -0
  143. package/dist/stores/file/index.d.ts +22 -13
  144. package/dist/stores/file/index.js +4 -4
  145. package/dist/stores/file/permission-rule-store.d.ts +11 -0
  146. package/dist/stores/file/permission-rule-store.js +23 -9
  147. package/dist/stores/file/strategy-store.d.ts +3 -3
  148. package/dist/tools/fs/bash-readonly-classifier.d.ts +87 -3
  149. package/dist/tools/fs/bash-readonly-classifier.js +106 -4
  150. package/dist/tools/fs/fs-bash.js +9 -5
  151. package/dist/tools/fs/fs-shared.d.ts +52 -1
  152. package/dist/tools/fs/fs-shared.js +14 -0
  153. package/dist/tools/fs/fs-write.d.ts +5 -5
  154. package/dist/tools/fs/fs-write.js +71 -14
  155. package/dist/tools/fs/index.d.ts +6 -1
  156. package/dist/tools/fs/index.js +1 -1
  157. package/dist/tools/web.js +2 -1
  158. package/package.json +5 -1
  159. package/test/export-surface.snapshot.json +169 -23
  160. package/dist/core/file-snapshot-store.d.ts +0 -165
  161. package/dist/core/file-snapshot-store.js +0 -259
  162. package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +0 -13
  163. package/dist/core/store-contracts/file-snapshot-store-contract.js +0 -134
  164. package/dist/stores/file/file-snapshot-store.d.ts +0 -58
  165. package/dist/stores/file/file-snapshot-store.js +0 -353
@@ -119,11 +119,11 @@ export type ToolContentOrigin = "external" | "execution" | "local";
119
119
  * design/77 §4 — what a {@link ToolSpec.reversibilityProbe} returns for ONE call.
120
120
  *
121
121
  * `reversible` alone decides: the gate tightens a surviving `allow` to `ask` unless it is exactly `true`,
122
- * and neither optional member can widen that. They exist so the resulting approval card can say WHY, a
122
+ * and no optional member can widen that. They exist so the resulting approval card can say WHY, a
123
123
  * thing the probe knows at the moment it decides and nothing downstream can re-derive.
124
124
  *
125
125
  * Supply `cause` when the reason has parts a consumer should render itself; `reason` when it is prose
126
- * this engine cannot interpret. Both are optional and independent, so a probe written against the
126
+ * this engine cannot interpret. All three are optional and independent, so a probe written against the
127
127
  * original bare `{ reversible }` shape remains valid.
128
128
  */
129
129
  export interface ReversibilityVerdict {
@@ -133,6 +133,27 @@ export interface ReversibilityVerdict {
133
133
  /** Structured account — see {@link import("./checkpoint-store.js").ProbeCause}. Validated by the gate;
134
134
  * a malformed value costs the DISCLOSURE only, never the ask. */
135
135
  cause?: import("./checkpoint-store.js").ProbeCause;
136
+ /**
137
+ * #502 (additive) — this demotion is STRUCTURAL, not the probe hesitating: the call crosses a
138
+ * boundary the deployment declared, so a persisted allow rule must not retire the resulting ask.
139
+ * The gate folds it into the #144 mandate family ("allow rules silence the CLASSIFIER's questions,
140
+ * never a MANDATED one") beside an operator `shellGate:"always"` and the tool's own
141
+ * egress/irreversibility marks: a matching rule is DISCLOSED as shadowed instead of clearing the
142
+ * ask, and the card's "stop asking me this" offer is withheld rather than offered and then refused.
143
+ *
144
+ * Read on the demotion arm only — a `reversible: true` verdict ends the gate with no ask to mandate,
145
+ * and this member cannot make one. It is the one member a DEPLOYMENT-supplied probe may set that the
146
+ * engine acts on, and it is safe for the same reason the probe itself is: the effect is strictly
147
+ * TIGHTENING (one more question survives one more configuration), so an over-claiming probe costs
148
+ * its own deployment a re-confirmation and can never turn an ask into an allow.
149
+ *
150
+ * The built-in shell probe sets it for exactly one family: a listed reader naming a path outside the
151
+ * session's allowed directories. The engine's own out-of-root demotion has no clearing channel in
152
+ * this version other than confirming the call each time — a directory-scoped grant that WOULD clear
153
+ * it is a separate, later mechanism, and until it exists a surface must not word the refusal as
154
+ * though one already answers it.
155
+ */
156
+ mandated?: boolean;
136
157
  }
137
158
  export interface ToolSpec<TParams extends TSchema = TSchema> {
138
159
  /**
@@ -280,7 +301,9 @@ export interface ToolSpec<TParams extends TSchema = TSchema> {
280
301
  * - `"maybe"` — reversibility depends on the args; the gate calls {@link reversibilityProbe} (when one is
281
302
  * declared) on a surviving `allow` and tightens to `ask` UNLESS the probe reports reversible. A tool
282
303
  * that declares a `reversibilityProbe` but omits `irreversibility` defaults to `"maybe"` (so it always
283
- * enters the activation set and is gated).
304
+ * enters the activation set and is gated). Since #502 the probe ALSO runs over a surviving `ask`
305
+ * — see {@link reversibilityProbe} for what that arm reads (only {@link ReversibilityVerdict.mandated})
306
+ * and what it costs; it tightens nothing there, because an ask cannot be tightened into an ask.
284
307
  * - `"always"` — irreversible; the gate ALWAYS tightens a surviving `allow` to `ask` (like {@link egress}).
285
308
  *
286
309
  * The tighten mirrors the egress tighten (it only ever tightens allow→ask; a `deny`/`ask` is untouched),
@@ -296,13 +319,28 @@ export interface ToolSpec<TParams extends TSchema = TSchema> {
296
319
  irreversibility?: "never" | "maybe" | "always";
297
320
  /**
298
321
  * design/77 §4: optional deployment-injected probe for an `irreversibility:"maybe"` tool. Given the call's
299
- * (post-hook) args, it reports whether THIS specific call is reversible. The gate calls it ONLY when the
300
- * surviving decision is `allow` and `irreversibility` resolves to `"maybe"`; it is time-bounded (the
322
+ * (post-hook) args, it reports whether THIS specific call is reversible. It is time-bounded (the
301
323
  * approval timeout when configured, a 30s default when absent — the probe wait is always finite) and
302
324
  * **fail-closed**: a non-`reversible` verdict, a timeout, or a throw all tighten to
303
325
  * `ask`. Declaring this probe defaults `irreversibility` to `"maybe"`. It is read from the spec at
304
326
  * prepare-time and captured in a closure — NOT a tool argument — so the model cannot monkey-patch it.
305
327
  *
328
+ * **WHEN THE GATE CALLS IT — two arms, and the second is newer than this paragraph's first draft**
329
+ * (#502; `irreversibility` must resolve to `"maybe"` for either):
330
+ * 1. the surviving decision is `allow` — the TIGHTEN arm: the verdict decides allow→ask, and a
331
+ * tightening verdict's `reason`/`cause` ride the minted ask;
332
+ * 2. the surviving decision is already `ask` — the MANDATE arm: the probe is the only source of
333
+ * {@link ReversibilityVerdict.mandated}, and an ask another layer raised needed it too. (Before
334
+ * #502 a policy that asked FIRST skipped this block entirely, so an out-of-root read never
335
+ * acquired its mandate and a covering allow rule retired the question — measured, not theorised.)
336
+ * Here the `reversible` verdict is DISCARDED (a probe may never un-ask another layer's question),
337
+ * `reason`/`cause` are not merged onto somebody else's ask, and only `mandated` is read.
338
+ * The TIGHTEN is still `allow`→`ask` only — a `deny`/`ask` is never re-minted — but the INVOCATION
339
+ * set is wider than the tighten's, and that difference is the deployment's to budget for: on an
340
+ * ask-first policy the probe is called (and awaited, to the bound above) on every `"maybe"`-tier
341
+ * call, and a probe that throws or times out reports through `onError` with `phase: "hook"` on
342
+ * those calls too. A probe should therefore be a cheap, side-effect-free inspection of the args.
343
+ *
306
344
  * **`reason` (optional, additive both ways)** — WHY this call was not proven reversible, in the probe's
307
345
  * own words, minted in the same pass that reached the verdict (so nothing downstream re-derives it).
308
346
  * Read ONLY on a tightening verdict: a `reversible: true` return ends the gate, and a timeout/throw
@@ -888,6 +926,43 @@ export interface ToolExecuteContext {
888
926
  * inference decides, exactly as before the seat existed.
889
927
  */
890
928
  memoryPersistenceCapable?: boolean;
929
+ /**
930
+ * design/383 §2.5 — TRUE ⇔ the spawning session is under a memory-capture OPT-OUT at the moment
931
+ * a delegation tool reads this seat (declared at its prepare, a standing record, or a mid-run
932
+ * flip — the seat is live, not a prepare-time snapshot). The delegation lanes forward it into
933
+ * every child's trusted internals as the capture FLOOR (`memoryCaptureFloor`): a child of an
934
+ * opted-out session captures nothing, whatever its chosen AgentDefinition says — the opt-out is
935
+ * a floor no selection loosens (the `memoryPersistenceCapable:false` floor's exact law, on the
936
+ * privacy axis). Trusted Runner-filled seat, never a model/tool argument.
937
+ */
938
+ memoryCaptureOptedOut?: boolean;
939
+ /**
940
+ * design/383 §2.5 (rescan post-6.0.0-RC) — the floor seat's THIRD state: TRUE ⇔ the spawning
941
+ * session's capture opt-out state is INDETERMINATE at the moment a delegation tool reads this
942
+ * seat (its capture record store is faulting, and no genuine record is known in-process). The
943
+ * delegation lanes forward it into the child's trusted internals as the floor's indeterminate
944
+ * twin (`memoryCaptureFloorIndeterminate`): the child's prepare re-resolves it against the live
945
+ * record query — an ancestor record found takes the ordinary floor arm; a still-faulting store
946
+ * runs the child under the same fail-closed suppression an own-store outage imposes (no
947
+ * irreversible record is ever minted off an unreadable state); a readable-and-clean answer
948
+ * proceeds clean. Never TRUE beside {@link memoryCaptureOptedOut} — a known opt-out is
949
+ * determinate. Trusted Runner-filled seat, never a model/tool argument.
950
+ */
951
+ memoryCaptureIndeterminate?: boolean;
952
+ /** design/383 §2.5 — the spawning session's write-plane control dir (the coordinate its capture
953
+ * record is keyed under), forwarded beside the floor bit so a cross-plane child's record-query
954
+ * leg reads the PARENT's carrier, not its own plane's. Trusted Runner-filled seat. */
955
+ memoryCaptureControlDir?: string;
956
+ /** design/383 §2.5 (codex round 3) — the WHOLE ancestor chain's capture-record coordinates
957
+ * ({sessionId, controlDir?} per generation, root first), appended one row per spawn. The
958
+ * record-query leg walks ALL of it: a root that flips after spawning an already-running child
959
+ * AND grandchild is invisible to the grandchild's one-hop parent query (the clean middle
960
+ * session has no record of its own to find), while the chain reaches the root directly.
961
+ * Trusted Runner-filled seat, never a model argument. */
962
+ memoryCaptureAncestors?: ReadonlyArray<{
963
+ sessionId: string;
964
+ controlDir?: string;
965
+ }>;
891
966
  /**
892
967
  * the parent task's per-model auth hook ({@link TaskSpec.getApiKeyAndHeaders}), inherited
893
968
  * verbatim down the delegation tree like `principal`/`clientContext` (Runner-filled, read-only, NEVER
@@ -1155,6 +1230,48 @@ export type BeforeWriteResult = {
1155
1230
  reason: string;
1156
1231
  };
1157
1232
  export type BeforeWriteHook = (req: BeforeWriteRequest) => BeforeWriteResult | undefined | Promise<BeforeWriteResult | undefined>;
1233
+ /**
1234
+ * design/381 — the hands band's FIRST-TOUCH HISTORY hook, called before EVERY file mutation in
1235
+ * Write/Edit/NotebookEdit (after the `beforeWrite` content gate passed, immediately before the
1236
+ * final env write) with the resolved canonical containment key. The Runner wires it to
1237
+ * `FileHistoryStore.trackEdit` (backup-before-first-edit, the CC FileHistory trigger-point set
1238
+ * minus the bash simulation arm sema does not have — DV-7); later calls for an already-tracked
1239
+ * path are cheap store no-ops. `{ok:false}` carries the REFUSAL TEXT the tool must answer instead
1240
+ * of writing (DV-14 default: no durable first-touch state ⇒ the edit is refused, typed and loud —
1241
+ * any proceed-unprotected policy is resolved INSIDE the hook by the Runner, never by the band).
1242
+ * A THROWING hook fails CLOSED (same posture as the write gate). Absent hook ⇒ byte-identical
1243
+ * behavior (no history store wired).
1244
+ */
1245
+ export interface TrackEditRequest {
1246
+ tool: "Write" | "Edit" | "NotebookEdit";
1247
+ /** The model-supplied path argument (for the error-message coordinate the model knows). */
1248
+ path: string;
1249
+ /** The resolved canonical containment key — the identity the history record is keyed on. */
1250
+ key: string;
1251
+ /** The tool call's abort signal, threaded into the store's env read. */
1252
+ signal?: AbortSignal;
1253
+ }
1254
+ export type TrackEditResult = {
1255
+ ok: true;
1256
+ /**
1257
+ * #491 — the compensating handle for the ordering the topology cannot avoid: the pre-image
1258
+ * capture runs BEFORE the write, so a write that then FAILS (lost create race, ENOSPC,
1259
+ * EACCES, read-only mount) would leave a first-touch record for an edit that never happened —
1260
+ * and an `existed-not` record makes an untouched path a rewind DELETE target, removing bytes
1261
+ * whoever writes them next (the user, a peer process) owns. Present ONLY when THIS call minted
1262
+ * the record (an already-tracked path belongs to the edit that first touched it and must never
1263
+ * be discarded); the band calls it on the write-failure arm of every mutation lane. `proof`
1264
+ * says which retraction rule applies: `"proven"` = the write's own error code guarantees
1265
+ * nothing was written, `"verify"` = ambiguous, so the history seat must re-read the path and
1266
+ * retract only if it still matches what the first touch recorded. It never throws and never
1267
+ * blocks the failure it accompanies — the original write error is the answer.
1268
+ */
1269
+ annul?: (proof: "proven" | "verify") => Promise<void>;
1270
+ } | {
1271
+ ok: false;
1272
+ refusal: string;
1273
+ };
1274
+ export type TrackFileEditHook = (req: TrackEditRequest) => Promise<TrackEditResult>;
1158
1275
  /**
1159
1276
  * design/141 件2 — the SAFE deployment-configurable subset of the hands toolkit ({@link RunnerDeps.hands}).
1160
1277
  * Only fields whose injection is purely additive for a deployment are here; Runner-internal orchestration
@@ -1845,11 +1962,13 @@ export interface TaskSpec {
1845
1962
  * rewinding to before the very first message = start a NEW session. (A first-class branch-reset entry
1846
1963
  * that could support this without a second root is a recorded design candidate, not built.)
1847
1964
  *
1848
- * E19 interaction: with {@link rewindFiles}, the `"before"` file anchor is NOT the target's own entry id
1849
- * (snapshots key turn-END leaves, and a user message never has one) it is the nearest snapshotted
1850
- * ANCESTOR of the target, so files and context land on the same side of the cut. If no such snapshot
1851
- * exists, prepare fails loud (`rewind_snapshot.unresolvable`) instead of silently leaving the files at
1852
- * their pre-rewind state while the conversation moved back.
1965
+ * File-history interaction (design/381): with {@link restoreFiles}, the `"before"` file anchor is the
1966
+ * target's OWN entry id first history boundaries are keyed by a turn's initial entry and mean "file
1967
+ * state when that turn began", so B(target) IS the before-state. When the target carries no boundary
1968
+ * (a mixed-epoch session, or a non-turn-initial target), the anchor falls back to the nearest
1969
+ * boundary-carrying ANCESTOR, so files and context land on the same side of the cut. If no such
1970
+ * boundary exists, prepare fails loud (`rewind_snapshot.unresolvable`) instead of silently leaving
1971
+ * the files at their pre-rewind state while the conversation moved back.
1853
1972
  * Fail-loud: setting this without {@link resumeAt}, or an unknown mode value, rejects at prepare.
1854
1973
  */
1855
1974
  resumeAtMode?: "at" | "before";
@@ -1943,22 +2062,47 @@ export interface TaskSpec {
1943
2062
  role?: ModelRole;
1944
2063
  };
1945
2064
  /**
1946
- * design/101 §E19opt-in working-tree **rewind**: when this task uses {@link resumeAt} to branch at a prior
1947
- * message AND a snapshot was captured for that message, RESTORE the files to how they were at that message
1948
- * (pairs with E18: branch the session + restore the tree). Also makes every COMPLETED turn capture a
1949
- * snapshot keyed by its leaf `SessionTreeEntry.id`. Requires `RunnerDeps.fileSnapshotStore`. **v1 is
1950
- * LOCAL-only** a remote ({@link RunnerDeps.executionEnvFactory}) env defers rewind to its own VM-snapshot
1951
- * backend behind the same seam. OFF/omitted = zero snapshot/restore work.
1952
- */
1953
- rewindFiles?: boolean;
1954
- /**
1955
- * design/101 §E19 + R8 (CC Rewind `code`-only mode) restore the working tree to the snapshot captured at
1956
- * this prior message entryId WITHOUT forking the conversation (no {@link resumeAt} leaf-branch). This is CC's
1957
- * "Restore code" mode (vs "Restore code and conversation" = {@link resumeAt}+{@link rewindFiles}, and "Restore
1958
- * conversation" = {@link resumeAt} alone). Honored ONLY when {@link resumeAt} is ABSENT (with `resumeAt` set,
1959
- * that branch's own `rewindFiles` governs they are different operations); a no-snapshot target is an
1960
- * expected no-op (same as `rewindFiles`). Requires `RunnerDeps.fileSnapshotStore`. The session leaf is
1961
- * untouched the next turn continues the CURRENT conversation, only the files moved.
2065
+ * design/381 (DV-9)the RESTORE half of file rewind: when this task uses {@link resumeAt} to branch at
2066
+ * a prior entry, ALSO converge the working tree's **agent-edited files** (the tracked set files this
2067
+ * session touched through Edit/Write/NotebookEdit; nothing else is ever visible to the history, so a
2068
+ * user's parallel work is structurally untouchable) to the history boundary of that entry (pairs with
2069
+ * E18: branch the session + restore the tracked files). SCOPE = THIS session: a delegated
2070
+ * subagent runs under its own session and its edits land in its own history scope, so a parent
2071
+ * rewind does not converge files only a subagent touched (the retired whole-tree restore did —
2072
+ * a design/381 narrowing, disclosed here). Requires `RunnerDeps.fileHistoryStore`.
2073
+ * Fail-loud: setting this without {@link resumeAt} rejects at prepare (`rewind.invalid_spec`) — a
2074
+ * restore with no anchor is a contradiction, not a default.
2075
+ *
2076
+ * There is NO capture flag anymore: first-touch tracking is ALWAYS ON whenever a
2077
+ * `RunnerDeps.fileHistoryStore` is wired (per-edited-file backups are near-zero cost no tree walk,
2078
+ * no size gate), which is what retired the old `rewindFiles` capture request. The retired
2079
+ * `rewindFiles` spelling is handled at prepare: its capture sense (true, no `resumeAt`) is a
2080
+ * tolerated no-op with a deprecation disclosure (the intent is structurally satisfied); its restore
2081
+ * sense (true, with `resumeAt`) is a typed retirement rejection naming THIS field — the restore
2082
+ * semantics genuinely changed (whole-tree ⇒ tracked set), so that migration must be explicit.
2083
+ */
2084
+ restoreFiles?: boolean;
2085
+ /**
2086
+ * design/381 (DV-15) — opt-in tolerance for a PARTIAL file restore: when the {@link restoreFiles} /
2087
+ * {@link rewindFilesTo} convergence ends with ≥1 per-file refusal or failure (the others applied),
2088
+ * the default is a loud terminal failure carrying the per-file ledger (`rewind.restore_failed`) —
2089
+ * a silent continue would run the agent on a mixed-epoch tree without anyone knowing. Setting this
2090
+ * `true` keeps the run going instead and discloses the same ledger as a `restore_partial` note on
2091
+ * {@link TaskResult.rewindNotes} (the reference UI's "skipped files" seat in library form). The
2092
+ * documented recovery either way: re-run the same restore — it is per-file idempotent and converges.
2093
+ */
2094
+ acceptPartialRestore?: boolean;
2095
+ /**
2096
+ * design/101 §E19 R8 + design/381 (CC Rewind `code`-only mode) — converge the tracked set (see
2097
+ * {@link restoreFiles} for what that is and is not) to the history boundary of this prior entryId
2098
+ * WITHOUT forking the conversation (no {@link resumeAt} leaf-branch). This is CC's "Restore code"
2099
+ * mode (vs "Restore code and conversation" = {@link resumeAt}+{@link restoreFiles}, and "Restore
2100
+ * conversation" = {@link resumeAt} alone). Legal ONLY when {@link resumeAt} is ABSENT — handing
2101
+ * BOTH anchors is refused loud (`rewind.conflicting_targets`): two file targets is a
2102
+ * contradiction, and silently preferring one is the silent-swallow family this seam refuses; a
2103
+ * target with no history boundary fails loud (`rewind_snapshot.unresolvable`). Requires
2104
+ * `RunnerDeps.fileHistoryStore`. The session leaf is untouched — the next turn continues the
2105
+ * CURRENT conversation, only the files moved.
1962
2106
  */
1963
2107
  rewindFilesTo?: string;
1964
2108
  /**
@@ -2851,11 +2995,24 @@ export interface TaskSpec {
2851
2995
  * {@link budgetUsd}, and for the same reason): the lane only rides boundaries whose turn resolved
2852
2996
  * ≥1 tool call, so a boundary steer never EXTENDS a run that reached its natural end.
2853
2997
  *
2854
- * Default OFF like every other member of this family. NOTE: this relaxes the same design/74 "no
2855
- * budget language reaches the model" default that {@link budgetUsd} does an opted-in deployment
2856
- * chooses CC parity (where, since 2.1.245, this readout is on by default).
2998
+ * DEFAULT ON since design/374 slice 4 (ruled 2026-08-28) boolean, not `true`: explicit
2999
+ * `false` is the opt-out, the {@link backgroundTasks} / listing-family contract rather than the
3000
+ * `true`-only 133 form. BREAKING for a deployment that declares a token ceiling: pre-flip it saw
3001
+ * no `<total_tokens>` frame without opting in, and a probe pinning that absence reds.
3002
+ *
3003
+ * WHY THIS LANE AND NOT {@link budgetUsd}. CC 2.1.245+ ships the readout on by default (its own
3004
+ * settings schema reads "Defaults to padded-countdown"), which is the fact that reopened the
3005
+ * 2026-07-30 registration; the design/74 "no budget language reaches the model" default is
3006
+ * relaxed here for the same reason CC relaxed it. The BYOM honesty objection does not survive
3007
+ * either: core estimates nothing, publishes only the ceiling it already ENFORCES, and a task
3008
+ * that declares no token ceiling never fires the countdown arms at all — so the flip is
3009
+ * observable exactly in the deployments that asked for a ceiling. That property is EXACT, not
3010
+ * approximate: CC's two CONSTANT arms (`fixed`/`infinite`) publish a number with no ceiling
3011
+ * behind it, so the DEFAULT deliberately does not reach them — they stay behind an explicit
3012
+ * `true` here (see the runner's lane gate). The $ twin keeps its opt-in
3013
+ * default: CC has no equivalent default-on posture for it.
2857
3014
  */
2858
- totalTokensReminder?: true;
3015
+ totalTokensReminder?: boolean;
2859
3016
  /**
2860
3017
  * RB-318 — which arm of the readout, CC 2.1.245's closed set (`off` / `infinite` / `fixed` /
2861
3018
  * `countdown` / `padded-countdown`); absent ⇒ CC's own default `"padded-countdown"`.
@@ -3038,6 +3195,30 @@ export type EffectiveMemoryScopes = {
3038
3195
  };
3039
3196
  export interface TaskResult {
3040
3197
  taskId: string;
3198
+ /**
3199
+ * #499 — the ENGINE-minted identity of THIS run (uuidv7), minted once at the top of prepare and
3200
+ * never rewritten. The third id of the trio, and the only one the engine owns:
3201
+ * - {@link taskId} — the HOST's task identity, or (when the host named none) the session id: it is
3202
+ * `spec.taskId ?? sessionId` and therefore answers the SAME value for every run of one session;
3203
+ * - {@link sessionId} — the conversation, shared by every run that continues it;
3204
+ * - `runId` — this call. Two runs of one session ALWAYS differ here; a host-supplied `taskId` does
3205
+ * not change it (the host's handle and the engine's run are different questions).
3206
+ *
3207
+ * Use it to join a run's per-run disclosures (`task.user_steer_undrained` /
3208
+ * `task.user_followup_undrained` / `route.fallback_to_primary` / `steering.parked_input_blocked` /
3209
+ * `task.turn_interrupted` all carry `detail.runId`) and its `task.start`/`task.end` trace pair to
3210
+ * the result they belong to. Do NOT use it as an aggregation key across runs — that is what
3211
+ * `taskId`/`taskSignature` are for.
3212
+ *
3213
+ * **In-presence condition**: present on every terminal of a run that ENTERED prepare — a prepare-time
3214
+ * refusal included. The id is minted and published at prepare's FIRST statement precisely so this
3215
+ * holds: prepare itself emits run-scoped disclosures (`route.fallback_to_primary`), and a disclosure
3216
+ * naming a run that no terminal ever names would be unjoinable on exactly the failure path it is
3217
+ * about. It is absent only where no run was entered — a throw above the prepare call — and on
3218
+ * stub/synthesized results a host assembles itself. Optional for that reason, never fabricated, and
3219
+ * never the `"unknown"` sentinel `taskId`/`sessionId` fall back to on that path.
3220
+ */
3221
+ runId?: string;
3041
3222
  /** Use this to continue the same conversation on the next call. */
3042
3223
  sessionId: string;
3043
3224
  status: TaskStatus;
@@ -3204,6 +3385,36 @@ export interface TaskResult {
3204
3385
  * the ceiling was never evaluated (typically a store or decorator that predates the cost arm).
3205
3386
  */
3206
3387
  errorCode?: string;
3388
+ /**
3389
+ * Present when this run's terminal failure came from the PROVIDER BOUNDARY — the transport or the
3390
+ * provider failed the model call, as opposed to this deployment refusing to send one, a limit being
3391
+ * reached, or the model's own output being unusable. Its PRESENCE is the assertion; the members are
3392
+ * whatever the failing attempt actually stated about itself:
3393
+ * - `status` — the failing HTTP status, when the provider answered with one. Absent for a
3394
+ * connect failure, a stall, a mid-stream tear, and an in-band error frame delivered inside a
3395
+ * 200 — never zeroed, since 0 reads as a number a consumer may format.
3396
+ * - `requestId` — the provider's own request identifier for that attempt, when it stated one in a
3397
+ * shape worth carrying. This is the handle a provider's support channel asks for.
3398
+ * Both may be absent together: that is a provider failure nobody could label further, which is a
3399
+ * different statement from "not a provider failure" (the field itself absent).
3400
+ *
3401
+ * Nested rather than three loose keys so the assertion and its evidence cannot drift apart — there
3402
+ * is no state in which a consumer sees the claim beside a status belonging to some other attempt.
3403
+ * The same family rides the assistant message itself as `isApiErrorMessage` / `apiErrorStatus` /
3404
+ * `requestId`; this seat is its RUN-LEVEL projection, lifted from that message at the one assembly
3405
+ * point rather than re-derived, and the two spellings are named here so the mapping is written down
3406
+ * instead of inferred.
3407
+ *
3408
+ * PRESENCE, deliberately narrow: filled only when the provider failure is the terminal that NAMED
3409
+ * this result. A run that hit a provider failure and then a higher-ranked terminal (a budget
3410
+ * ceiling, a walltime cut) reports that other cause and leaves this absent — the same rule
3411
+ * {@link usageWindowRetryAfterMs} states for itself, and for the same reason: a diagnostic seat
3412
+ * beside a cause it does not belong to is worse than a missing one.
3413
+ */
3414
+ apiFailure?: {
3415
+ status?: number;
3416
+ requestId?: string;
3417
+ };
3207
3418
  /**
3208
3419
  * How long (milliseconds) until the deployment usage window that stopped this task frees up — the wait
3209
3420
  * hint a scheduler needs to decide WHEN to re-submit, rather than polling.
@@ -3259,24 +3470,32 @@ export interface TaskResult {
3259
3470
  * `errorCode="output.invalid"`, and this stays undefined.) */
3260
3471
  structuredOutput?: unknown;
3261
3472
  /**
3262
- * design/101 §E19 — NON-FATAL rewind disclosures for this run: the legs where the rewind machinery did
3263
- * something other than what the request's plain reading implies, reported as data instead of silence.
3264
- * Present only when at least one applies (absent = nothing to disclose), so a consumer that ignores the
3265
- * field is unchanged. A rewind that could NOT be delivered as asked is NOT a note — it is a terminal
3266
- * failure (`rewind_snapshot.unresolvable` / `rewind.store_unconfigured` / `rewind.restore_failed`).
3473
+ * design/101 §E19 + design/381 — NON-FATAL rewind disclosures for this run: the legs where the rewind
3474
+ * machinery did something other than what the request's plain reading implies, reported as data instead
3475
+ * of silence. Present only when at least one applies (absent = nothing to disclose), so a consumer that
3476
+ * ignores the field is unchanged. A rewind that could NOT be delivered as asked is NOT a note — it is a
3477
+ * terminal failure (`rewind_snapshot.unresolvable` / `rewind.store_unconfigured` /
3478
+ * `rewind.restore_failed` / `rewind.invalid_spec` / `rewind.rewind_files_retired` /
3479
+ * `rewind.conflicting_targets`).
3267
3480
  *
3268
3481
  * - `conversation_only` — {@link TaskSpec.resumeAt} branched the transcript WITHOUT
3269
- * {@link TaskSpec.rewindFiles}, so the working tree was deliberately left where it was (CC's "Restore
3270
- * conversation" mode). Legal and useful; the note is what makes the two axes' divergence visible.
3271
- * - `files_env_unsupported` — a capture or restore was requested but this deployment mounts no
3482
+ * {@link TaskSpec.restoreFiles}, so the working tree was deliberately left where it was (CC's
3483
+ * "Restore conversation" mode). Legal and useful; the note is what makes the two axes' divergence
3484
+ * visible.
3485
+ * - `files_env_unsupported` — a file restore was requested but this deployment mounts no
3272
3486
  * filesystem-capable `ExecutionEnv` (stub env), so the file axis was inert. Not an error: such a
3273
3487
  * deployment has no working tree to rewind.
3274
- * - `snapshot_store_unconfigured` — `rewindFiles` asked for per-turn capture but no
3275
- * {@link RunnerDeps.fileSnapshotStore} is wired, so this turn produced no snapshot and cannot be
3276
- * rewound to later. (Requesting a RESTORE with no store is a fail-loud configuration error instead.)
3488
+ * - `restore_partial` — {@link TaskSpec.acceptPartialRestore} tolerated a partial restore: ≥1 tracked
3489
+ * file was refused or failed while the others converged; the message carries the per-file ledger
3490
+ * summary (applied / identical / refused / failed). Present ONLY under that explicit opt-in the
3491
+ * default posture is the loud terminal `rewind.restore_failed` (DV-15).
3492
+ *
3493
+ * (design/381 closed-set surgery: `snapshot_store_unconfigured` is RETIRED — its only producer was
3494
+ * the per-task capture request, and capture is now always-on when a store is wired; a restore request
3495
+ * with no store keeps the fail-loud terminal instead.)
3277
3496
  */
3278
3497
  rewindNotes?: Array<{
3279
- code: "conversation_only" | "files_env_unsupported" | "snapshot_store_unconfigured";
3498
+ code: "conversation_only" | "files_env_unsupported" | "restore_partial";
3280
3499
  /** Human-readable statement of what did NOT happen and why — safe to show a user verbatim. */
3281
3500
  message: string;
3282
3501
  }>;
@@ -3293,6 +3512,27 @@ export interface TaskResult {
3293
3512
  * (a steer/follow-up) continued the run past the rejection and it later ended naturally.
3294
3513
  */
3295
3514
  haltedOnUserRejection?: true;
3515
+ /**
3516
+ * design/373 (#504) — present (`true`) exactly when a {@link TaskStream.halt} was ACCEPTED while
3517
+ * this run was live: the person issued the bare user interrupt (the CC Esc form — cut the
3518
+ * in-flight turn, stop at that boundary, wait for their next input). The ruled terminal form is
3519
+ * completed-with-marker, this seat being the marker: `status` is `"completed"` on the ordinary
3520
+ * path (a clean, resumable ending — nothing failed, nothing is suspended, the session continues
3521
+ * via the ordinary next-run front door), and this field is what tells a person-stopped ending
3522
+ * apart from a natural finish — same reading discipline as {@link haltedOnUserRejection}, its
3523
+ * sibling seat ("stopped by the user, awaiting their direction", never "the task finished its
3524
+ * work"). `result` is whatever the model had produced before the halt (possibly empty — a halt
3525
+ * can land before any output; the run then completes empty rather than failing). Pass-through on
3526
+ * EVERY terminal the run still reaches (a halt ACCEPTED first that then raced a real
3527
+ * failure/limit truthfully says a person also stopped it), and — the stated honest window — on
3528
+ * a natural completion the halt arrived too late to prevent (past the loop's final commit
3529
+ * point): the fact reported is the ACCEPTED HALT, and the transcript says how far the model
3530
+ * got. First-writer-wins against the run's abort (the `interrupt()` attribution law, the same
3531
+ * gate): a halt landing AFTER the abort signal already fired neither cut nor stopped anything —
3532
+ * that ending belongs to the abort, and this seat stays ABSENT rather than signing someone
3533
+ * else's stop with the halt caller's name. Absent everywhere else; never `false`.
3534
+ */
3535
+ haltedByUser?: true;
3296
3536
  /**
3297
3537
  * The deliveries of AskUserQuestion calls a person ANSWERED but whose call never executed to collect
3298
3538
  * the answer (the leg ended first — abort, batch teardown, or a loop failure). Rides EVERY terminal
@@ -3307,6 +3547,52 @@ export interface TaskResult {
3307
3547
  deliveryId: string;
3308
3548
  toolCallId: string;
3309
3549
  }>;
3550
+ /**
3551
+ * #481 — HOST-SUPPLIED: the approval plane's report that this leg ended with human approvals still
3552
+ * OUTSTANDING (the ask identities it is holding, and when the OLDEST of them was created).
3553
+ *
3554
+ * **Who writes it, and why not the engine.** Core mints no ask identity — an ask id belongs to the
3555
+ * deployment's approval store, and `resolveAsk` only awaits the host's `onAsk` — and core starts no
3556
+ * approval clock. So this seat is filled by the `Runner` a deployment hands to
3557
+ * `runWorkflow`/`startWorkflow`: that call both RAN the leg and owns the pending-approval map, which
3558
+ * is the only place the two facts meet without inventing an attribution. Core's own Runner never
3559
+ * writes it. ABSENT is therefore the ordinary world — an older deployment, or one whose approvals are
3560
+ * always answered inline — and every such leg behaves exactly as it did before this seat existed.
3561
+ *
3562
+ * **Three-state, deliberately.** Absent = "this deployment is not reporting outstanding approvals, or
3563
+ * there are none"; present = "these asks are outstanding". An EMPTY `askIds` is neither and is
3564
+ * refused: it would let a half-written record read as "waiting on nothing", which is precisely the
3565
+ * misdiagnosis the seat exists to prevent. A present-but-malformed value is announced and then
3566
+ * treated as no report — never silently folded into absence, and never granted the exemption below.
3567
+ *
3568
+ * **What core does with it** (`src/orchestration/workflow.ts`): a workflow agent leg whose progress
3569
+ * watchdog fired while an approval was outstanding was NOT stalled — it was waiting on a person, by
3570
+ * design — so that attempt is not charged against the stall-retry budget. The run's own
3571
+ * `totalTimeoutMs` remains the backstop for a wait nobody ever answers.
3572
+ */
3573
+ pendingApproval?: {
3574
+ askIds: readonly string[];
3575
+ oldestCreatedAtMs: number;
3576
+ };
3577
+ /**
3578
+ * #485 — HOST-SUPPLIED: how long this leg spent BLOCKED on human approvals, in ms, as the deployment's
3579
+ * approval plane measured it. Same writer and same absence contract as {@link pendingApproval}: the
3580
+ * approval clock belongs to whoever holds the ask, so the figure is SINGLE-SOURCED there and core neither
3581
+ * estimates it nor accumulates it (two sides publishing two numbers for one wait is the drift this seat
3582
+ * exists to avoid). ABSENT is the ordinary world.
3583
+ *
3584
+ * Already CUMULATIVE for the leg — a receipt states the leg's total wait so far, not that attempt's
3585
+ * increment — so a later receipt SUPERSEDES an earlier one. Adding successive receipts double-counts.
3586
+ *
3587
+ * **Absence is not zero.** `0` is "nobody was waited on"; absent is "this deployment does not supply the
3588
+ * fact". A consumer that defaults absence to `0` reports a deployment's silence as a measurement.
3589
+ *
3590
+ * What it is FOR: a workflow that hit its total timeout cannot otherwise say whether it was SLOW or
3591
+ * WAITING. `WorkflowRun.timeoutInterruption.approvalWaitedMs` carries the largest leg's figure onto the
3592
+ * terminal disclosure for exactly that reading — near `timeoutMs` ⇒ the window went on a person, not on
3593
+ * work.
3594
+ */
3595
+ approvalWaitedMs?: number;
3310
3596
  /**
3311
3597
  * #240 (design/199 v1.1) — the READ-face this leg's read surfaces actually judged under, as an
3312
3598
  * engine-filled OBSERVATION (never a knob: writing it on a spec does nothing). It is the run's ONE
@@ -3512,7 +3798,7 @@ export interface TaskResult {
3512
3798
  * byte change, always paired with a `marked` disclosure),
3513
3799
  * · `envelope` (text shaped like one of the engine's OTHER authority envelopes — the DISCLOSED
3514
3800
  * subset is `task-notification` / `new-diagnostics` / `user_memory` / `skills` /
3515
- * `total_tokens`; `scope` is
3801
+ * `total_tokens` / `instruction-files`; `scope` is
3516
3802
  * fenced but not disclosed, since `<scope>…</scope>` is also an ordinary build-file element.
3517
3803
  * That family carries no mark, so its sentence is positional rather than byte-testable. It
3518
3804
  * rides ON the reminder copy when both families hit, so `envelope` can be bumped alongside
@@ -3691,6 +3977,25 @@ export interface BrainStatus {
3691
3977
  * A wait longer than the re-announce slice is re-emitted with the REMAINING time, so a consumer
3692
3978
  * that simply overwrites its row always shows a live number instead of a stale one. */
3693
3979
  retryInMs?: number;
3980
+ /**
3981
+ * The WALL-CLOCK instant the announced wait is expected to end — `Date.now() + retryInMs`, read at
3982
+ * the moment the frame is emitted, in the EMITTING core process's clock. Stamped centrally at the
3983
+ * one emit point, so the invariant is total: present exactly when {@link retryInMs} is present, and
3984
+ * absent on every frame that announces no wait (`recovered` / `gave_up`, and the immediate
3985
+ * output-cap re-send, which takes no backoff).
3986
+ *
3987
+ * WHY IT IS PRODUCED HERE rather than derived by the consumer: `Date.now() + retryInMs` is only
3988
+ * correct for a reader that received the frame instantly. A consumer several process hops away
3989
+ * (serving layer → client) recomputes a deadline already shifted by the transit, so a countdown
3990
+ * built that way restarts on every re-announced slice instead of converging. The producer is the
3991
+ * only party that can state the instant, so it does.
3992
+ *
3993
+ * CLOCK DOMAIN, stated to remove the ambiguity: WALL CLOCK (`Date.now()`, epoch ms), NOT a
3994
+ * monotonic reading. A consumer must not compare it against its own monotonic timers, and must
3995
+ * treat it as approximate across a clock adjustment or a machine boundary — {@link retryInMs} stays
3996
+ * the authoritative RELATIVE quantity, and this seat is the absolute convenience derived from it.
3997
+ */
3998
+ retryAtMs?: number;
3694
3999
  /** RB-420-c — 1-based index of the attempt that just failed (the wait precedes attempt `attempt + 1`);
3695
4000
  * same numbering as the `brain.retry` telemetry frame. Absent on frames that are not a retry wait. */
3696
4001
  attempt?: number;
@@ -4200,7 +4505,7 @@ export type TaskEvent = ({
4200
4505
  * message-identity Phase 1 (the CC `SDKCompactBoundary` parity enabler): a persisted session-tree entry was
4201
4506
  * just COMMITTED for a RENDERABLE message the consumer has already streamed. `entryId` = `SessionTreeEntry.id`
4202
4507
  * (uuidv7, persisted, replica-stable) — the SAME id space as `compacted.preserved_segment.firstKeptEntryId`,
4203
- * E18 `resumeAt`, and E19 `rewindFiles`. `role` + (for a tool result) `toolCallId` let a consumer bind it to
4508
+ * E18 `resumeAt`, and the design/381 `rewindFilesTo`. `role` + (for a tool result) `toolCallId` let a consumer bind it to
4204
4509
  * the message it rendered: a tool result by its `toolCallId`; an assistant/user message by stream order (this
4205
4510
  * is the terminal boundary for the content just streamed, after its deltas and before its tool results). A
4206
4511
  * consumer builds its own `entryId → message` map from these to resolve `preserved_segment` WITHOUT the
@@ -4639,12 +4944,125 @@ export interface TaskStream extends AsyncIterable<TaskEvent> {
4639
4944
  * landing after the tool finished is a no-op. Unknown toolCallIds are a no-op (nothing to detach).
4640
4945
  */
4641
4946
  detach(toolCallId: string): void;
4947
+ /**
4948
+ * design/383 §2.1 — the MID-SESSION memory-capture opt-out verb ("聊着聊着发现不该记" — the
4949
+ * design's primary scenario): from this call on, the running session commits NOTHING to the
4950
+ * long-term memory store, its already-committed contributions leave the consolidation candidate
4951
+ * set (the retroactive A2 arm), and the session window's residue on the writable memory root is
4952
+ * boundary-swept into control-plane quarantine (per-path named; a sweep failure REFUSES the call
4953
+ * loudly — the one-way record stands either way, so the failure mode is "opt-out on, residue
4954
+ * named", never a silent partial).
4955
+ *
4956
+ * ONE-WAY: no reverse verb exists at any layer; a repeat call is an idempotent `"existed"`. The
4957
+ * way back to capture is a NEW session (a deliberate product cost — "which turns count" has no
4958
+ * honest answer for a retroactive re-enable).
4959
+ *
4960
+ * AUTHORITY (steer-lane law): core does not authenticate — holding this stream IS the capability
4961
+ * handle, exactly as with {@link steer}; a service exposing this verb over the wire owns the
4962
+ * verified-principal / session-ownership check at its own face (a bare sessionId relay would let
4963
+ * anyone switch off anyone's memory: an availability attack). The call re-adjudicates the
4964
+ * per-principal entitlement AT FLIP TIME (`RuntimeCaps.allowMemoryOptOut` under
4965
+ * {@link RunnerDeps.memoryCapturePolicy}, fresh resolve — a denied flip refuses typed
4966
+ * `memory.capture_optout_denied` and leaves ZERO record residue). MODEL-UNREACHABLE by
4967
+ * construction (§2.6): no tool face, no MCP face, and governed workflow specs cannot spell it —
4968
+ * a model that could declare an opt-out could silently disable a user's memory; one that could
4969
+ * revoke it would pierce the privacy control.
4970
+ *
4971
+ * Typed refusals (the closed set, one per arm — rescan post-6.0.0-RC completed the roster):
4972
+ * `memory.capture_optout_denied` (entitlement), `memory.capture_optout_unpersisted` (the record
4973
+ * could not be durably written — a store-mark failure OR a failing §2.7 consolidation-epoch
4974
+ * bump, both refused rather than held in-process, which the first resume would silently break),
4975
+ * `memory.capture_optout_unavailable` (this run mounted no memory session — nothing to opt out
4976
+ * of), `memory.capture_optout_sweep_failed` (the record STANDS but the §2.3 boundary sweep could
4977
+ * not contain named residue paths — a repeat call is the retry lane),
4978
+ * `config.memory_capture_unsupported` (remote/per-run deployment shape with no
4979
+ * {@link RunnerDeps.memoryCaptureRecordStore} — the record would not survive to a resume
4980
+ * replica), `steering.invalid_content` (a non-string `options.reason`), plus the steer-family
4981
+ * `steering.not_running` once the task has finished. Resolves `{ outcome }`: `"created"` = this
4982
+ * call made the crossing (the `memory.capture_opted_out` notice minted); `"existed"` = already
4983
+ * recorded (idempotent; no second notice).
4984
+ */
4985
+ optOutMemoryCapture(options?: {
4986
+ reason?: string;
4987
+ }): Promise<{
4988
+ outcome: "created" | "existed";
4989
+ }>;
4642
4990
  /**
4643
4991
  * Hard-**interrupt** the running task — a real abort (not a best-effort hint), equivalent to firing the
4644
4992
  * task's `signal` (design/47). No-op if the task already finished. For a SOFT redirect that lets the
4645
- * task keep running and adjust, use {@link steer} instead.
4993
+ * task keep running and adjust, use {@link steer} instead. For the CC-Esc "stop and wait for my
4994
+ * input" form — cut the turn, keep the ending clean and resumable — use {@link halt}.
4646
4995
  */
4647
4996
  interrupt(): Promise<void>;
4997
+ /**
4998
+ * design/373 (#504) — the BARE user interrupt, the CC Esc true form ("cut + stop"): CUT the
4999
+ * in-flight turn and STOP the run at that manufactured boundary, collecting to a clean,
5000
+ * resumable ending. The run-model equivalent of CC's session staying alive waiting for the
5001
+ * user's next input. Three verbs, three powers: {@link interrupt} hard-aborts the run (the
5002
+ * orphan-reconcile ending), {@link steer} with `priority:"now"` cuts the turn AND CONTINUES the
5003
+ * run with the steer text, `halt` cuts the turn and lets the run END awaiting the user.
5004
+ *
5005
+ * The CUT half is the design/373 S1 turn-scoped settle, verbatim: finished tool calls keep their
5006
+ * REAL results; never-started ones settle as paired interrupted results
5007
+ * (`errorKind:"interrupted_never_started"`); the CC-verbatim interruption marker lands in the
5008
+ * session as an engine-note user frame. Every REAL cut is announced (`task.turn_interrupted`
5009
+ * notice, `detail.cause:"user_halt"` — the steer-now landing keeps its own message and no
5010
+ * `cause` key). The STOP half: from acceptance on, NO new model turn starts — the loop ends at
5011
+ * whichever boundary comes first (both boundary faces consult the latch, the pre-turn one
5012
+ * included, so a halt landing between turns cannot buy one more turn). With nothing in flight
5013
+ * the cut half is a no-op (`turnCut:false`) and the stop half alone answers — no false marker.
5014
+ *
5015
+ * **Terminal form (#504 detail ①, ruled completed-with-marker over a new status/stop-reason
5016
+ * member):** the run ends `status:"completed"` with the additive result seat
5017
+ * {@link TaskResult.haltedByUser} `: true` as the discriminator. CC anchor: 250 has NO typed
5018
+ * "interrupted" terminal — Esc leaves a clean continuable session with the transcript marker as
5019
+ * the record, and the SDK result taxonomy (`error_during_execution`/`error_max_turns`/…) gains
5020
+ * no interrupt member; a new closed-set status/stop-reason row would therefore be a non-anchored
5021
+ * invention, while completed-plus-marker is also this repo's own precedent seat
5022
+ * ({@link TaskResult.haltedOnUserRejection} — "the model did not finish; the person stopped it
5023
+ * and the run awaits their direction"). Consumers discriminating "user stopped it" vs "ran to
5024
+ * completion" (billing, continuation copy) read the one boolean.
5025
+ *
5026
+ * **Queued-but-undelivered frames (#504 detail ②, ruled keep-don't-clear):** a halt clears
5027
+ * NOTHING. CC anchor: 250's `control_request:"interrupt"` handler keeps the command queue by
5028
+ * default and answers `still_queued`; clearing is the explicit `cancel_queued:true` opt-in
5029
+ * (`cleared_on_cancel`) — destruction is never the interrupt's default meaning. The run-model
5030
+ * translation: queued steer/follow-up frames stay queued through the cut, are NOT drained into a
5031
+ * turn that will never start, and settle by the run's EXISTING terminal contract for
5032
+ * accepted-but-undelivered input — durable park when a suspend seat exists, else the loud
5033
+ * per-run accounts (`task.user_steer_undrained` / `task.user_followup_undrained`). The receipt
5034
+ * semantics were always "queued = accepted, never consumed": the caller holds the bytes, the
5035
+ * account tells it what to re-send, and `inputId` idempotency makes host-side re-delivery on the
5036
+ * next run replay-safe. (A per-session durable park for non-suspend endings would be new storage
5037
+ * machinery — out of this slice, recorded.)
5038
+ *
5039
+ * **Resume hand-off (#504 detail ③, ruled the ordinary user-lane front door):** the next input
5040
+ * enters as the NEXT RUN's objective on the same session (`runTask`/`runTaskStream` with this
5041
+ * result's `sessionId`) — no special lane, no checkpoint. The halted transcript ends with the
5042
+ * interruption marker exactly where CC's session shows it (marker, then the user's next
5043
+ * message), so the resumed model reads the same shape a CC session shows after Esc. A
5044
+ * checkpoint-resume is neither minted nor needed: `"completed"` + `sessionId` IS the
5045
+ * continuation contract, and a marker-less halted transcript (nothing was in flight to cut) is
5046
+ * an ordinary clean boundary.
5047
+ *
5048
+ * Receipt: resolves `{ turnCut }` — `true` iff THIS call cut a live turn (the completion-race
5049
+ * arm is stated, not hidden: a turn whose work finished as the abort landed ends normally and
5050
+ * the run still stops at its boundary). Repeat calls are accepted and answer honestly
5051
+ * (`turnCut:false` once the stop is already latched); the notice is per-cut, the result seat
5052
+ * per-run. AUTHORITY (steer-lane law): holding this stream IS the capability — a deployment
5053
+ * relaying this verb owns its own gate (relay IS authorization; it cancels in-flight tool work
5054
+ * and ends the run's forward progress). No text ⇒ the `userPromptSubmit` screen has no domain
5055
+ * here (nothing enters the model). Honest window, stated: a halt accepted in the run's last
5056
+ * moments (past the loop's final commit point) finds the run completing naturally — the result
5057
+ * then reports the natural completion WITH the `haltedByUser` seat (the person did press stop
5058
+ * while it finished; the transcript says how far the model got). Typed refusals: the
5059
+ * steer-family `steering.not_running` once the task has finished (teardown included); a halt
5060
+ * issued BEFORE the run's first prompt polls the same bounded birth window as {@link steer} and
5061
+ * then stops the run before its first model turn (an empty, cleanly-halted completed run).
5062
+ */
5063
+ halt(): Promise<{
5064
+ turnCut: boolean;
5065
+ }>;
4648
5066
  /**
4649
5067
  * Reap this task's resources — **callable after it finished**, idempotent, never throws (design/50 §3,
4650
5068
  * design/51). The fan-out (v2) cancel primitive: an orchestrator's `cancelAll` is the shared
@@ -4771,6 +5189,31 @@ export interface RuntimeCaps {
4771
5189
  * deployment resolves to a no-op (asks flow the original chain; no warn — the gate, not a mistake).
4772
5190
  */
4773
5191
  autoMode?: boolean;
5192
+ /**
5193
+ * design/383 §3.1 — may this principal declare a session memory-capture OPT-OUT
5194
+ * (`TaskSpec.memory.capture: "off"` / the Runner flip verb)? `false` = the deployment REQUIRES
5195
+ * capture for this principal (compliance / knowledge retention) — the declaration is refused
5196
+ * terminal (`memory.capture_optout_denied`). `undefined` / `true` = no per-principal restriction.
5197
+ *
5198
+ * ⚠️ POLARITY REVERSAL — this member is the family's one deliberate exception to the
5199
+ * fail-closed resolver degrade (`{allowWorkflows:false, allowFork:false}`): on the PRIVACY axis
5200
+ * "deny" means "capture the session of a user who explicitly said no", which is the IRREVERSIBLE
5201
+ * side (a wrongly-captured session has been read, may be folded into products, may have synced —
5202
+ * deleting later does not unhappen it), while a wrongly-honored opt-out costs one uncaptured
5203
+ * session (recoverable). Under the default/`"open"` posture a resolver FAULT therefore reads as
5204
+ * ALLOW (+ a named `onError` disclosure), and only an explicit `false` denies; a deployment for
5205
+ * which the compliance side is the heavier irreversibility declares `memoryCapturePolicy:
5206
+ * "governed"`, which flips the fault arm to refuse-to-run (see {@link RunnerDeps.memoryCapturePolicy}).
5207
+ *
5208
+ * 🔒 FAULT-TRANSIT CONTRACT (server F2, written here because the resolver's habits run the other
5209
+ * way): a resolver failure for THIS member must surface as the member being ABSENT (`undefined`)
5210
+ * or as a THROW — both read as "fault". The resolver MUST NOT coin a stand-in explicit value on
5211
+ * failure (the `{allowWorkflows:false}` degrade habit): an explicit `false` IS the denied
5212
+ * verdict, and encoding an outage as one silently swaps the fault arm for the verdict arm — under
5213
+ * `"open"` that is the exact polarity inversion this member exists to prevent. A non-boolean
5214
+ * value here is treated as a fault (never truthiness-read) and disclosed.
5215
+ */
5216
+ allowMemoryOptOut?: boolean;
4774
5217
  }
4775
5218
  /**
4776
5219
  * design/129-B — one lifecycle event of a BACKGROUND delegation child, delivered to the
@@ -5071,7 +5514,9 @@ export interface EngineNotice {
5071
5514
  * `autoCompactTokens` ABOVE its physical window; the trigger-side geometry was clamped to the
5072
5515
  * physical window (an autocompact window only ever lowers the trigger) and the bad value is
5073
5516
  * announced once per prepared task; `detail: { modelId, declaredAutoCompactTokens,
5074
- * physicalWindow, sessionId }`.
5517
+ * physicalWindow, sessionId, runId }` — `runId` (#499) is what makes the "once per prepared
5518
+ * task" claim checkable: the session id beside it is shared by every run of the session, so two
5519
+ * runs clamping the same model used to mint byte-identical lines.
5075
5520
  * - `"config.env_timeout_discarded"` — a Bash timeout knob (option or env) held a value that is not
5076
5521
  * the value in force; `detail: { knob, raw, usedMs }`.
5077
5522
  * - `"config.materialize_env_discarded"` — `SEMA_TOOL_MATERIALIZE_STRATEGY` held a value outside the
@@ -5079,35 +5524,61 @@ export interface EngineNotice {
5079
5524
  * - `"tool_result.offload_put_failed"` (#167) — a clear-with-offload persist's fire-and-forget put
5080
5525
  * failed; THIS attempt stored nothing (the failure arm reports, it never re-inserts under the
5081
5526
  * ref) — an earlier attempt of the same idempotent re-put may already have stored the row, so
5082
- * the notice claims a failed write, not an empty ref; `detail: { ref, sessionId, cause }`.
5083
- * Per-occurrence, not per-process-deduplicated: each failed write is a distinct fact.
5527
+ * the notice claims a failed write, not an empty ref;
5528
+ * `detail: { ref, sessionId, cause, occurrenceId }`.
5529
+ * Per-occurrence, not per-process-deduplicated: each failed write is a distinct fact — and
5530
+ * `occurrenceId` (#495, uuidv7 minted at the failure) is what lets a consumer act on that:
5531
+ * `ref` names the ROW, and the same ref is attempted again on every later re-clear, so two
5532
+ * genuine losses of one ref were byte-identical notices. Dedup key: (code, occurrenceId).
5084
5533
  * - `"workflow.governance_key_stripped"` (#235) — the fields of an LLM-authored workflow `agent(spec)`
5085
5534
  * that did NOT cross the governed default-deny whitelist: an unrecognized/control-plane field, or a
5086
5535
  * `readFace` value that is not the containment-tightening one. The spawn PROCEEDS on the deployment
5087
5536
  * baseline (the strip is not a refusal in this window), which is why the drop is announced;
5088
- * `detail: { total, stripped: [{ key, reason }], omitted? }`, the rendered key list bounded in count
5089
- * and length because the names come from the untrusted script. One aggregated notice per governed
5090
- * child build, not de-duplicated across builds: each spec is a distinct fact.
5537
+ * `detail: { total, stripped: [{ key, reason }], omitted?, occurrenceId }`, the rendered key list
5538
+ * bounded in count and length because the names come from the untrusted script. One aggregated
5539
+ * notice per governed child build, not de-duplicated across builds: each spec is a distinct fact
5540
+ * `occurrenceId` (#495, uuidv7 minted at the announcement) carries that claim to the consumer,
5541
+ * which otherwise saw two builds of the same spec as one repeated notice. Dedup key:
5542
+ * (code, occurrenceId).
5543
+ * - `"workflow.agent_option_ignored"` (#505 ①) — the OPTIONS-object twin of the row above: keys of
5544
+ * `agent(spec, opts)`'s SECOND argument that were not applied (unrecognized, or a known option
5545
+ * whose value its type check rejected — `label: 42`). The spawn PROCEEDS without them, which is
5546
+ * why the drop is announced. A separate code because the two membranes accept different key
5547
+ * sets and the message names its own. `detail: { total, ignored: [key], omitted?, occurrenceId }`,
5548
+ * same bounding and same per-call (never de-duplicated) semantics as the spec-side row; dedup
5549
+ * key: (code, occurrenceId). NOT the whole family: a SPEC field written on the options object
5550
+ * (`model`, `modelName`, `thinking`, …) is REFUSED at the membrane instead — a silently ignored
5551
+ * `model` runs a different agent than the author asked for.
5091
5552
  * - `"mcp.revocation_probe_failed"` (design/338) — the deployment's `mcpRevocations.isRevoked`
5092
5553
  * probe threw; MCP dispatch FAILS OPEN (revocation is a tightening face) and this announces
5093
- * once per materialization (a resume re-materializes and may announce again). `detail: { message }`
5094
- * — no sessionId (a deployment wiring fact, not session-attributed), `"operator"` audience by
5554
+ * once per materialization (a resume re-materializes and may announce again).
5555
+ * `detail: { message, runId }` still no sessionId (a deployment wiring fact, not
5556
+ * session-attributed); `runId` (#499) makes the once-per-materialization claim CHECKABLE without
5557
+ * attributing the line to a conversation, an opaque invocation id saying which materialization
5558
+ * rather than whose. `"operator"` audience by
5095
5559
  * its explicit {@link NOTICE_AUDIENCE} row (#433 made the registry total over the catalog —
5096
5560
  * no engine-minted code is audience-defaulted any more). The refusal itself
5097
5561
  * (`mcp.server_revoked`) is a tool RESULT code, not a notice.
5098
5562
  * - `"config.models_swapped"` — `Runner.swapModels` replaced the model catalog generation
5099
- * (zero-restart model switching). `detail: { models, tiers }` — key COUNTS only, never the
5563
+ * (zero-restart model switching). `detail: { models, tiers, occurrenceId }` — key COUNTS only,
5564
+ * never the
5100
5565
  * catalog itself. In-flight tasks finish on the models they resolved at prepare (natural
5101
5566
  * snapshot); every later prepare resolves against the new generation. A failed swap (illegal
5102
5567
  * tier binding) throws atomically and mints nothing.
5568
+ * `occurrenceId` (#495, uuidv7 minted at the announcement): the verb carries no run and no
5569
+ * session, and the payload is a pair of counts, so two swaps of the same shape were byte-
5570
+ * identical lines. Dedup key: (code, occurrenceId).
5103
5571
  * - `"route.fallback_to_primary"` (key↔URL pairing, `src/brain/route-adjudicator.ts`) — a
5104
5572
  * DERIVED-leg model (role/tier/system-default resolution, never a caller-explicit one) failed
5105
5573
  * the pairing pre-flight and the seat fell back to the primary model instead of sinking the
5106
5574
  * task; the notice is the loud half of that swap.
5107
- * `detail: { seat, from, to, cause, fixHint, sessionId? }` — `cause` is the refusal code
5575
+ * `detail: { seat, from, to, cause, fixHint, sessionId?, runId? }` — `cause` is the refusal code
5108
5576
  * (`route.credential_mismatch` / `route.credential_missing`); `sessionId` (#433, additive)
5109
- * rides when the adjudicating seat knows its session a correlation key only, the audience
5110
- * stays `"operator"` (see the registry's entitlement-not-routability note).
5577
+ * rides when the adjudicating seat knows its session, and `runId` (#499, additive) names the
5578
+ * INVOCATION whose seats were being resolved — both correlation keys only, the audience
5579
+ * stays `"operator"` (see the registry's entitlement-not-routability note). Two runs of one
5580
+ * session falling back on the same seat used to mint byte-identical lines; `runId` is what
5581
+ * tells one task falling back twice apart from two tasks falling back once each.
5111
5582
  * Explicitly-named models never mint this: they refuse at the brain's request gate instead.
5112
5583
  * - `"route.base_url_changed_key_unchanged"` (key↔URL pairing) — `Runner.swapModels` moved a
5113
5584
  * same-name entry's `baseUrl` while its Model-visible credential half (auth-bearing headers)
@@ -5115,8 +5586,11 @@ export interface EngineNotice {
5115
5586
  * PROVIDER changed, the credential reference needs the same update. Advisory only, never a
5116
5587
  * refusal; per-model-hook credentials are invisible to the catalog swap, so the notice is
5117
5588
  * deliberately conservative (it may fire when a hook-side credential DID change in lockstep).
5118
- * One aggregated notice per swap; `detail: { entries: [{ modelId, from, to }], total }`
5119
- * (rendered list bounded, total always exact).
5589
+ * One aggregated notice per swap; `detail: { entries: [{ modelId, from, to }], total,
5590
+ * occurrenceId }` (rendered list bounded, total always exact). `occurrenceId` (#495, uuidv7
5591
+ * minted at the announcement, independent of the `config.models_swapped` line's own): the
5592
+ * bounded render makes two swaps moving the same first entries byte-identical.
5593
+ * Dedup key: (code, occurrenceId).
5120
5594
  * - `"config.read_face_deployment_clamped"` (#237) — a deployment-wide `readFace: "open"` is not
5121
5595
  * in force beside a read-only (verifier) mount: it clamps to "roots" without throwing
5122
5596
  * (stricter-wins; the clamp verdict stands, only its occurrence was undisclosed). Announced
@@ -5152,10 +5626,13 @@ export interface EngineNotice {
5152
5626
  * frame's two count keys). They are NOT redelivered (a steer aimed at a finished run must not
5153
5627
  * fire at the next one — unlike ENGINE notes, which pend per session); the loud half of the
5154
5628
  * #257 contract's "accepted = enqueued, not consumed" sentence;
5155
- * `detail: { steer, taskId?, sessionId? }` / `{ followUp, taskId?, sessionId? }` (`sessionId`
5156
- * = #433's routing half for these two `"user"`-audience rows see
5157
- * {@link undrainedUserInputNotices}; omitted when the caller has none, never fabricated).
5158
- * Per-run, at most once per family (the terminal sweep is a single site).
5629
+ * `detail: { steer, taskId?, sessionId?, runId? }` / `{ followUp, taskId?, sessionId?, runId? }`
5630
+ * (`sessionId` = #433's routing half for these two `"user"`-audience rows; `runId` = #499's
5631
+ * per-run identity, the key this account is actually about — see
5632
+ * {@link undrainedUserInputNotices}; all omitted when the caller has none, never fabricated).
5633
+ * Per-run, at most once per family (the terminal sweep is a single site) — which is exactly why
5634
+ * `taskId`/`sessionId` cannot key it: both answer the same value for two runs of one session,
5635
+ * so two runs each stranding one steer folded into one indistinguishable row.
5159
5636
  * **#389 (two corrections).** ① The family now fires on the INTERRUPT path too: `abort()` used
5160
5637
  * to empty both queues before agent_end could count them, so the one loss path an operator most
5161
5638
  * needs to hear about was the one path that stayed silent. ② On a DURABLE PARK the verdict is
@@ -5164,17 +5641,29 @@ export interface EngineNotice {
5164
5641
  * — what is announced is the remainder that could NOT be carried, so a fully-migrated park says
5165
5642
  * nothing at all and a queue-full / no-longer-pending row still says exactly what was lost.
5166
5643
  *
5167
- * - `"task.turn_interrupted"` (design/373) — a caller-provenance steer with `priority: "now"`
5168
- * ACTUALLY CUT the running turn: the in-flight provider stream / tool batch was aborted at a
5169
- * manufactured boundary (finished tool calls keep their real results, never-started ones
5170
- * settle as paired interrupted results, the interruption marker lands) and the run CONTINUES
5171
- * with the steer at the queue head. One notice per REAL cut (a `now` that found nothing in
5172
- * flight, or whose frame already rode the imminent boundary, announces nothing no false
5173
- * interrupt claims); every cut turn counts toward `maxTurns`, so an interrupt storm's cost is
5174
- * bounded and each of its cuts is on the record. `detail: { inputId, sessionId, actorId?,
5175
- * taskId? }` `inputId` is the steer's own correlation key (the `human_input` frame's id),
5176
- * `actorId` the caller's asserted identity when one rode the steer. Audience `"user"`: the
5177
- * person whose input forced the boundary is the one entitled to see that it landed.
5644
+ * - `"task.turn_interrupted"` (design/373) — a running turn was ACTUALLY CUT at a manufactured
5645
+ * boundary: the in-flight provider stream / tool batch was aborted (finished tool calls keep
5646
+ * their real results, never-started ones settle as paired interrupted results, the interruption
5647
+ * marker lands). **TWO mint lanes, one code** — the CUT is lane-independent, so it is one fact
5648
+ * with one spelling, and `detail.cause` is the arm discriminator (a consumer that must tell
5649
+ * them apart reads that key; one that only cares "a turn was cut" reads neither):
5650
+ * · the STEER-NOW lane (design/373) a caller-provenance steer with `priority: "now"` cut
5651
+ * the turn and the run CONTINUES with the steer at the queue head.
5652
+ * `detail: { inputId, sessionId, runId, actorId?, taskId? }`, NO `cause` key `inputId` is
5653
+ * the steer's own correlation key (the `human_input` frame's id) and `actorId` the caller's
5654
+ * asserted identity when one rode the steer.
5655
+ * · the BARE-HALT lane (#504, {@link TaskStream.halt}) — the person's Esc-form stop cut the
5656
+ * turn and the run does NOT continue: it collects to a clean, resumable
5657
+ * `status:"completed"` ending carrying {@link TaskResult.haltedByUser}.
5658
+ * `detail: { cause: "user_halt", sessionId, runId, taskId? }` — no `inputId` and no
5659
+ * `actorId`, because no text entered the model and the verb carries no caller identity. A
5660
+ * consumer keying `detail.inputId` off this row must treat it as ABSENT on this lane.
5661
+ * In both lanes: one notice per REAL cut (a `now` that found nothing in flight or whose frame
5662
+ * already rode the imminent boundary, and a halt with nothing in flight, announce nothing — no
5663
+ * false interrupt claims); `runId` (#499) is the INVOCATION that was cut (the other two ids are
5664
+ * shared by every run of the session); every cut turn counts toward `maxTurns`, so an interrupt
5665
+ * storm's cost is bounded and each of its cuts is on the record. Audience `"user"`: the person
5666
+ * who forced the boundary is the one entitled to see that it landed.
5178
5667
  * RETIRED here (BREAKING, named): `"task.injection_priority_unimplemented"` — the ladder is
5179
5668
  * implemented, so the unhonored-knob disclosure it carried has no referent; consumers must
5180
5669
  * judge ladder support by VERSION, never by that code's absence.
@@ -5187,8 +5676,10 @@ export interface EngineNotice {
5187
5676
  * same fact is the entry's own `human_input` account with `delivery: "blocked"` — the two share
5188
5677
  * the inputId, so "accepted at park, screened at redelivery" is auditable end to end. Audience
5189
5678
  * `"user"` (the person whose instruction was withheld is the one entitled to re-issue it);
5190
- * `detail: { inputId?, sessionId, taskId? }` — `inputId` is the parked entry's stored
5191
- * correlation key.
5679
+ * `detail: { inputId?, sessionId, runId, taskId? }` — `inputId` is the parked entry's stored
5680
+ * correlation key (OPTIONAL: a row parked without one exists), and `runId` (#499) names the
5681
+ * resume leg that withheld it, so a session resumed twice does not report two withholdings as
5682
+ * one.
5192
5683
  *
5193
5684
  * - `"memory.session_polluted"` (design/178 §3, #324a; message mode-aware since design/336) —
5194
5685
  * this session's memory crossed into the one-way externally-exposed state (a tool classified
@@ -5256,7 +5747,9 @@ export interface EngineNotice {
5256
5747
  * TASK LEG (a resume leg may announce again — a new leg's audit stream is a new fact;
5257
5748
  * deliberately no durable once-per-session state), never on the `"static-face"` default, and
5258
5749
  * never for a delivered `"external"` attestation (that mark still lands);
5259
- * `detail: { reason, subagentType?, sessionId? }` — `reason` is the same sentence the waived
5750
+ * `detail: { reason, subagentType?, sessionId?, runId }` — `runId` (#499) is what makes the
5751
+ * ONCE-PER-LEG guarantee above checkable, since a resume leg may legitimately announce again and
5752
+ * the session id cannot tell the two legs apart; `reason` is the same sentence the waived
5260
5753
  * mark would have carried, neutralized/length-bounded (tool and agent-type names are
5261
5754
  * host/model-controlled inputs).
5262
5755
  * - `"memory.content_class_declared"` (design/378) — an `McpServerSpec` entry carries an explicit
@@ -5271,7 +5764,10 @@ export interface EngineNotice {
5271
5764
  * condition as the classification it talks about — a leg with no engine-memory session and no
5272
5765
  * provenance recorder classifies nothing, so there is no posture to report and no line is
5273
5766
  * minted. `detail: { server,
5274
- * contentOrigin, toolCount, execIsExternalContent?, sessionId? }` — `server` is the
5767
+ * contentOrigin, toolCount, execIsExternalContent?, sessionId?, runId }` — `runId` (#499) names
5768
+ * the LEG, which is what "one line per declared entry per prepared task leg" is a claim about:
5769
+ * the session id beside it is shared by every leg of the session, so two legs reading one
5770
+ * declaration used to mint byte-identical rows. `server` is the
5275
5771
  * host-authored entry name, neutralized/length-bounded; `toolCount` is what this entry actually
5276
5772
  * mounted (0 for a server that failed to connect — the declaration still stands and is still
5277
5773
  * disclosed); `execIsExternalContent` rides the `"execution"` value only and reports the strict
@@ -5281,11 +5777,60 @@ export interface EngineNotice {
5281
5777
  * long after the dial, and D-10 already rules topology is not evidence of the lineage this line
5282
5778
  * audits — connection facts live on `MaterializedMcp.statuses` instead.
5283
5779
  *
5780
+ * - `"memory.capture_opted_out"` (design/383 §2.8) — memory capture is OFF for this session and
5781
+ * the switch provably TOOK: nothing the session produces will be committed to the long-term
5782
+ * store and its contributions no longer enter consolidation. The user must be able to SEE the
5783
+ * switch land rather than infer it from silence, which is why it is announced rather than left
5784
+ * implicit. States three facts and an exit, and deliberately overclaims none of them: entries
5785
+ * committed BEFORE the opt-out remain in the library and stay retrievable, entries already
5786
+ * folded into consolidation products leave those products unaffected, and deleting committed
5787
+ * content is a separate host action (the erase valve), never a side effect of this switch.
5788
+ * `detail: { sessionId, origin }` — `origin` is the closed set
5789
+ * `"declaration" | "flip-verb" | "standing-record" | "delegation-floor" | "fork-origin"`, and
5790
+ * the message is written per value (a delegated child's inherited floor and a fork's inherited
5791
+ * one-way record say so, rather than claiming the session declared it). Audience `"user"`.
5792
+ * Dedup unit: once per SESSION — key `(code, detail.sessionId)`, so the standing-record resume
5793
+ * arm folds into the standing entry instead of double-announcing.
5794
+ * - `"memory.capture_optout_unpersisted"` (design/383 §2.8) — the OPPOSITE receipt, and a
5795
+ * separate code on purpose ("it is off for you" and "it could not be turned off" must never
5796
+ * share a spelling): the opt-out could NOT be durably recorded (the control-plane write
5797
+ * failed), so it is REFUSED rather than honored in-process under a switch that would silently
5798
+ * stop holding at the next resume. Minted only beside that refusal — the throw carrying the
5799
+ * same code — never beside a success, and no success notice is minted on this arm.
5800
+ * `detail: { sessionId }`. Audience `"user"` (the operator repairs the store, but the
5801
+ * entitlement to the fact belongs to whoever asked for the privacy). **TWO ingresses, and they
5802
+ * settle differently — a consumer must not read one as the other:**
5803
+ * · PREPARE-TIME (a declared/inherited opt-out, before the run starts) — the refusal is
5804
+ * TERMINAL: the task never runs and nothing is captured. Dedup unit: once per task, since
5805
+ * the refusal it rides settles the task.
5806
+ * · the LIVE FLIP VERB ({@link TaskStream.optOutMemoryCapture} mid-run) — the refusal is
5807
+ * SCOPED TO THAT CALL: it rejects the verb's promise and nothing else. The run keeps going
5808
+ * and capture is still ENABLED (the switch did not take), which is exactly why the notice
5809
+ * is owed. Because no record was retained, a retry can fail the same way, so this ingress
5810
+ * can announce ONCE PER ATTEMPT — a consumer de-duplicating per task would hide later
5811
+ * failures of a switch the person is still trying to throw.
5812
+ * Read the run's own outcome (or the verb's rejection) to tell the two apart; this code alone
5813
+ * does not say whether anything stopped.
5284
5814
  * - `"memory.consolidation_incomplete"` (design/376, LLM consolidation driver) — a driver run
5285
5815
  * settled without reaching the fixpoint: `detail` names the stop reason (the closed
5286
5816
  * `ConsolidationRunStopReason` set), cycles done, and the residue (write-failure or
5287
5817
  * fuse-refused groups by name). Advisory: committed cycles stand (add-only, never rolled
5288
5818
  * back); the recovery verb is re-running the host driver, which resumes the same pending run.
5819
+ * - `"memory.consolidation_withheld"` (design/383 §1.4) — the exposure partition engaged on a
5820
+ * consolidation run, so the CLEAN arm's authoring prompts were composed WITHOUT the marked
5821
+ * rows: anything this run's clean products say was written without them, and their silence on a
5822
+ * topic therefore does not mean the library is silent on it (the withheld rows still stand and
5823
+ * are still readable). TWO counts, deliberately separate sentences and separate keys —
5824
+ * `withheld` rows were kept out of the clean arm's prompts, and `neverSent` of those were not
5825
+ * sent to the consolidation model AT ALL (a lone marked row has no group to fold, so it is
5826
+ * withheld from every call). "Withheld from the clean arm" and "never left this machine" are
5827
+ * different facts about a person's own data and must not be merged into one comforting number,
5828
+ * which is why `sentToProvider` is carried as its own member rather than left to subtraction.
5829
+ * `detail: { scope, runId, withheld, neverSent, sentToProvider }` — `scope` is
5830
+ * neutralized/length-bounded. Audience `"user"`: the ONE consolidation row that is a reader's
5831
+ * fact rather than an operator's (the other five report what a host-driven maintenance run DID;
5832
+ * this one reports what the products a person will READ were written without). Minted once per
5833
+ * run, beside the receipt — the dedup unit.
5289
5834
  * - `"memory.consolidation_driver_superseded"` is an ERROR code, not a notice (named here only
5290
5835
  * to keep the family's spellings in one place): a concurrent driver invocation took over this
5291
5836
  * scope's run row (attempt fencing) and the losing worker's call THROWS with this `code` —
@@ -5323,7 +5868,10 @@ export interface EngineNotice {
5323
5868
  * - `"memory.consolidation_refused"` (design/339 §6.2) — the notice dialect of a consolidation
5324
5869
  * verb's coded structured refusal (the verb itself throws
5325
5870
  * {@link import("../core/memory-engine/consolidation.js").ConsolidationRefusedError});
5326
- * `detail: { refusalCode, scope? }`.
5871
+ * `detail: { refusalCode, scope?, occurrenceId? }`. `occurrenceId` (#495) is the THROWN ERROR's
5872
+ * own id, minted once where the refusal happens and read off it by the notice factory — one
5873
+ * throw rendered twice keeps one id, two refusals sharing a code/scope/message get two. Omitted
5874
+ * (never fabricated) when the caller composes the line with no error in hand.
5327
5875
  *
5328
5876
  * Deliberately NOT a notice family: brain retry/reconnect liveness (a rate limit, a 5xx, a
5329
5877
  * transient network failure being retried). Those are per-attempt liveness frames with their own
@@ -5381,11 +5929,17 @@ export declare function deliverEngineNotice(onNotice: ((notice: EngineNotice) =>
5381
5929
  * — the one throat — lifts it to the typed top-level key and the two spellings cannot disagree.
5382
5930
  * Omitted when the caller has none (never fabricated: the message is what the run lost, and a made-up
5383
5931
  * routing key would deliver it to the wrong stream).
5932
+ *
5933
+ * `runId` (#499, additive and optional): THE key for this account. The two ids above name the task and
5934
+ * the conversation, and both answer the same value for two runs of one session whenever the host
5935
+ * supplies no task id — so two runs each stranding one steer produced two byte-identical rows, read as
5936
+ * one repeated disclosure about one run. The account is per-RUN by its own contract sentence ("at most
5937
+ * once per family, per run"), so it must carry the run's own id. Same omit-never-fabricate rule.
5384
5938
  */
5385
5939
  export declare function undrainedUserInputNotices(counts: {
5386
5940
  steer: number;
5387
5941
  followUp: number;
5388
- }, taskId?: string, sessionId?: string): EngineNotice[];
5942
+ }, taskId?: string, sessionId?: string, runId?: string): EngineNotice[];
5389
5943
  /**
5390
5944
  * design/380 O2 — the facts handed to a deployment's {@link RunnerDeps.resumePreflight}: the row's
5391
5945
  * own recorded identity + placement record, read off the persisted checkpoint (never off the resume
@@ -5610,6 +6164,48 @@ export interface RunnerDeps {
5610
6164
  * never truthiness).
5611
6165
  */
5612
6166
  memoryProvenance?: "off" | "carry";
6167
+ /**
6168
+ * design/383 §3.1 — the deployment's memory-capture POSTURE (the frame the per-principal
6169
+ * {@link RuntimeCaps.allowMemoryOptOut} verdict is read inside). Closed spellings, screened at
6170
+ * the config doors (`config.memory_capture_policy` — a garbage value refuses the prepare, never
6171
+ * folds to any posture):
6172
+ * - absent / `"open"` — an opt-out declaration takes effect at face value; only a resolver's
6173
+ * EXPLICIT `false` denies it (`memory.capture_optout_denied`, terminal). Resolver fault
6174
+ * (throw / non-boolean / absent-on-fault) ⇒ the opt-out STILL TAKES EFFECT, with a named
6175
+ * `onError({phase:"config"})` disclosure — the polarity-reversal arm: on this posture the
6176
+ * irreversible failure is capturing a session whose user said no, so the fail-safe faces the
6177
+ * other way from the caps family's deny-on-outage degrade. Under the ABSENT posture the first
6178
+ * opt-out actually honored additionally announces one operator line (once per process): the
6179
+ * switch is in use and no policy was ever configured.
6180
+ * - `"governed"` — the per-principal verdict is MANDATORY (the mixed-fleet compliance
6181
+ * deployment: some principals are retention-required). A verdict of `true` allows, `false`
6182
+ * denies; resolver fault / absent verdict ⇒ the task is REFUSED (fail-closed): this posture
6183
+ * is the deployment explicitly declaring the compliance side's irreversibility (a
6184
+ * retention-required user acquiring an opt-out during an outage window) the heavier one.
6185
+ * - `"capture-required"` — every opt-out declaration is refused (deployment-wide forced
6186
+ * retention, one line, resolver-independent).
6187
+ * The two fault polarities are BOTH deliberate and BOTH pinned; the cost table lives beside the
6188
+ * adjudicator. Authority vs record separation (§3.3): this posture governs whether a NEW
6189
+ * declaration (or a resumed run) may proceed — it never retroactively re-enables capture for a
6190
+ * session whose one-way opt-out record already stands (record wins for capture; under
6191
+ * `"governed"`/`"capture-required"` a standing record whose entitlement has been revoked refuses
6192
+ * to CONTINUE instead — zero capture and zero out-of-policy running, availability paid).
6193
+ */
6194
+ memoryCapturePolicy?: "open" | "governed" | "capture-required";
6195
+ /**
6196
+ * design/383 §2.1b — an optional host-supplied carrier for the session capture opt-out record
6197
+ * (per memory plane, keyed by that plane's control dir). Absent = the control-plane file trio
6198
+ * (single-process/single-host deployments: byte-identical behavior). A deployment whose control
6199
+ * plane does not survive into resume replicas (the stateless multi-replica server form) supplies
6200
+ * a durable store here; see {@link import("./memory-engine/layout.js").SessionCaptureRecordStore}
6201
+ * for the per-leg fail-closed contract. Also the §2.1b capability signal: on a REMOTE-execution
6202
+ * deployment with no store supplied here, a `capture:"off"` declaration is refused
6203
+ * (`config.memory_capture_unsupported`) rather than accepted onto a carrier that the next resume
6204
+ * cannot see — a false privacy promise is worse than a loud one.
6205
+ */
6206
+ memoryCaptureRecordStore?: (plane: {
6207
+ controlDir: string;
6208
+ }) => import("./memory-engine/layout.js").SessionCaptureRecordStore;
5613
6209
  /**
5614
6210
  * design/339 §6.1 — the v3 memory-consolidation switch, threaded verbatim to the engine seat
5615
6211
  * ({@link import("../core/memory-engine/engine.js").MemoryEngineOptions.consolidation} — same
@@ -5645,6 +6241,23 @@ export interface RunnerDeps {
5645
6241
  * author gets no channel to point the library's consolidation at a model of its choosing.
5646
6242
  * Absent alongside absent {@link memoryConsolidation} ⇒ nothing runs and nothing changes
5647
6243
  * (the v3 default posture).
6244
+ *
6245
+ * ⚠️ EGRESS — read this before configuring the seat (design/383 §1.5). Turning consolidation on
6246
+ * SENDS MEMORY CONTENT TO A MODEL PROVIDER. Specifically:
6247
+ * - WHAT: that cycle's ELIGIBLE CANDIDATES — not the whole library (entries already superseded,
6248
+ * of an ineligible type, or held out by a governance gate are not in the set) — each rendered
6249
+ * as its `name`, `description` and `body`. The bodies are the user's own notes, verbatim.
6250
+ * - WHERE: the provider this seat resolves to. When `chat` is absent that is whatever the
6251
+ * `consolidate → summarize` role chain lands on, which MAY BE THE SAME PROVIDER AS THE MAIN
6252
+ * MODEL and may equally be a different vendor — the resolution is the deployment's, and the
6253
+ * driver announces the model id and endpoint it actually used at the moment of the first call.
6254
+ * - MARKED CONTENT: entries carrying an external-origin (or repo-ingest `trust`) marker are
6255
+ * withheld from the CLEAN products' prompts and minted through a separate marked-only call.
6256
+ * "Withheld from the clean arm" is NOT "not sent" — such rows still go to the provider in
6257
+ * their own call, except a lone marked row, which is sent nowhere. The run receipt reports the
6258
+ * two counts separately (`withheldFromCleanArm` / `withheldNeverSent`) and so should any UI.
6259
+ * - IRREVERSIBILITY: bytes that have already crossed to a provider cannot be recalled by any
6260
+ * later configuration change. Disabling the seat stops FUTURE egress and nothing else.
5648
6261
  */
5649
6262
  memoryConsolidationDriver?: {
5650
6263
  chat?: {
@@ -5723,13 +6336,61 @@ export interface RunnerDeps {
5723
6336
  */
5724
6337
  resumePreflightTimeoutMs?: number;
5725
6338
  /**
5726
- * design/101 §E19 working-tree snapshot backend for {@link TaskSpec.rewindFiles}. A task with
5727
- * `rewindFiles` captures a snapshot per completed turn (keyed by the leaf `SessionTreeEntry.id`) and, when it
5728
- * also branches via {@link TaskSpec.resumeAt}, restores the snapshot for that entry. Omitted ⇒ rewind is OFF
5729
- * (no snapshot/restore work). The default reference impl is {@link InMemoryFileSnapshotStore}; a deployment
5730
- * supplies a durable / git / VM-snapshot backend behind the same seam (v1 restore is LOCAL-only).
5731
- */
5732
- fileSnapshotStore?: import("./file-snapshot-store.js").FileSnapshotStore;
6339
+ * design/381the per-edited-file rewind history backend (CC FileHistory topology; replaces the
6340
+ * E19 whole-tree `fileSnapshotStore`). WIRING IS THE OPT-IN (DV-9, the reference's own SDK
6341
+ * posture): with a store present, every Edit/Write/NotebookEdit first-touch is backed up before
6342
+ * the byte changes, every turn mints a history boundary keyed by its own initial session entry
6343
+ * ("file state when this turn began"), and a {@link TaskSpec.resumeAt}+{@link TaskSpec.restoreFiles}
6344
+ * (or {@link TaskSpec.rewindFilesTo}) run converges the tracked set back to a boundary. Omitted ⇒
6345
+ * rewind is OFF: zero track calls, zero boundary work, zero disclosure noise; a restore request
6346
+ * then fails loud (`rewind.store_unconfigured`). The reference impl is
6347
+ * {@link InMemoryFileHistoryStore}; a deployment supplies a durable backend behind the same seam
6348
+ * (backends MUST reuse the exported env-half helpers — see the contract JSDoc). Works against ANY
6349
+ * ExecutionEnv (local or remote): bytes are read/written through the env's FileSystem face and
6350
+ * stored host-side, so an env rebuild does not lose history.
6351
+ */
6352
+ fileHistoryStore?: import("./file-history-store.js").FileHistoryStore;
6353
+ /**
6354
+ * design/381 DV-14 (终审落锤) — what an Edit/Write/NotebookEdit does when its FIRST-TOUCH history
6355
+ * record cannot be persisted (`FileHistoryStore.trackEdit` answered `{ok:false}` = NO durable
6356
+ * first-touch state exists). Default **"refuse"**: the edit is refused with a typed tool error
6357
+ * naming the history-store failure — proceeding would leave the path indistinguishable from
6358
+ * never-touched, and the store's post-recovery retry would then mint already-modified bytes as
6359
+ * pristine v1 (the fake-pristine hole this default welds shut).
6360
+ *
6361
+ * `"proceed-unprotected"` is the availability concession arm: the edit proceeds and the failure is
6362
+ * disclosed via `onError` (phase "rewind"). ITS RESIDUAL, stated so the choice is informed: during
6363
+ * a store outage NO durable trace of the touch can be written, so "the pristine promise is void"
6364
+ * lives only in this process's memory — after a process restart the risk is exactly the default
6365
+ * arm's nightmare again (the store recovers, the next edit mints already-modified bytes as v1).
6366
+ * A deployment choosing this arm owns that window and is expected to record the disclosed events
6367
+ * in its own audit surface. Values outside the two-member set refuse prepare loudly.
6368
+ */
6369
+ onTrackFailure?: "refuse" | "proceed-unprotected";
6370
+ /**
6371
+ * design/381 §5.3① / §10 S-1 — the ADDITIVE discriminant for the rewind refusal that this engine
6372
+ * cannot decide on its own. When a restore finds no boundary for the requested entry, there are
6373
+ * two very different worlds: the boundary NEVER existed (those turns ran with no history store, or
6374
+ * it was reaped), or it exists in the RETIRED whole-tree snapshot epoch, which the per-edited-file
6375
+ * seat deliberately does not restore. The engine holds no whole-tree state, so absent this probe
6376
+ * the refusal honestly names BOTH possibilities rather than picking one — the default, and the
6377
+ * only honest wording for a deployment that never had a whole-tree epoch at all.
6378
+ *
6379
+ * A deployment that DOES hold the retired epoch (its assembly layer can ask the old table whether
6380
+ * that entry has a legacy row) supplies the bit here, and the refusal splits into the two precise
6381
+ * messages. `true` = a whole-tree-era boundary exists for that entry; `false` = it definitively
6382
+ * does not; `undefined` = the deployment cannot tell, which reads exactly like an absent probe.
6383
+ *
6384
+ * Consulted ONLY on the refusal path (never on a successful restore), so a slow probe costs
6385
+ * nothing in the ordinary case. It is a DIAGNOSTIC input, never a control input: a probe that
6386
+ * throws, hangs past the caller's own patience, or answers a non-boolean cannot change the outcome
6387
+ * — the refusal still happens, the wording falls back to the honest both-possibilities form, and
6388
+ * the bad value is disclosed through `onError` (phase "rewind") rather than silently absorbed.
6389
+ */
6390
+ legacyRewindBoundaryProbe?: (query: {
6391
+ sessionId: string;
6392
+ entryId: string;
6393
+ }) => boolean | undefined | Promise<boolean | undefined>;
5733
6394
  /**
5734
6395
  * design/99 §E6 — DURABLE per-session permission rules (a deny-narrowing folded into the task-time tool
5735
6396
  * policy). Omitted ⇒ the feature is OFF (no rules read, zero behavior change). A deployment supplies a
@@ -5775,6 +6436,17 @@ export interface RunnerDeps {
5775
6436
  * adopted principal's bucket forever.
5776
6437
  */
5777
6438
  localOwnerRules?: boolean;
6439
+ /**
6440
+ * design/382 §4.3 — the SESSION-RULE OVERLAY: where `{kind:"session"}` scoped allow rules live
6441
+ * (the session's own state, never the persisted store). Wired, the gate's persisted-rule lane
6442
+ * splices the CURRENT session's overlay rows in front of the store's `list()` on every
6443
+ * adjudication — session > project > global, the narrowest-first reporting order — and threads the
6444
+ * session's identity as the eligibility context's third axis; a host lands rows into it through
6445
+ * the consent protocol (`RuleConsentDeps.sessionRules`, a session-scope candidate's redemption).
6446
+ * Omitted ⇒ the session dimension does not exist at this gate and the lane is byte-identical.
6447
+ * See `permission-rule-session.ts` for the contract and the reference implementation.
6448
+ */
6449
+ sessionPermissionRules?: import("./permission-rule-session.js").SessionRuleOverlay;
5778
6450
  /**
5779
6451
  * design/182 §7 — the ORG rule overlay for an org-GOVERNED deployment. Constructed with
5780
6452
  * `createOrgRuleOverlay` (that constructor is the boot gate: a governed declaration with no snapshot
@@ -5909,6 +6581,37 @@ export interface RunnerDeps {
5909
6581
  consecutiveFailures: number;
5910
6582
  lastCause: string;
5911
6583
  }) => void;
6584
+ /**
6585
+ * #503 — OPT IN to recording this arming's serializable criteria (an
6586
+ * {@link import("./auto-mode-arming.js").AutoModeArmingRecipe}) on the constraint-chain entries a
6587
+ * durable park freezes, so a CROSS-PROCESS redemption can rebuild an equivalent classifier over its
6588
+ * own model leg ({@link import("./auto-mode-rebuild.js").rebuildAutoModeDecider}) instead of
6589
+ * answering `unavailable` forever and parking the same approval for a human on every redemption.
6590
+ *
6591
+ * DEFAULT OFF, and the default is the safety story: a row minted without it is byte-identical to
6592
+ * the pre-#503 shape, so a mixed-version fleet (a worker that predates this field resuming a row a
6593
+ * newer worker minted) keeps the digest it can compute. Turn it on fleet-wide, not per-worker.
6594
+ *
6595
+ * What gets written down: the rule overrides, the settings-deny rules, the session-context facts,
6596
+ * the window bounds and the two knobs — i.e. deployment-authored configuration, on a durable row.
6597
+ * `sessionContext` is the one field that routinely carries per-session facts (a user-identity
6598
+ * line); a deployment that treats those as sensitive at rest should keep them out of the arming
6599
+ * face rather than out of this flag.
6600
+ *
6601
+ * Recording is CONDITIONAL on the session breaker still being closed at park time: a session that
6602
+ * already fell back to non-auto hands nothing forward (the redemption must never run wider than the
6603
+ * ancestor it inherits). A `true` here with a non-serializable face announces once and records
6604
+ * nothing (`phase:"config"`).
6605
+ */
6606
+ persistArming?: boolean;
6607
+ /**
6608
+ * #503 — the deployment's auto-mode SETTINGS GENERATION (a config revision, a settings hash, a
6609
+ * publish id — the string is opaque to the engine). Recorded in the arming recipe and compared at
6610
+ * rebuild time against the REDEEMING deployment's own: it is a declaration axis that can only make
6611
+ * the rebuild stricter (see {@link import("./auto-mode-arming.js").foldAutoModeArming}). Absent =
6612
+ * undeclared, which the fold treats as "cannot assume unchanged".
6613
+ */
6614
+ settingsEpoch?: string;
5912
6615
  };
5913
6616
  /**
5914
6617
  * design/129-B — PROCESS-level observer for background delegation children (Agent/Fork
@@ -6340,8 +7043,11 @@ export interface RunnerDeps {
6340
7043
  * unrepaired transcript.
6341
7044
  * - `"suggestions"` — the best-effort follow-up-suggestions pass failed or timed out; the result
6342
7045
  * simply carries no suggestions.
6343
- * - `"rewind"` — the per-turn working-tree snapshot backing file rewind failed or was skipped (e.g. a
6344
- * too-large root inside its cooldown window); turns without a snapshot cannot be rewound to.
7046
+ * - `"rewind"` — the file-history machinery backing rewind reported a best-effort failure: a
7047
+ * turn-start boundary capture failed or timed out (that turn cannot be rewound to; the turn
7048
+ * itself proceeds), or a first-touch failure was tolerated under
7049
+ * `onTrackFailure:"proceed-unprotected"`. (design/381: the whole-tree size gate and its
7050
+ * `classification:"too_large"` payload are retired — per-edited-file history has no size axis.)
6345
7051
  * - `"hook"` — a deployment hook misbehaved: a callback threw, or returned a verdict it was not
6346
7052
  * allowed to (e.g. a declared-observational hook). The engine applies the hook's own documented
6347
7053
  * fallback (swallow, or fail-closed deny, per its contract) and reports the fact here.