@sema-agent/core 5.64.0 → 6.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 +70 -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 +113 -4
  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 +2 -1
  33. package/dist/core/governance-codes.js +14 -0
  34. package/dist/core/hooks.d.ts +48 -8
  35. package/dist/core/hooks.js +39 -22
  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 +6 -0
  45. package/dist/core/memory-engine/engine.d.ts +329 -15
  46. package/dist/core/memory-engine/engine.js +364 -34
  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 +80 -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 +20 -0
  65. package/dist/core/permission-rule-consent.js +12 -3
  66. package/dist/core/permission-rule-model.d.ts +67 -7
  67. package/dist/core/permission-rule-model.js +53 -7
  68. package/dist/core/permission-rule-store.js +15 -10
  69. package/dist/core/permission-rule-sync.js +15 -11
  70. package/dist/core/remote-env.d.ts +3 -3
  71. package/dist/core/retention-policy.d.ts +9 -0
  72. package/dist/core/retention-policy.js +5 -2
  73. package/dist/core/retention.d.ts +13 -2
  74. package/dist/core/runner/assemble-result.d.ts +19 -1
  75. package/dist/core/runner/assemble-result.js +17 -2
  76. package/dist/core/runner/compaction-call-options.d.ts +93 -0
  77. package/dist/core/runner/compaction-call-options.js +3 -0
  78. package/dist/core/runner/memory-capture-optout.d.ts +80 -0
  79. package/dist/core/runner/memory-capture-optout.js +53 -0
  80. package/dist/core/runner/prepare-config-doors.d.ts +5 -0
  81. package/dist/core/runner/prepare-config-doors.js +16 -0
  82. package/dist/core/runner/prepare-hands-readface.d.ts +119 -5
  83. package/dist/core/runner/prepare-hands-readface.js +103 -8
  84. package/dist/core/runner/prepare-memory.d.ts +88 -0
  85. package/dist/core/runner/prepare-memory.js +306 -25
  86. package/dist/core/runner/prepare-task.d.ts +156 -5
  87. package/dist/core/runner/prepare-task.js +488 -98
  88. package/dist/core/runner/runtask.d.ts +27 -20
  89. package/dist/core/runner/runtask.js +283 -99
  90. package/dist/core/runner/session-file-state-replay.d.ts +18 -10
  91. package/dist/core/runner/session-file-state-replay.js +52 -1
  92. package/dist/core/runner/tool-disclosure.js +2 -1
  93. package/dist/core/runner/turn-attachments.d.ts +22 -12
  94. package/dist/core/session-store.d.ts +1 -1
  95. package/dist/core/session-store.js +6 -1
  96. package/dist/core/session.d.ts +34 -1
  97. package/dist/core/store-contracts/file-history-store-contract.d.ts +3 -0
  98. package/dist/core/store-contracts/file-history-store-contract.js +720 -0
  99. package/dist/core/stub-env.d.ts +4 -0
  100. package/dist/core/stub-env.js +1 -0
  101. package/dist/core/task-registry-shared.js +30 -2
  102. package/dist/core/tool-errors.js +1 -0
  103. package/dist/core/tool-policy.d.ts +172 -1
  104. package/dist/core/tool-policy.js +32 -1
  105. package/dist/core/tool-result-store.js +2 -1
  106. package/dist/core/trace.d.ts +24 -0
  107. package/dist/core/types.d.ts +875 -97
  108. package/dist/core/types.js +4 -3
  109. package/dist/core/untrusted-text.d.ts +1 -1
  110. package/dist/core/untrusted-text.js +8 -0
  111. package/dist/core/workflow-run-store-contract.js +17 -0
  112. package/dist/core/workflow-run-store.d.ts +20 -0
  113. package/dist/core/workflow-run-store.js +1 -0
  114. package/dist/engine/compaction/compaction.d.ts +88 -10
  115. package/dist/engine/compaction/compaction.js +109 -30
  116. package/dist/engine/execution-env/node-execution-env.d.ts +9 -1
  117. package/dist/engine/execution-env/node-execution-env.js +28 -0
  118. package/dist/engine/harness/agent-harness.d.ts +52 -1
  119. package/dist/engine/harness/agent-harness.js +36 -1
  120. package/dist/engine/harness/types.d.ts +44 -1
  121. package/dist/engine/llm/types.d.ts +50 -4
  122. package/dist/engine/loop/agent-loop.d.ts +5 -1
  123. package/dist/engine/loop/agent-loop.js +25 -0
  124. package/dist/engine/loop/types.d.ts +19 -0
  125. package/dist/engine/lsp/node-lsp-manager.d.ts +1 -1
  126. package/dist/engine/session/session.js +1 -1
  127. package/dist/index.d.ts +18 -8
  128. package/dist/index.js +14 -6
  129. package/dist/orchestration/run-workflow-tool.d.ts +20 -2
  130. package/dist/orchestration/run-workflow-tool.js +22 -3
  131. package/dist/orchestration/workflow-governance.d.ts +59 -1
  132. package/dist/orchestration/workflow-governance.js +61 -8
  133. package/dist/orchestration/workflow-meta.d.ts +4 -2
  134. package/dist/orchestration/workflow-primitives.js +56 -13
  135. package/dist/orchestration/workflow-types.d.ts +112 -1
  136. package/dist/orchestration/workflow-types.js +2 -2
  137. package/dist/orchestration/workflow.d.ts +20 -0
  138. package/dist/orchestration/workflow.js +182 -14
  139. package/dist/prompt-assembly/event-registry.js +1 -1
  140. package/dist/prompts/default.d.ts +15 -7
  141. package/dist/prompts/default.js +3 -0
  142. package/dist/stores/file/file-history-store.d.ts +368 -0
  143. package/dist/stores/file/file-history-store.js +1248 -0
  144. package/dist/stores/file/index.d.ts +22 -13
  145. package/dist/stores/file/index.js +4 -4
  146. package/dist/stores/file/permission-rule-store.js +1 -0
  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 +159 -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
@@ -12,6 +12,7 @@ export declare const GOVERNANCE_CODES: {
12
12
  readonly "config.compliance_required": "transient";
13
13
  readonly "config.compliance_denied": "terminal";
14
14
  readonly "config.locked_key": "terminal";
15
+ readonly "memory.capture_optout_denied": "terminal";
15
16
  };
16
17
  export type GovernanceCode = keyof typeof GOVERNANCE_CODES;
17
18
  /**
@@ -93,7 +94,7 @@ export type NoticeAudience = "user" | "operator";
93
94
  * src/ for notice mint shapes and names any code that is minted but unregistered, or registered but
94
95
  * no longer minted.
95
96
  */
96
- export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "delegation.transcript_integrity", "mcp.revocation_probe_failed", "workflow.governance_key_stripped", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "route.fallback_to_primary", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "tool_result.offload_put_failed"];
97
+ export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "delegation.transcript_integrity", "mcp.revocation_probe_failed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "route.fallback_to_primary", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "tool_result.offload_put_failed"];
97
98
  /** A code this engine mints (see {@link ENGINE_NOTICE_CODES}). NOT the type of
98
99
  * `EngineNotice.code`, which stays `string` — a host forwarding its own notices through the same
99
100
  * sink is a supported shape, and narrowing that field would break it. */
@@ -5,6 +5,7 @@ export const GOVERNANCE_CODES = {
5
5
  "config.compliance_required": "transient",
6
6
  "config.compliance_denied": "terminal",
7
7
  "config.locked_key": "terminal",
8
+ "memory.capture_optout_denied": "terminal",
8
9
  };
9
10
  export const NON_GOVERNANCE_MEMORY_CODES = new Set([
10
11
  "memory.secret_detected",
@@ -27,6 +28,7 @@ export const NON_GOVERNANCE_MEMORY_CODES = new Set([
27
28
  "memory.consolidation_incomplete",
28
29
  "memory.consolidation_conflict",
29
30
  "memory.consolidation_refused",
31
+ "memory.consolidation_withheld",
30
32
  "memory.consolidation_disabled",
31
33
  "memory.consolidation_gated",
32
34
  "memory.consolidation_busy",
@@ -68,6 +70,10 @@ export const NON_GOVERNANCE_MEMORY_CODES = new Set([
68
70
  "memory.origin_clear_pending",
69
71
  "memory.origin_clear_conflict",
70
72
  "memory.origin_clear_failed",
73
+ "memory.capture_opted_out",
74
+ "memory.capture_optout_unpersisted",
75
+ "memory.capture_optout_unavailable",
76
+ "memory.capture_optout_sweep_failed",
71
77
  ]);
72
78
  export function governanceRetryClass(code) {
73
79
  if (Object.prototype.hasOwnProperty.call(GOVERNANCE_CODES, code)) {
@@ -97,6 +103,7 @@ export const ENGINE_NOTICE_CODES = [
97
103
  "delegation.transcript_integrity",
98
104
  "mcp.revocation_probe_failed",
99
105
  "workflow.governance_key_stripped",
106
+ "workflow.agent_option_ignored",
100
107
  "memory.session_polluted",
101
108
  "memory.harvest_quarantined",
102
109
  "memory.delegation_static_mark_waived",
@@ -109,12 +116,15 @@ export const ENGINE_NOTICE_CODES = [
109
116
  "memory.consolidation_conflict",
110
117
  "memory.consolidation_incomplete",
111
118
  "memory.consolidation_refused",
119
+ "memory.consolidation_withheld",
112
120
  "route.fallback_to_primary",
113
121
  "route.base_url_changed_key_unchanged",
114
122
  "task.user_steer_undrained",
115
123
  "task.user_followup_undrained",
116
124
  "steering.parked_input_blocked",
117
125
  "task.turn_interrupted",
126
+ "memory.capture_opted_out",
127
+ "memory.capture_optout_unpersisted",
118
128
  "tool_result.offload_put_failed",
119
129
  ];
120
130
  const NOTICE_AUDIENCE_TABLE = {
@@ -128,6 +138,9 @@ const NOTICE_AUDIENCE_TABLE = {
128
138
  "task.user_followup_undrained": "user",
129
139
  "task.turn_interrupted": "user",
130
140
  "steering.parked_input_blocked": "user",
141
+ "memory.capture_opted_out": "user",
142
+ "memory.capture_optout_unpersisted": "user",
143
+ "memory.consolidation_withheld": "user",
131
144
  "config.autocompact_window_clamped": "operator",
132
145
  "config.env_timeout_discarded": "operator",
133
146
  "config.materialize_env_discarded": "operator",
@@ -139,6 +152,7 @@ const NOTICE_AUDIENCE_TABLE = {
139
152
  "delegation.transcript_integrity": "operator",
140
153
  "mcp.revocation_probe_failed": "operator",
141
154
  "workflow.governance_key_stripped": "operator",
155
+ "workflow.agent_option_ignored": "operator",
142
156
  "memory.content_class_declared": "operator",
143
157
  "memory.consolidation_recommended": "operator",
144
158
  "memory.consolidation_committed": "operator",
@@ -278,6 +278,10 @@ export interface Hooks {
278
278
  * answer in a second says so with `timeoutMs`.
279
279
  */
280
280
  export declare const DEFAULT_HOOK_TIMEOUT_MS = 600000;
281
+ /** `setTimeout`'s own ceiling: above it the platform silently clamps to ~1ms, which would turn a
282
+ * deployment's "very long bound" into an instant expiry it never asked for. Refused loudly instead
283
+ * (same ceiling, same posture as the gate's `approvalTimeoutMs`). */
284
+ export declare const MAX_HOOK_TIMEOUT_MS = 2147483647;
281
285
  /**
282
286
  * How long an observation seat still gets when the task is cancelled `elapsed` ms into its wait — the
283
287
  * grace, capped by what is LEFT of the declared deadline.
@@ -303,7 +307,11 @@ export declare function postAbortGraceMs(remainingMs: number): number;
303
307
  */
304
308
  export declare function resolveHookTimeoutMs(supplied: number | undefined, report?: (err: unknown) => void, owner?: object): number;
305
309
  /** The ten {@link Hooks} callback seats, named so a bound wait can say WHICH one did not answer. */
306
- export type HookSeatName = "preToolUse" | "postToolUse" | "postToolUseFailure" | "postToolBatch" | "userPromptSubmit" | "stop" | "stopFailure" | "preCompact" | "postCompact" | "permissionDenied";
310
+ export type HookSeatName = "preToolUse" | "postToolUse" | "postToolUseFailure" | "postToolBatch" | "userPromptSubmit" | "stop" | "stopFailure" | "preCompact" | "postCompact" | "permissionDenied"
311
+ /** design/380 O2 — not a {@link Hooks} record seat: the `RunnerDeps.resumePreflight` deployment
312
+ * callback, run through the same bounded-wait home so the pre-CAS resume ladder cannot grow its
313
+ * own dialect of "a deployment callback gets a deadline". */
314
+ | "resumePreflight";
307
315
  /** What one bounded seat invocation produced: the seat's own value, or the fact that the wait ended
308
316
  * first. A seat that THREW is neither — the throw propagates out of {@link runHookSeat} unchanged, so
309
317
  * each site's existing crash arm keeps owning it. */
@@ -1281,7 +1289,14 @@ export interface ToolGateInput {
1281
1289
  * parameter here is positional and `probeReason` is also a `string`, so inserting this beside its
1282
1290
  * logical neighbour would silently re-bind three existing call sites' arguments with no type
1283
1291
  * error to catch it. */
1284
- matchedAskRule?: string) => Promise<ToolGateResult["suspend"] | ParkAttemptFailed | undefined>;
1292
+ matchedAskRule?: string,
1293
+ /** #502: the surviving ask's engine-stamped `probeMandated` — threaded for the SAME errand as
1294
+ * `askDecisionReason` and `matchedAskRule`: the park mint's rule-suggestion door must know that
1295
+ * this ask is one no persisted rule may clear, or the parked card offers a "stop asking me this"
1296
+ * rule the lane would then refuse to honor. APPENDED at the tail for the reason its neighbour
1297
+ * states — every parameter here is positional — and typed `boolean` so a mis-bind against the two
1298
+ * trailing `string` seats is a compile error rather than a silent re-binding. */
1299
+ probeMandated?: boolean) => Promise<ToolGateResult["suspend"] | ParkAttemptFailed | undefined>;
1285
1300
  /**
1286
1301
  * design/174 — route a policy `ask` on the reserved question tool to this run's CONTENT-ask channel
1287
1302
  * before it can become a park or a refusal. Called in the `ask` branch with the FINAL post-hook,
@@ -1313,9 +1328,15 @@ export interface ToolGateInput {
1313
1328
  irreversibility?: "never" | "maybe" | "always";
1314
1329
  /**
1315
1330
  * design/77 §4: the `"maybe"`-tier probe from the tool's spec (read at prepare-time, captured here — not a
1316
- * tool argument). Called ONLY when `irreversibility === "maybe"` && the surviving decision is `allow`,
1317
- * time-bounded by {@link approvalTimeoutMs}. Fail-closed: anything other than `{ reversible: true }`
1331
+ * tool argument). Called when `irreversibility === "maybe"` && the surviving decision is `allow` (the
1332
+ * TIGHTEN arm) and, since #502, when it is already `ask` (the MANDATE arm: the probe is the only
1333
+ * source of `mandated`, and an ask another layer raised needs it too; there the `reversible` verdict is
1334
+ * discarded, since a probe may never un-ask another layer's question). Both arms are time-bounded by
1335
+ * {@link approvalTimeoutMs} (30s default). Fail-closed: anything other than `{ reversible: true }`
1318
1336
  * (including a timeout or a throw) tightens to `ask`. A probe is never trusted to AUTO-ALLOW past the gate.
1337
+ * The invocation set is therefore WIDER than the tighten's: an ask-first policy calls this on every
1338
+ * `"maybe"`-tier call, and a throwing/hanging probe costs an `onError(phase:"hook")` and the wait there
1339
+ * too — see `ToolSpec.reversibilityProbe` for the deployment-facing statement of that cost.
1319
1340
  *
1320
1341
  * An optional `reason` on a TIGHTENING verdict names the cause. The gate neutralizes + caps it and
1321
1342
  * carries it onto both approval routes (`AskRequest.probeReason`, `RiskDescriptor.probeReason`) — see
@@ -1445,6 +1466,13 @@ export interface ToolGateInput {
1445
1466
  * tool's own egress/irreversibility marks are structural requirements, not classifier hesitation
1446
1467
  * ("allow rules silence the classifier's questions, never a mandated one"). When a rule matches
1447
1468
  * but cannot clear, the surviving ask discloses it (message + `persistedRuleShadowed`).
1469
+ * · #502: an ask the call's own reversibility probe declared STRUCTURAL (`probeMandated`) joins that
1470
+ * family. It is the one PER-CALL member of it, and the reason there has to be one: the built-in
1471
+ * shell probe raises it for a read outside the session's allowed directories, which is a boundary
1472
+ * the deployment declared but which arrives wearing the classify doctrine's own "maybe" tier —
1473
+ * the very tier the bullet below keeps as this lane's home turf. Without a per-call member, a
1474
+ * compound was cleared segment-wise by the conjunction arm and an out-of-root read plus a sender
1475
+ * ran on two ordinary standing rules with no question asked.
1448
1476
  * · #213: an ask carrying `matchedAskRule` (an explicit `ask` permission rule matched this call)
1449
1477
  * is never consumed — a person's standing "ask me each time" outranks the memory of one past
1450
1478
  * yes, across lanes exactly as the DSL's own deny > ask > allow order says within one. Same
@@ -1609,23 +1637,35 @@ timeoutMs?: number): ToolPolicy;
1609
1637
  * classifier's questions, never a mandated one, and this predicate is the mandated-family half of
1610
1638
  * that boundary (the real-approval/governance half rides the decision's own `requiresRealApproval`
1611
1639
  * bit, which the org layer stamps):
1612
- * · `egress` — the tool's own external-write mark, judged FIRST: it is the tool's declaration even
1640
+ * · `probeMandated` — this CALL's own reversibility probe declared its demotion STRUCTURAL
1641
+ * (`"probe_mandate"`), judged FIRST because it is the only per-call member here: the three below
1642
+ * are properties of the TOOL and are true of every call on the seat, so a mandate that is true of
1643
+ * this one call must not be shadowed by the tier that happens to carry it (#502: the built-in
1644
+ * shell probe raises it for a listed reader naming a path outside the session's roots — a boundary
1645
+ * the deployment declared, which is exactly what the classify tier alone cannot say);
1646
+ * · `egress` — the tool's own external-write mark, judged next: it is the tool's declaration even
1613
1647
  * when the coarse doctrine also installed a shell tier on the same seat;
1614
1648
  * · `shellGated` + tier `"always"` — the operator's per-call confirmation doctrine
1615
1649
  * (`"operator_always"`); the classify doctrine installs `"maybe"`, and THOSE asks stay the rule
1616
- * lane's home turf (`undefined`);
1650
+ * lane's home turf (`undefined`) — that is the don't-ask-again main case, and the per-call member
1651
+ * above is deliberately the ONLY thing that carves a mandate out of it;
1617
1652
  * · a tool's OWN `"always"`/`"maybe"` irreversibility tier without the doctrine (`"tool_marks"`).
1618
1653
  *
1619
1654
  * Two consumers, one derivation: the gate's silencing arm (a matching rule is disclosed as shadowed
1620
1655
  * instead of clearing the ask) and the runner's suggestion factory (a mandated ask offers no
1621
1656
  * "stop asking me this" option — a rule minted from it would never clear it). A drift between the
1622
- * two would let a card offer a rule the lane then refuses to honor.
1657
+ * two would let a card offer a rule the lane then refuses to honor. The per-call member reaches both
1658
+ * the same way every other per-call fact does: the gate stamps it on the surviving ask, the
1659
+ * synchronous sites read it off the decision they spread, and the park leg threads its own parameter.
1623
1660
  */
1624
1661
  export declare function persistedRuleMandateOf(marks: {
1625
1662
  egress?: boolean;
1626
1663
  shellGated?: boolean;
1627
1664
  irreversibility?: "never" | "maybe" | "always";
1628
- }): "operator_always" | "tool_marks" | undefined;
1665
+ /** #502: the surviving ask's engine-stamped `probeMandated` — see
1666
+ * {@link import("./types.js").ReversibilityVerdict.mandated}. */
1667
+ probeMandated?: boolean;
1668
+ }): "operator_always" | "tool_marks" | "probe_mandate" | undefined;
1629
1669
  /**
1630
1670
  * The design/37 **two-phase tool gate** — the single chokepoint that makes the load-bearing invariant
1631
1671
  * structural ("a hook's `allow` cannot bypass the policy's `deny`/`ask`"):
@@ -7,7 +7,7 @@ import { ASK_USER_QUESTION_TOOL_NAME } from "./ask-question.js";
7
7
  import { createSafeNotifier } from "./safe-notify.js";
8
8
  import { ORG_ADJUDICATION_TIMEOUT_MS, ORG_RULE_DECISION_REASON, ORG_UNAVAILABLE_DECISION_REASON, settleOrgVerdictWithin } from "./permission-rule-org.js";
9
9
  export const DEFAULT_HOOK_TIMEOUT_MS = 600_000;
10
- const MAX_HOOK_TIMEOUT_MS = 2_147_483_647;
10
+ export const MAX_HOOK_TIMEOUT_MS = 2_147_483_647;
11
11
  const POST_ABORT_GRACE_MS = 1_000;
12
12
  export function postAbortGraceMs(remainingMs) {
13
13
  return Math.max(0, Math.min(POST_ABORT_GRACE_MS, remainingMs));
@@ -411,15 +411,17 @@ function withProbeTimeout(p, ms, signal) {
411
411
  }
412
412
  const DEFAULT_PROBE_TIMEOUT_MS = 30_000;
413
413
  export function persistedRuleMandateOf(marks) {
414
- return marks.egress === true
415
- ? "tool_marks"
416
- : marks.shellGated === true
417
- ? marks.irreversibility === "always"
418
- ? "operator_always"
419
- : undefined
420
- : marks.irreversibility === "always" || marks.irreversibility === "maybe"
421
- ? "tool_marks"
422
- : undefined;
414
+ return marks.probeMandated === true
415
+ ? "probe_mandate"
416
+ : marks.egress === true
417
+ ? "tool_marks"
418
+ : marks.shellGated === true
419
+ ? marks.irreversibility === "always"
420
+ ? "operator_always"
421
+ : undefined
422
+ : marks.irreversibility === "always" || marks.irreversibility === "maybe"
423
+ ? "tool_marks"
424
+ : undefined;
423
425
  }
424
426
  export async function runToolGate(input) {
425
427
  const { event, preToolUse, adjudicate, resolveAsk, suspendAsk } = input;
@@ -543,10 +545,12 @@ export async function runToolGate(input) {
543
545
  };
544
546
  denySource = "safety";
545
547
  }
546
- if (decision.action === "allow" && (input.irreversibility === "always" || input.irreversibility === "maybe")) {
547
- let tighten = true;
548
+ const alreadyAsking = decision.action === "ask";
549
+ if ((decision.action === "allow" || alreadyAsking) && (input.irreversibility === "always" || input.irreversibility === "maybe")) {
550
+ let tighten = !alreadyAsking;
548
551
  let probeReason;
549
552
  let probeCause;
553
+ let probeMandated;
550
554
  if (input.irreversibility === "maybe" && input.reversibilityProbe) {
551
555
  let reversible = false;
552
556
  const probeArgs = policyRewrite !== undefined ? policyRewrite : currentInput;
@@ -570,6 +574,8 @@ export async function runToolGate(input) {
570
574
  if (typeof raw === "string" && raw.trim().length > 0)
571
575
  probeReason = inlineUntrusted(raw, PROBE_REASON_MAX);
572
576
  probeCause = normalizeProbeCause(verdict?.cause);
577
+ if (verdict?.mandated === true)
578
+ probeMandated = true;
573
579
  }
574
580
  }
575
581
  catch (err) {
@@ -577,7 +583,7 @@ export async function runToolGate(input) {
577
583
  if (input.abortSignal?.aborted !== true)
578
584
  traceHookCrash(input, err, notifier);
579
585
  }
580
- tighten = !reversible;
586
+ tighten = !alreadyAsking && !reversible;
581
587
  }
582
588
  if (tighten) {
583
589
  decision = {
@@ -586,9 +592,13 @@ export async function runToolGate(input) {
586
592
  decisionReason: "safety",
587
593
  ...(probeReason !== undefined ? { probeReason } : {}),
588
594
  ...(probeCause !== undefined ? { probeCause } : {}),
595
+ ...(probeMandated === true ? { probeMandated: true } : {}),
589
596
  };
590
597
  denySource = input.shellGated === true ? "shellGate" : "safety";
591
598
  }
599
+ else if (alreadyAsking && probeMandated === true && decision.action === "ask") {
600
+ decision = { ...decision, probeMandated: true };
601
+ }
592
602
  }
593
603
  const breakerKnownOpen = (d) => {
594
604
  try {
@@ -706,7 +716,12 @@ export async function runToolGate(input) {
706
716
  currentInput = policyRewrite;
707
717
  req.args = policyRewrite;
708
718
  }
709
- const persistedRuleMandate = persistedRuleMandateOf(input);
719
+ const persistedRuleMandate = persistedRuleMandateOf({
720
+ egress: input.egress,
721
+ shellGated: input.shellGated,
722
+ irreversibility: input.irreversibility,
723
+ probeMandated: decision.action === "ask" && decision.probeMandated === true,
724
+ });
710
725
  let personalEvidence = { dotsAbsent: input.persistedRules === undefined ? "not_wired" : "not_adjudicated" };
711
726
  let laneCoverage;
712
727
  if (input.persistedRules &&
@@ -764,11 +779,13 @@ export async function runToolGate(input) {
764
779
  else if (hitRules !== undefined) {
765
780
  personalEvidence = unionDotsOf(hitRules);
766
781
  const shownRule = disclosedRuleSet(hitRules);
767
- const mandateNoun = persistedRuleMandate === "operator_always"
768
- ? "this deployment mandates per-call confirmation for shell commands (shellGate: always)"
769
- : persistedRuleMandate !== undefined
770
- ? "this tool carries egress/irreversibility marks (a mandated confirmation a rule cannot clear)"
771
- : "an explicit ask rule matched this call (a person's ask-me-each-time outranks a standing allow rule)";
782
+ const mandateNoun = persistedRuleMandate === "probe_mandate"
783
+ ? "the reversibility check declared this call structurally gated (the built-in shell check raises this for a read outside the directories allowed for this session) — it is cleared by confirming this call, never by a standing allow rule"
784
+ : persistedRuleMandate === "operator_always"
785
+ ? "this deployment mandates per-call confirmation for shell commands (shellGate: always)"
786
+ : persistedRuleMandate !== undefined
787
+ ? "this tool carries egress/irreversibility marks (a mandated confirmation a rule cannot clear)"
788
+ : "an explicit ask rule matched this call (a person's ask-me-each-time outranks a standing allow rule)";
772
789
  decision = {
773
790
  ...decision,
774
791
  persistedRuleShadowed: shownRule,
@@ -841,7 +858,7 @@ export async function runToolGate(input) {
841
858
  ? { origin: orgAskOrigin !== undefined ? `org_${orgAskOrigin}` : "policy" }
842
859
  : undefined;
843
860
  if (suspendAsk && decision.action === "ask") {
844
- const suspended = await suspendAsk(req, currentInput, safety, undefined, realApprovalOf(decision), decision.action === "ask" ? decision.persistedRuleShadowed : undefined, decision.action === "ask" ? decision.decisionReason : undefined, decision.action === "ask" ? decision.probeReason : undefined, decision.action === "ask" ? decision.probeCause : undefined, decision.action === "ask" ? decision.segmentCoverage : undefined, decision.action === "ask" ? decision.matchedAskRule : undefined);
861
+ const suspended = await suspendAsk(req, currentInput, safety, undefined, realApprovalOf(decision), decision.action === "ask" ? decision.persistedRuleShadowed : undefined, decision.action === "ask" ? decision.decisionReason : undefined, decision.action === "ask" ? decision.probeReason : undefined, decision.action === "ask" ? decision.probeCause : undefined, decision.action === "ask" ? decision.segmentCoverage : undefined, decision.action === "ask" ? decision.matchedAskRule : undefined, decision.action === "ask" ? decision.probeMandated : undefined);
845
862
  if (suspended) {
846
863
  if ("parkFailed" in suspended)
847
864
  parkFailed = suspended.parkFailed;
@@ -860,7 +877,7 @@ export async function runToolGate(input) {
860
877
  req.args = outcome.presentedInput;
861
878
  }
862
879
  if (suspendAsk && outcome.parkDeclined && parkFailed === undefined) {
863
- const suspended = await suspendAsk(req, currentInput, safety, true, realApprovalOf(decision), decision.action === "ask" ? decision.persistedRuleShadowed : undefined, decision.action === "ask" ? decision.decisionReason : undefined, decision.action === "ask" ? decision.probeReason : undefined, decision.action === "ask" ? decision.probeCause : undefined, decision.action === "ask" ? decision.segmentCoverage : undefined, decision.action === "ask" ? decision.matchedAskRule : undefined);
880
+ const suspended = await suspendAsk(req, currentInput, safety, true, realApprovalOf(decision), decision.action === "ask" ? decision.persistedRuleShadowed : undefined, decision.action === "ask" ? decision.decisionReason : undefined, decision.action === "ask" ? decision.probeReason : undefined, decision.action === "ask" ? decision.probeCause : undefined, decision.action === "ask" ? decision.segmentCoverage : undefined, decision.action === "ask" ? decision.matchedAskRule : undefined, decision.action === "ask" ? decision.probeMandated : undefined);
864
881
  if (suspended) {
865
882
  if ("parkFailed" in suspended)
866
883
  parkFailed = suspended.parkFailed;
@@ -892,7 +909,7 @@ export async function runToolGate(input) {
892
909
  askDenyResolution = resolved.resolution;
893
910
  decision = resolved;
894
911
  if (resolved.action === "deny" && resolved.approverUnavailable === true && suspendAsk && parkFailed === undefined) {
895
- const suspended = await suspendAsk(req, currentInput, safety, true, realApprovalOf(askBeforeResolve), askBeforeResolve.action === "ask" ? askBeforeResolve.persistedRuleShadowed : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.decisionReason : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.probeReason : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.probeCause : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.segmentCoverage : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.matchedAskRule : undefined);
912
+ const suspended = await suspendAsk(req, currentInput, safety, true, realApprovalOf(askBeforeResolve), askBeforeResolve.action === "ask" ? askBeforeResolve.persistedRuleShadowed : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.decisionReason : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.probeReason : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.probeCause : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.segmentCoverage : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.matchedAskRule : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.probeMandated : undefined);
896
913
  if (suspended) {
897
914
  if ("parkFailed" in suspended)
898
915
  parkFailed = suspended.parkFailed;
@@ -55,7 +55,7 @@ export interface LspSymbolInfo {
55
55
  * `{kind:"none"}` keeps getting). The failure reasons exist because one literal "No results." used to cover
56
56
  * both real emptiness and every transport-level fault — a findReferences that TIMED OUT read as "no
57
57
  * references, safe to delete" (CC 2.1.220 keeps these apart: `Error performing ${operation}: ...` vs the
58
- * plain empty-result strings, pretty220.js ~474840).
58
+ * plain empty-result strings, (pretty220.js:474840, historical; not relocated in 250 — see cc-250 anchors)).
59
59
  */
60
60
  export type LspNoneReason = "unsupported_operation" | "server_error" | "timeout" | "cancelled" | "server_terminated" | "no_call_hierarchy_item"
61
61
  /** HRD-LSP-7: the QUERIED file could not be read for the didOpen/didChange re-sync (deleted between the
@@ -178,7 +178,7 @@ export interface LspTransport {
178
178
  /** Reads a workspace file's CURRENT text for the session's didOpen/didChange re-sync. TOC = local fs; TOB =
179
179
  * the sandbox ExecutionEnv. */
180
180
  export type LspReadText = (filePath: string, signal?: AbortSignal) => Promise<string>;
181
- /** CC 2.1.220's LSP file cap (10MB — `File too large for LSP analysis`, pretty220.js ~474745). Single-sourced
181
+ /** CC 2.1.220's LSP file cap (10MB — `File too large for LSP analysis`, (pretty220.js:474745, historical; not relocated in 250 — see cc-250 anchors)). Single-sourced
182
182
  * in core: the TOOL-layer check in {@link createLspTool} (the model-visible enforcement, CC-parity) and the
183
183
  * defense-in-depth throw in the engine's `defaultLspReadText` must agree on one number. */
184
184
  export declare const MAX_LSP_FILE_BYTES: number;
@@ -1,7 +1,7 @@
1
1
  /**
2
- * MCP client integration. Protocol revision `2025-11-25`, supplied by `@modelcontextprotocol/sdk` 1.29.x
3
- * (core pins the SDK and does not negotiate the revision itself). Client side only — core never implements
4
- * an MCP server.
2
+ * MCP client integration. Protocol revision `2025-11-25`, supplied by `@modelcontextprotocol/sdk` 1.30.0
3
+ * (core pins the SDK EXACTLY and does not negotiate the revision itself). Client side only — core never
4
+ * implements an MCP server.
5
5
  *
6
6
  * ── DEPRECATION-IMMUNITY REGISTRY (MCP revision `2026-07-28`) ────────────────────────────────────────
7
7
  * The `2026-07-28` revision deprecates four surfaces this module has NEVER implemented. That is not an
@@ -25,6 +25,28 @@
25
25
  * reopening one is a LATER protocol revision that revives the surface, or a concrete consumer requirement
26
26
  * recorded under its own ticket — not the absence of the feature.
27
27
  *
28
+ * THE `2026-07-28` DELIVERY FORM, and why it does not narrow the four (design/384 §1, §2-J). That revision
29
+ * does not carry `roots/list`, `sampling/createMessage` or `elicitation/create` as standalone JSON-RPC
30
+ * methods at all. Its request-method table (as a 2026-era client dispatches it — CC 2.1.250's bundled era
31
+ * descriptor) is exactly: `tools/call`, `tools/list`, `prompts/get`, `prompts/list`, `resources/list`,
32
+ * `resources/templates/list`, `resources/read`, `completion/complete`, `server/discover`,
33
+ * `subscriptions/listen`. The three arrive instead through a DIFFERENT door — as embedded `inputRequests`
34
+ * inside an `input_required` result of `tools/call` / `prompts/get` / `resources/read`, answered by
35
+ * RETRYING the original request with `inputResponses`.
36
+ *
37
+ * That is a change of DELIVERY FORM ONLY, and it buys no exemption. Items 1 and 2 are reasoned about what
38
+ * the capability MEANS, not about how it is framed on the wire: an embedded `roots/list` enumerates the
39
+ * very root list item 1 refuses to host, and an embedded `sampling/createMessage` spends the very model
40
+ * channel and budget item 2 refuses to lend. So embedded roots/sampling are banned ON THE SAME TERMS and
41
+ * must clear the SAME reopening bar — a new door is not a reason, and "the registry only named the legacy
42
+ * method" is not a loophole. What this paragraph exists to prevent is the OPPOSITE misreading: items 1-4
43
+ * were written against the `2025-11-25` surface and are not themselves a ruling on the 2026 mechanics.
44
+ * `elicitation/create` — which core DOES implement (opt-in, form mode) and which is NOT one of the four —
45
+ * comes through that same door, so an era connection would have to decide how it is driven; that is
46
+ * design/384 P3's question, not this registry's. Nothing here is reachable today: core speaks `2025-11-25`,
47
+ * where all three are standalone methods, and core's `initialize` declares neither `roots` nor `sampling`
48
+ * (pinned).
49
+ *
28
50
  * Two more revision-relevant absences, same reasoning, different cause:
29
51
  * - `resources/subscribe` / `resources/unsubscribe` — the revision REMOVES the methods. Tool-list freshness
30
52
  * here is served by the explicit `RefreshMcpTools` tool over the existing connection, which is also the
@@ -47,7 +69,7 @@ import { type ReminderDisclosureCounts } from "./reminder-disclosure.js";
47
69
  *
48
70
  * Trust note (design F): a server's `annotations` are SERVER-controlled and "not guaranteed faithful" (MCP
49
71
  * spec), so they may only ever TIGHTEN — `destructiveHint` adds an `ask`/suspend, `openWorldHint` adds an
50
- * egress tighten — never LOWER. A server's `readOnlyHint` does NOT lower `effect` (that would let a hostile
72
+ * egress tighten — never LOWER. A server's `readOnlyHint` does NOT lower `effect` (that would let an adversarial
51
73
  * server lie on a mutating tool to escape repeat-safety + path-confinement). The ONLY trusted way to LOWER an
52
74
  * MCP tool below the fail-closed `write` default is the caller's {@link McpServerSpec.toolAxes} override
53
75
  * (caller = trust root). The synthetic first-party resource tools are the one exception — core sets their
@@ -278,7 +300,7 @@ export interface McpRefreshResult {
278
300
  }
279
301
  /** design/99 §E9 — projected per-server MCP status (see {@link MaterializedMcp.statuses}). NOTE: `serverInfo`
280
302
  * and `error` are SERVER-controlled strings (verbatim from the remote) — UNTRUSTED; a consumer rendering them
281
- * into a TUI/log must treat them as such (a hostile server could embed ANSI/break-out sequences). `toolNames`
303
+ * into a TUI/log must treat them as such (a misbehaving server could embed ANSI/break-out sequences). `toolNames`
282
304
  * are namespaced with a core-controlled `mcp__<server>__` prefix (design/108 ①). */
283
305
  export interface McpServerStatus {
284
306
  name: string;
@@ -577,7 +599,7 @@ export declare function normalizeMcpToolSchema(schema: unknown): McpSchemaNormal
577
599
  * and not a technicality: a schema with no object root is unusable everywhere, whereas this charset is
578
600
  * ONE provider's rule, and this engine is bring-your-own-model. Dropping here would delete a working
579
601
  * tool from a deployment whose provider accepts the name — a Chinese- or Japanese-named parameter is
580
- * the ordinary case, not a hostile one. Detection was the gap; removal was never the mandate.
602
+ * the ordinary case, not an adversarial one. Detection was the gap; removal was never the mandate.
581
603
  *
582
604
  * TOP-LEVEL only, like CC: a nested property name is not what the provider validates.
583
605
  */
@@ -703,7 +725,7 @@ export declare function walkMcpListPages<T>(fetchPage: (cursor: string | undefin
703
725
  * `structuredContent` from "a JSON object" to ANY JSON value (the same revision that relaxes
704
726
  * `inputSchema`/`outputSchema` to the full JSON Schema 2020-12 keyword set).
705
727
  *
706
- * SDK 1.29's `CallToolResultSchema` still types the field as a record, so a spec-legal array / string /
728
+ * SDK 1.30.0's `CallToolResultSchema` still types the field as a record, so a spec-legal array / string /
707
729
  * number / boolean / null fails validation for the WHOLE result — and the failure takes the `content`
708
730
  * blocks down with it: a server returning perfectly usable text plus a scalar `structuredContent`
709
731
  * produced nothing but `Invalid tools/call result: expected record, received number`. That is a silent
@@ -74,6 +74,12 @@ export interface ConsolidationDriverRunRow {
74
74
  /** design/376-C1 A-5 disclosure — marked candidates withheld from the clean arm's prompts at
75
75
  * mint time (present ⇔ the partition engaged over a marked library). */
76
76
  withheldFromCleanArm?: number;
77
+ /** design/383 §1.5 ③ — of {@link withheldFromCleanArm}, how many rows were withheld from EVERY
78
+ * call, i.e. never sent to the provider at all (a lone marked row has no group to fold, so no
79
+ * arm renders it). Recorded at mint time rather than derived from the roster later: the roster
80
+ * dies with a discarded cache, and "your data never left this machine" must not become
81
+ * unanswerable because an unrelated guard fired. */
82
+ withheldNeverSent?: number;
77
83
  /** The archived mint transcript's file name under {@link CONSOLIDATION_DRIVER_PLANS_DIR}. */
78
84
  planArchive?: string;
79
85
  /** COMMITTED cycles only (the store moved): a refused/fuse-blocked/zero-delta cycle row does
@@ -199,6 +205,11 @@ export interface ConsolidationRunReceipt {
199
205
  * product's silence about some fact key should know the current value may sit in a withheld
200
206
  * (marked, handle-faced) row. */
201
207
  withheldFromCleanArm?: number;
208
+ /** design/383 §1.5 ③ — of {@link withheldFromCleanArm}, the rows sent to NO call at all (never
209
+ * left this process). "Withheld from the clean arm" and "never sent to the provider" are
210
+ * different facts about a user's data; the receipt reports them separately so a host's wording
211
+ * can too. */
212
+ withheldNeverSent?: number;
202
213
  planArchive?: string;
203
214
  /** Advisory notices minted for this run (the `_incomplete` disclosure when not converged). */
204
215
  notices: EngineNotice[];
@@ -3,8 +3,8 @@ import { uuidv7 } from "../../internal/harness.js";
3
3
  import { atomicWriteFileSync, ensureDirExists, lockedStrictUpdate, readStrictSidecar, enqueueMemoryAnnouncement, ControlPlaneCorruptError } from "./layout.js";
4
4
  import { inlineUntrusted } from "../untrusted-text.js";
5
5
  import { CONSOLIDATION_FORCE_THROTTLE_FLOOR_MS, screenConsolidationOptions, supersessionFuseCeiling, } from "./consolidation.js";
6
- import { memoryConsolidationIncompleteNotice } from "./engine.js";
7
- import { CONSOLIDATION_DRIVE_ABORT, CONSOLIDATION_SNAPSHOT_PARKED, MEMORY_DISTILLER_CONTRACT_V1, driveConsolidationToFixpoint, isAliasModelId, llmPlanDistiller, mintExposurePartitionedPlan, mintLlmConsolidationPlan, normalizedCandidateRev, } from "./distiller.js";
6
+ import { memoryConsolidationIncompleteNotice, memoryConsolidationWithheldNotice } from "./engine.js";
7
+ import { CONSOLIDATION_DRIVE_ABORT, CONSOLIDATION_SNAPSHOT_PARKED, MEMORY_DISTILLER_CONTRACT_V1, detectCleanArmVerbatimLeak, driveConsolidationToFixpoint, isAliasModelId, llmPlanDistiller, mintExposurePartitionedPlan, mintLlmConsolidationPlan, normalizedCandidateRev, } from "./distiller.js";
8
8
  export const CONSOLIDATION_DRIVER_RUNS_FILE = "distiller-runs.json";
9
9
  export const CONSOLIDATION_DRIVER_PLANS_DIR = "distiller-plans";
10
10
  function coerceRunsFile(raw) {
@@ -71,6 +71,13 @@ class DriverMintFailure extends Error {
71
71
  this.detail = detail;
72
72
  }
73
73
  }
74
+ class DriverPurityViolation extends Error {
75
+ detail;
76
+ constructor(detail) {
77
+ super(detail);
78
+ this.detail = detail;
79
+ }
80
+ }
74
81
  class MintInvalidated extends Error {
75
82
  detail;
76
83
  residue;
@@ -150,6 +157,40 @@ export async function runMemoryConsolidationDriver(engine, scope, opts) {
150
157
  announceFailures.push(err instanceof Error ? err.message : String(err));
151
158
  }
152
159
  };
160
+ const egressEndpoint = ((raw) => {
161
+ if (raw === undefined)
162
+ return " (host-supplied chat seat; no endpoint recorded)";
163
+ try {
164
+ const u = new URL(raw);
165
+ return ` at ${inlineUntrusted(u.origin, 120)}`;
166
+ }
167
+ catch {
168
+ return " at an endpoint whose value could not be parsed as a URL (not echoed here — it may carry credentials)";
169
+ }
170
+ })(opts.baseUrl);
171
+ const egressSeat = `${inlineUntrusted(opts.model, 80)}${egressEndpoint}`;
172
+ const announceMintEgress = (calls, offered, plan) => {
173
+ if (calls === 0) {
174
+ announce([
175
+ `memory consolidation driver run ${run.runId} for scope ${inlineUntrusted(scope, 80)} made NO model call: all ${offered} offered candidate(s) were withheld from every arm, so no memory content left this process.`,
176
+ ]);
177
+ return;
178
+ }
179
+ const armLine = (plan?.minting.arms ?? []).map((a) => `${a.arm} arm ${a.candidateCount} candidate(s) in ${a.calls} call(s)`).join(", ");
180
+ announce([
181
+ `memory consolidation driver run ${run.runId} for scope ${inlineUntrusted(scope, 80)} SENT memory content to the configured consolidation model: ${egressSeat} — ${armLine || `up to ${offered} candidate(s) over ${calls} call(s) (the mint failed before its per-arm split was recorded; an arm that never ran sent nothing)`}. Each candidate was rendered as its name, description and body.`,
182
+ ]);
183
+ const withheld = run.withheldFromCleanArm ?? 0;
184
+ if (withheld > 0) {
185
+ const neverSent = run.withheldNeverSent ?? 0;
186
+ const sent = Math.max(0, withheld - neverSent);
187
+ announce([
188
+ ` ${withheld} externally-marked entr${withheld === 1 ? "y was" : "ies were"} withheld from the clean products' prompts.` +
189
+ (sent > 0 ? ` ${sent} of those ${sent === 1 ? "was" : "were"} sent to the model in a separate marked-only call.` : "") +
190
+ (neverSent > 0 ? ` ${neverSent} ${neverSent === 1 ? "was" : "were"} not sent to the model at all.` : ""),
191
+ ]);
192
+ }
193
+ };
153
194
  const settle = (outcome, drive, stopDetail) => {
154
195
  run.state = "settled";
155
196
  run.outcome = outcome;
@@ -164,6 +205,11 @@ export async function runMemoryConsolidationDriver(engine, scope, opts) {
164
205
  notices.push(notice);
165
206
  announce([notice.message]);
166
207
  }
208
+ if ((run.withheldFromCleanArm ?? 0) > 0) {
209
+ const withheldNotice = memoryConsolidationWithheldNotice({ scope, runId: run.runId, withheld: run.withheldFromCleanArm, neverSent: run.withheldNeverSent ?? 0 });
210
+ notices.push(withheldNotice);
211
+ announce([withheldNotice.message]);
212
+ }
167
213
  return {
168
214
  runId: run.runId,
169
215
  scope,
@@ -183,6 +229,7 @@ export async function runMemoryConsolidationDriver(engine, scope, opts) {
183
229
  writeFailures: [...run.writeFailures],
184
230
  residue: run.residue ?? [],
185
231
  ...(run.withheldFromCleanArm !== undefined ? { withheldFromCleanArm: run.withheldFromCleanArm } : {}),
232
+ ...(run.withheldNeverSent !== undefined ? { withheldNeverSent: run.withheldNeverSent } : {}),
186
233
  ...(run.planArchive !== undefined ? { planArchive: run.planArchive } : {}),
187
234
  notices,
188
235
  ...(announceFailures.length > 0 ? { announceFailures: [...announceFailures] } : {}),
@@ -210,6 +257,9 @@ export async function runMemoryConsolidationDriver(engine, scope, opts) {
210
257
  catch (err) {
211
258
  if (codeOf(err) === "config.memory_consolidation_driver")
212
259
  throw err;
260
+ announce([
261
+ `memory consolidation driver run ${run.runId} for scope ${inlineUntrusted(scope, 80)} ATTEMPTED to send memory content to the configured consolidation model: ${egressSeat} — the seat threw before any answer was recorded, so whether any content reached the provider is NOT observable from here; up to ${candidates.length} candidate(s) were in flight, each rendered as its name, description and body. Treat this as a possible transmission.`,
262
+ ]);
213
263
  throw new DriverMintFailure(`the model seat failed during the mint: ${err instanceof Error ? err.message : String(err)}`);
214
264
  }
215
265
  const transcript = minted.ok ? minted.plan.minting.transcript : minted.calls;
@@ -219,9 +269,16 @@ export async function runMemoryConsolidationDriver(engine, scope, opts) {
219
269
  run.usage.completionTokens += c.usage?.completionTokens ?? 0;
220
270
  }
221
271
  if (!minted.ok) {
272
+ announceMintEgress(transcript.length, candidates.length);
222
273
  throw new DriverMintFailure(minted.reason);
223
274
  }
224
275
  const plan = minted.plan;
276
+ if (plan.withheldFromCleanArm !== undefined) {
277
+ run.withheldFromCleanArm = plan.withheldFromCleanArm;
278
+ const markedServed = (plan.servedCandidates ?? []).filter((r) => r.marked).length;
279
+ run.withheldNeverSent = Math.max(0, plan.withheldFromCleanArm - markedServed);
280
+ }
281
+ announceMintEgress(transcript.length, candidates.length, plan);
225
282
  run.usage.retries += plan.minting.parseRepairs.retries;
226
283
  run.usage.truncations += plan.minting.parseRepairs.lengthCapped.length;
227
284
  run.repairs = {
@@ -239,6 +296,14 @@ export async function runMemoryConsolidationDriver(engine, scope, opts) {
239
296
  if (ratio > mintRepairBudget) {
240
297
  throw new DriverMintFailure(`the mint needed structural repairs on ${dropped} of ${total} member claim(s) (${(ratio * 100).toFixed(1)}% > mintRepairBudget ${(mintRepairBudget * 100).toFixed(1)}%) — a plan repaired this much does not represent the model's intent, and committing it would fold entries the harness chose, not the model (hallucinated ${plan.minting.repairs.hallucinatedMembers}, duplicate ${plan.minting.repairs.duplicateMembers}, nonInteger ${plan.minting.repairs.nonIntegerMembers}; emptied groups ${plan.minting.repairs.emptyGroups}, oversize ${plan.minting.repairs.oversizeGroups.length})`);
241
298
  }
299
+ const purity = detectCleanArmVerbatimLeak(plan);
300
+ if (purity.findings.length > 0) {
301
+ run.planArchive = distillerPlanArchiveName(run.runId, run.attempt);
302
+ writeDriverRun(controlDir, run);
303
+ archiveDistillerPlan(controlDir, run.runId, plan, run.attempt);
304
+ const where = [...new Set(purity.findings.map((f) => `#${f.productIndex}.${f.field}`))].sort();
305
+ throw new DriverPurityViolation(`the exposure partition's clean arm is not clean: ${purity.findings.length} verbatim marked-side byte sequence(s) surfaced in ${new Set(purity.findings.map((f) => f.productIndex)).size} product(s) that would commit UNMARKED, across ${purity.scannedProducts} scanned product(s) against ${purity.markedUniqueShingles} marked-unique shingle(s). Locations (product index into the archived plan . field): ${where.slice(0, 8).join(", ")}${where.length > 8 ? ", …" : ""}. The whole mint is discarded with zero writes; NO model-authored bytes (neither the offending text nor the product keys) are reproduced here — re-run detectCleanArmVerbatimLeak over the archived plan ${run.planArchive} for the byte-level witness.`);
306
+ }
242
307
  run.planArchive = distillerPlanArchiveName(run.runId, run.attempt);
243
308
  run.planCache = {
244
309
  maxInputsPerProduct: plan.maxInputsPerProduct,
@@ -246,8 +311,6 @@ export async function runMemoryConsolidationDriver(engine, scope, opts) {
246
311
  ...(plan.mintExposure !== undefined ? { mintExposure: plan.mintExposure } : {}),
247
312
  ...(plan.servedCandidates !== undefined ? { servedCandidates: plan.servedCandidates } : {}),
248
313
  };
249
- if (plan.withheldFromCleanArm !== undefined)
250
- run.withheldFromCleanArm = plan.withheldFromCleanArm;
251
314
  writeDriverRun(controlDir, run);
252
315
  archiveDistillerPlan(controlDir, run.runId, plan, run.attempt);
253
316
  }
@@ -382,6 +445,10 @@ export async function runMemoryConsolidationDriver(engine, scope, opts) {
382
445
  announce([`memory consolidation driver run ${run.runId} for scope ${inlineUntrusted(scope, 80)} FAILED before any write: ${inlineUntrusted(err.detail, 300)}`]);
383
446
  return settle("driver_failed", undefined, err.detail);
384
447
  }
448
+ if (err instanceof DriverPurityViolation) {
449
+ announce([`memory consolidation driver run ${run.runId} for scope ${inlineUntrusted(scope, 80)} REFUSED on the clean-arm purity gate (no writes): ${inlineUntrusted(err.detail, 300)}`]);
450
+ return settle("purity_violation", undefined, err.detail);
451
+ }
385
452
  if (err instanceof MintInvalidated) {
386
453
  announce([`memory consolidation driver run ${run.runId} for scope ${inlineUntrusted(scope, 80)} MINT INVALIDATED: ${inlineUntrusted(err.detail, 300)}`]);
387
454
  if (run.residue === undefined && err.residue.length > 0)