@sema-agent/core 5.65.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 (159) hide show
  1. package/CHANGELOG.md +46 -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 +2 -1
  33. package/dist/core/governance-codes.js +14 -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 +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/retention-policy.d.ts +9 -0
  71. package/dist/core/retention-policy.js +5 -2
  72. package/dist/core/retention.d.ts +13 -2
  73. package/dist/core/runner/assemble-result.d.ts +19 -1
  74. package/dist/core/runner/assemble-result.js +17 -2
  75. package/dist/core/runner/compaction-call-options.d.ts +93 -0
  76. package/dist/core/runner/compaction-call-options.js +3 -0
  77. package/dist/core/runner/memory-capture-optout.d.ts +80 -0
  78. package/dist/core/runner/memory-capture-optout.js +53 -0
  79. package/dist/core/runner/prepare-config-doors.d.ts +5 -0
  80. package/dist/core/runner/prepare-config-doors.js +16 -0
  81. package/dist/core/runner/prepare-hands-readface.d.ts +110 -5
  82. package/dist/core/runner/prepare-hands-readface.js +99 -7
  83. package/dist/core/runner/prepare-memory.d.ts +88 -0
  84. package/dist/core/runner/prepare-memory.js +305 -24
  85. package/dist/core/runner/prepare-task.d.ts +141 -1
  86. package/dist/core/runner/prepare-task.js +443 -79
  87. package/dist/core/runner/runtask.d.ts +9 -20
  88. package/dist/core/runner/runtask.js +133 -96
  89. package/dist/core/runner/session-file-state-replay.d.ts +18 -10
  90. package/dist/core/runner/session-file-state-replay.js +52 -1
  91. package/dist/core/runner/tool-disclosure.js +2 -1
  92. package/dist/core/runner/turn-attachments.d.ts +22 -12
  93. package/dist/core/session-store.d.ts +1 -1
  94. package/dist/core/session-store.js +6 -1
  95. package/dist/core/session.d.ts +34 -1
  96. package/dist/core/store-contracts/file-history-store-contract.d.ts +3 -0
  97. package/dist/core/store-contracts/file-history-store-contract.js +720 -0
  98. package/dist/core/task-registry-shared.js +11 -1
  99. package/dist/core/tool-errors.js +1 -0
  100. package/dist/core/tool-policy.d.ts +172 -1
  101. package/dist/core/tool-policy.js +32 -1
  102. package/dist/core/tool-result-store.js +2 -1
  103. package/dist/core/trace.d.ts +24 -0
  104. package/dist/core/types.d.ts +784 -89
  105. package/dist/core/types.js +4 -3
  106. package/dist/core/untrusted-text.d.ts +1 -1
  107. package/dist/core/untrusted-text.js +8 -0
  108. package/dist/core/workflow-run-store-contract.js +6 -2
  109. package/dist/core/workflow-run-store.d.ts +4 -1
  110. package/dist/engine/compaction/compaction.d.ts +88 -10
  111. package/dist/engine/compaction/compaction.js +109 -30
  112. package/dist/engine/execution-env/node-execution-env.d.ts +9 -1
  113. package/dist/engine/execution-env/node-execution-env.js +28 -0
  114. package/dist/engine/harness/agent-harness.d.ts +52 -1
  115. package/dist/engine/harness/agent-harness.js +36 -1
  116. package/dist/engine/harness/types.d.ts +26 -1
  117. package/dist/engine/llm/types.d.ts +50 -4
  118. package/dist/engine/loop/agent-loop.d.ts +5 -1
  119. package/dist/engine/loop/agent-loop.js +25 -0
  120. package/dist/engine/loop/types.d.ts +19 -0
  121. package/dist/engine/lsp/node-lsp-manager.d.ts +1 -1
  122. package/dist/engine/session/session.js +1 -1
  123. package/dist/index.d.ts +18 -8
  124. package/dist/index.js +14 -6
  125. package/dist/orchestration/run-workflow-tool.d.ts +20 -2
  126. package/dist/orchestration/run-workflow-tool.js +22 -3
  127. package/dist/orchestration/workflow-governance.d.ts +59 -1
  128. package/dist/orchestration/workflow-governance.js +61 -8
  129. package/dist/orchestration/workflow-meta.d.ts +4 -2
  130. package/dist/orchestration/workflow-primitives.js +56 -13
  131. package/dist/orchestration/workflow-types.d.ts +78 -2
  132. package/dist/orchestration/workflow.d.ts +20 -0
  133. package/dist/orchestration/workflow.js +163 -14
  134. package/dist/prompt-assembly/event-registry.js +1 -1
  135. package/dist/prompts/default.d.ts +7 -7
  136. package/dist/stores/file/file-history-store.d.ts +368 -0
  137. package/dist/stores/file/file-history-store.js +1248 -0
  138. package/dist/stores/file/index.d.ts +22 -13
  139. package/dist/stores/file/index.js +4 -4
  140. package/dist/stores/file/permission-rule-store.js +1 -0
  141. package/dist/stores/file/strategy-store.d.ts +3 -3
  142. package/dist/tools/fs/bash-readonly-classifier.d.ts +87 -3
  143. package/dist/tools/fs/bash-readonly-classifier.js +106 -4
  144. package/dist/tools/fs/fs-bash.js +9 -5
  145. package/dist/tools/fs/fs-shared.d.ts +52 -1
  146. package/dist/tools/fs/fs-shared.js +14 -0
  147. package/dist/tools/fs/fs-write.d.ts +5 -5
  148. package/dist/tools/fs/fs-write.js +71 -14
  149. package/dist/tools/fs/index.d.ts +6 -1
  150. package/dist/tools/fs/index.js +1 -1
  151. package/dist/tools/web.js +2 -1
  152. package/package.json +5 -1
  153. package/test/export-surface.snapshot.json +155 -23
  154. package/dist/core/file-snapshot-store.d.ts +0 -165
  155. package/dist/core/file-snapshot-store.js +0 -259
  156. package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +0 -13
  157. package/dist/core/store-contracts/file-snapshot-store-contract.js +0 -134
  158. package/dist/stores/file/file-snapshot-store.d.ts +0 -58
  159. package/dist/stores/file/file-snapshot-store.js +0 -353
@@ -1,4 +1,5 @@
1
1
  import type { BeforeWriteHook, EffectiveMemoryScopes, RunnerDeps, TaskSpec, ToolSpec } from "../types.js";
2
+ import { type CaptureEntitlementInput, type MemoryCapturePosture } from "./memory-capture-optout.js";
2
3
  import type { Prepared } from "./prepare-task.js";
3
4
  export interface PrepareMemoryInput {
4
5
  spec: TaskSpec;
@@ -74,6 +75,85 @@ export interface PrepareMemoryInput {
74
75
  * top-level task; for a delegated child the caller passes the fail-closed empty set when the
75
76
  * chain carries no verdict (an older-shape chain must not read as "unconstrained").
76
77
  */
78
+ /**
79
+ * design/383 §3.1 — the SCREENED capture posture (prepare-config-doors; absent deps seat folded
80
+ * to `"open"`) plus whether the deployment DECLARED one at all (the once-per-process "switch in
81
+ * use, no policy configured" operator line fires only on the undeclared arm).
82
+ */
83
+ memoryCapturePolicy?: MemoryCapturePosture;
84
+ memoryCapturePolicyDeclared?: boolean;
85
+ /**
86
+ * design/383 §3.1 — the per-principal entitlement as resolved by prepare-task's ONE caps resolve
87
+ * (`value` = `runtimeCaps.allowMemoryOptOut` when boolean; `faulted` = the resolve threw or the
88
+ * member was non-boolean — per the member's F2 fault-transit contract a fault must never arrive
89
+ * as a coined explicit value). Consumed by the adjudicator on every ingress of this prepare.
90
+ */
91
+ captureEntitlement?: CaptureEntitlementInput;
92
+ /**
93
+ * design/383 §2.1b — the carrier-absence verdict, judged by prepare-task from the DECLARED
94
+ * deployment shape: a remote-execution deployment whose control plane does not follow the run
95
+ * (the per-run sandbox / stateless-replica form) with no
96
+ * {@link RunnerDeps.memoryCaptureRecordStore} supplied. TRUE + a `capture:"off"` declaration ⇒
97
+ * the declaration is refused (`config.memory_capture_unsupported`) — a promise the next resume
98
+ * replica cannot see is a false promise, and a loud refusal beats one.
99
+ */
100
+ captureCarrierUnsupported?: boolean;
101
+ /**
102
+ * design/383 §2.5 — the IMMEDIATE spawner's session id (trusted internals seat, filled by the
103
+ * delegation lanes; absent on top-level runs). The record-query leg of the delegation floor: a
104
+ * child's harvest legs also read the PARENT's opt-out record off the shared write-plane control
105
+ * store, so a parent whose opt-out landed AFTER this child spawned (the mid-run flip) still gets
106
+ * zero admission from in-flight children — the spec-carried floor covers only spawn-time state.
107
+ * Honest bounds (registered): the query sees the CHILD's write-plane store — a parent whose
108
+ * record lives on a different plane (a dual-root split where the two write different planes) is
109
+ * outside its reach, and a lane that fills no parent seat (TIER-3 durable revival) is covered
110
+ * only insofar as the child's OWN record (same sessionId across revival) or spec carriage
111
+ * survives that lane.
112
+ */
113
+ parentSessionId?: string;
114
+ /**
115
+ * design/383 §2.5 — the delegation FLOOR bit off the trusted internals chain
116
+ * (`internals.memoryCaptureFloor === true`): the spawning session was opted out at spawn time.
117
+ * A floored leg runs opted-out WITHOUT re-adjudication (the parent's granted opt-out tightens
118
+ * the tree; re-asking could only loosen) and mints its own one-way record (reason names the
119
+ * floor), so its grandchildren and its own resumes inherit mechanically.
120
+ */
121
+ captureFloor?: boolean;
122
+ /**
123
+ * design/383 §2.5 (rescan post-6.0.0-RC) — the floor seat's THIRD state off the same trusted
124
+ * chain: the spawning session's capture state was INDETERMINATE at spawn time (its record store
125
+ * faulted at the getter read). This leg resolves it against the live record query (see
126
+ * `armCaptureOptOut`): ancestor record found ⇒ floor; still faulting ⇒ this session runs
127
+ * indeterminate itself (mechanical restrictions, no record mint); readable-and-clean ⇒ clean.
128
+ * Mutually exclusive with `captureFloor` at the mint site (the spread keys on optedOut !== true).
129
+ */
130
+ captureFloorIndeterminate?: boolean;
131
+ /**
132
+ * design/383 §2.5 (rescan post-6.0.0-RC) — the HOST-SIDE FORK lane: the session this one was
133
+ * forked FROM (`SessionMetadata.forkedFrom`, read by prepare-task off the acquired session). A
134
+ * fork byte-copies the source's committed transcript, so the source's standing opt-out governs
135
+ * the copy: record found at prepare ⇒ the floor arm (mint, reason naming the fork, no
136
+ * re-adjudication — the source's granted opt-out tightens the lineage); source store faulting ⇒
137
+ * indeterminate (suppress, no mint); clean ⇒ ordinary run. Also seeded into the harvest
138
+ * record-query walk, so a source that flips WHILE the fork runs suppresses it live.
139
+ */
140
+ captureForkOrigin?: string;
141
+ /** design/383 §2.5 (rescan, codex adversarial round) — the fork-lineage read itself FAULTED
142
+ * (session metadata unreadable): whether this session forks an opted-out source is unknowable,
143
+ * so the capture arm runs INDETERMINATE (suppress fail-closed, disclose, mint nothing) — a
144
+ * metadata outage must not read as "no lineage" (读错不许伪装 clean). */
145
+ captureLineageFault?: boolean;
146
+ /** design/383 §2.5 (codex review adopted) — the SPAWNING session's write-plane control dir
147
+ * (trusted internals chain), so the record-query leg reaches a parent on a DIFFERENT memory
148
+ * plane (project-writing parent, personal-writing child). Absent ⇒ same-plane query only. */
149
+ parentCaptureQueryDir?: string;
150
+ /** design/383 §2.5 (codex round 3) — the full ancestor chain's capture coordinates off the
151
+ * trusted internals seat; the harvest closure walks every row (deduped with the parent seat),
152
+ * so a mid-run flip anywhere up the tree suppresses this in-flight descendant. */
153
+ captureAncestors?: ReadonlyArray<{
154
+ sessionId: string;
155
+ controlDir?: string;
156
+ }>;
77
157
  admissionCtx: {
78
158
  orgMemoryDenied: boolean;
79
159
  complianceDegraded: boolean;
@@ -96,6 +176,14 @@ export interface PrepareMemoryResult {
96
176
  * them (atomic pair) and runs the remaining conjuncts.
97
177
  */
98
178
  memoryTools?: ToolSpec[];
179
+ /**
180
+ * #493 单铸 — the recall-discipline segment EXACTLY as this phase appended it to
181
+ * {@link memoryBlock} (present iff {@link memoryTools} is). prepare-task's support-name
182
+ * retraction strips THIS string: the segment's composition now depends on the marked-presence
183
+ * signal only this phase holds, so a strip-site re-composition could mint a string that was
184
+ * never appended and leave the real one standing.
185
+ */
186
+ recallDisciplineSegment?: string;
99
187
  /**
100
188
  * RB-276 — engine-materialized files whose FULL disk text rides in {@link memoryBlock}, for the
101
189
  * caller to pre-mark READ through the parity-204 seeding lane (`prepare-task`'s `seedContextFiles`,
@@ -4,10 +4,13 @@ import { admitMemoryScopes } from "../memory-admission.js";
4
4
  import { adoptLegacyRepoDirs, canonicalize, deriveRepoControlPlaneDir, deriveProjectControlDir, deriveProjectMemoryDir, deriveRepoMemoryDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, isContainedIn, lookupProjectIdHint, recordProjectIdHint, resolveMemoryEngineRoot } from "../memory-engine/layout.js";
5
5
  import { classifyScopePlanes, derivePersonalControlDir, derivePersonalMemoryDir, mergeHarvestReports, mergeInjections, needsDualRoots, parsedProjectPlane } from "../memory-engine/dual-root.js";
6
6
  import { normalizeMemorySpec } from "../memory.js";
7
- import { MEMORY_ANNOUNCEMENT_READONLY_CODA, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_READONLY_NOTICE, MemoryEngine, entryFileHeadCarriesOrigin, memoryConsolidationRecommendedNotice, memoryHarvestQuarantinedNotice, memoryHoldNotices, memoryRecallDisciplineSegment, memorySessionPollutedNotice, pollutionContainmentCounts, } from "../memory-engine/engine.js";
7
+ import { MEMORY_ANNOUNCEMENT_CAPTURE_OPTOUT_CODA, MEMORY_ANNOUNCEMENT_READONLY_CODA, MEMORY_CAPTURE_INDETERMINATE_NOTICE, MEMORY_CAPTURE_OPTOUT_NOTICE, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_READONLY_NOTICE, MemoryEngine, entryFileHeadCarriesOrigin, memoryCaptureOptedOutNotice, memoryCaptureOptOutUnpersistedNotice, memoryConsolidationRecommendedNotice, memoryHarvestQuarantinedNotice, memoryHoldNotices, memoryRecallDisciplineSegment, memorySessionPollutedNotice, pollutionContainmentCounts, } from "../memory-engine/engine.js";
8
+ import { adjudicateCaptureOptOut, captureOptOutDeniedError, captureOptOutUnpersistedError } from "./memory-capture-optout.js";
9
+ import { readSessionCaptureOptOut } from "../memory-engine/layout.js";
8
10
  import { createMemoryEngineTools } from "../memory-engine/tools.js";
9
11
  import { assertScopeContractPlacement, parseScopeKey, resolveProjectId } from "../memory-engine/scope-contract.js";
10
12
  import { FileMemoryEngineBackend } from "../memory-engine/file-backend.js";
13
+ let undeclaredCapturePostureAnnounced = false;
11
14
  export async function prepareMemory(input) {
12
15
  const { spec, deps, sessionId, taskRootPath, memoryWriteGateRef, admissionCtx } = input;
13
16
  const memorySpec = normalizeMemorySpec(spec.memory);
@@ -41,8 +44,37 @@ export async function prepareMemory(input) {
41
44
  memorySpec.writeScope = outcome.writeScope;
42
45
  }
43
46
  const useMemoryEngine = Boolean(memorySpec && memorySpec.enabled && deps.memoryBackend);
47
+ const capturePosture = input.memoryCapturePolicy ?? "open";
48
+ const captureEntitlement = input.captureEntitlement ?? { value: undefined, faulted: false };
49
+ const captureDeclaredRaw = memorySpec?.capture === "off";
50
+ if (captureDeclaredRaw && memorySpec !== undefined && !memorySpec.enabled) {
51
+ deps.onError?.(new Error(`memory.capture:"off" appears beside memory.enabled:false — not a contradiction (memory is off entirely, so nothing is captured either way); the opt-out declaration is covered by enabled:false this run.`), { phase: "config", sessionId });
52
+ }
53
+ const captureDeclared = captureDeclaredRaw && useMemoryEngine;
54
+ const captureFloored = input.captureFloor === true && useMemoryEngine;
55
+ if (captureDeclared && !captureFloored) {
56
+ const verdict = adjudicateCaptureOptOut({ posture: capturePosture, entitlement: captureEntitlement, subject: "declaration" });
57
+ if (!verdict.allowed)
58
+ throw captureOptOutDeniedError(verdict.detail);
59
+ if (verdict.faultDisclosure !== undefined) {
60
+ deps.onError?.(new Error(verdict.faultDisclosure), { phase: "config", sessionId });
61
+ }
62
+ if (input.captureCarrierUnsupported === true) {
63
+ const e = new Error(`memory.capture:"off" cannot be honored on this deployment shape: the execution environment is remote/per-run ` +
64
+ `and no RunnerDeps.memoryCaptureRecordStore is supplied, so core cannot know the control plane survives to a ` +
65
+ `resume replica — a privacy promise that silently expires at the first resume is refused rather than made. A ` +
66
+ `deployment whose control plane IS durable (a single persistent host using a remote sandbox) opts back in by ` +
67
+ `supplying the store explicitly — fileSessionCaptureRecordStore(controlDir) is that one-line declaration.`);
68
+ e.code = "config.memory_capture_unsupported";
69
+ throw e;
70
+ }
71
+ }
72
+ let captureOptedOut = false;
73
+ let captureIndeterminate = false;
74
+ let captureLineageIndeterminate = false;
44
75
  let memoryEngineSession;
45
76
  let memoryTools;
77
+ let memoryMarkedEntriesServed = false;
46
78
  let effectiveMemoryScopes;
47
79
  const originOf = (scope) => scopeOriginsSnapshot !== undefined
48
80
  ? scopeOriginsSnapshot[scope] === "deployment"
@@ -132,15 +164,106 @@ export async function prepareMemory(input) {
132
164
  const pinnedCtl = b.controlPlaneRoot;
133
165
  return typeof pinnedCtl === "string" && pinnedCtl ? pinnedCtl : fallback;
134
166
  };
167
+ const captureStoreOpt = (b, fallback) => deps.memoryCaptureRecordStore !== undefined ? { captureRecordStore: deps.memoryCaptureRecordStore({ controlDir: effectiveControlDir(b, fallback) }) } : {};
135
168
  const choosePersonalBackend = () => typeof pinned === "string" && pinned ? new FileMemoryEngineBackend(personalMemoryDir, { controlDir: personalControlDir }) : backend;
136
169
  const createPersonalEngine = (personalBackend) => {
137
170
  return {
138
- engine: new MemoryEngine({ backend: personalBackend, memoryDir: personalMemoryDir, controlDir: personalControlDir, onIncident: onEngineIncident, provenance: input.memoryProvenance, ...(input.deps.memoryConsolidation !== undefined ? { consolidation: input.deps.memoryConsolidation } : {}) }),
171
+ engine: new MemoryEngine({ backend: personalBackend, memoryDir: personalMemoryDir, controlDir: personalControlDir, onIncident: onEngineIncident, provenance: input.memoryProvenance, ...captureStoreOpt(personalBackend, personalControlDir), ...(input.deps.memoryConsolidation !== undefined ? { consolidation: input.deps.memoryConsolidation } : {}) }),
139
172
  backend: personalBackend,
140
173
  };
141
174
  };
142
175
  const onEngineIncident = (err) => deps.onError?.(err, { phase: "memory", sessionId });
143
176
  const adoptionRestricted = input.memoryPersistenceDeclared === false;
177
+ const armCaptureOptOut = (eng) => {
178
+ const standingRead = eng.sessionCaptureOptOutOrFault(sessionId);
179
+ if (standingRead.fault) {
180
+ captureIndeterminate = true;
181
+ deps.onError?.(new Error(`the capture opt-out record store is unreadable at prepare — this session's capture state is indeterminate, and every memory commit boundary (read-side adoption included) suppresses fail-closed until the store answers.`), { phase: "memory", sessionId });
182
+ }
183
+ if (input.captureLineageFault === true) {
184
+ const firstIndeterminate = !captureIndeterminate;
185
+ captureIndeterminate = true;
186
+ captureLineageIndeterminate = true;
187
+ if (firstIndeterminate) {
188
+ deps.onError?.(new Error(`this session's fork-lineage metadata is unreadable — whether it continues an opted-out source session cannot be established, so its capture state is indeterminate: every memory commit boundary (read-side adoption included) suppresses fail-closed; no opt-out record is minted from an unreadable state.`), { phase: "memory", sessionId });
189
+ }
190
+ }
191
+ const standing = standingRead.record;
192
+ let floored = captureFloored;
193
+ if (!floored && !captureDeclared && standing === undefined && input.captureFloorIndeterminate === true) {
194
+ const rows = new Map();
195
+ for (const row of input.captureAncestors ?? [])
196
+ rows.set(row.sessionId, row.controlDir);
197
+ if (input.parentSessionId !== undefined && !rows.has(input.parentSessionId))
198
+ rows.set(input.parentSessionId, input.parentCaptureQueryDir);
199
+ let anyFault = standingRead.fault;
200
+ let found = false;
201
+ for (const [ancestorId, ancestorDir] of rows) {
202
+ const samePlane = eng.sessionCaptureOptOutOrFault(ancestorId);
203
+ if (samePlane.record !== undefined) {
204
+ found = true;
205
+ break;
206
+ }
207
+ if (samePlane.fault)
208
+ anyFault = true;
209
+ if (ancestorDir !== undefined && ancestorDir !== eng.controlPlaneDir) {
210
+ try {
211
+ const crossPlane = deps.memoryCaptureRecordStore !== undefined ? deps.memoryCaptureRecordStore({ controlDir: ancestorDir }).read(ancestorId) : readSessionCaptureOptOut(ancestorDir, ancestorId);
212
+ if (crossPlane !== undefined) {
213
+ found = true;
214
+ break;
215
+ }
216
+ }
217
+ catch {
218
+ anyFault = true;
219
+ }
220
+ }
221
+ }
222
+ if (found)
223
+ floored = true;
224
+ else if (anyFault) {
225
+ if (!captureIndeterminate) {
226
+ captureIndeterminate = true;
227
+ deps.onError?.(new Error(`the spawning session's capture opt-out state was INDETERMINATE at spawn (its record store faulted) and the live re-query still cannot answer — this delegated session inherits the suppression: every memory commit boundary (read-side adoption included) suppresses fail-closed until the store answers; no opt-out record is minted from an unreadable state.`), { phase: "memory", sessionId });
228
+ }
229
+ }
230
+ }
231
+ let forkInherited = false;
232
+ if (!floored && !captureDeclared && standing === undefined && input.captureForkOrigin !== undefined) {
233
+ const src = eng.sessionCaptureOptOutOrFault(input.captureForkOrigin);
234
+ if (src.record !== undefined)
235
+ forkInherited = true;
236
+ else if (src.fault && !captureIndeterminate) {
237
+ captureIndeterminate = true;
238
+ deps.onError?.(new Error(`this session is a FORK of session "${input.captureForkOrigin}" whose capture opt-out record store is unreadable — the fork's capture state is indeterminate (the copied transcript may be governed by a standing opt-out), so every memory commit boundary suppresses fail-closed until the store answers; no opt-out record is minted from an unreadable state.`), { phase: "memory", sessionId });
239
+ }
240
+ }
241
+ if (standing === undefined && !captureDeclared && !floored && !forkInherited)
242
+ return;
243
+ if (standing !== undefined && !captureDeclared && !floored && capturePosture !== "open") {
244
+ const verdict = adjudicateCaptureOptOut({ posture: capturePosture, entitlement: captureEntitlement, subject: "standing-record" });
245
+ if (!verdict.allowed)
246
+ throw captureOptOutDeniedError(verdict.detail);
247
+ }
248
+ if (standing === undefined) {
249
+ const outcome = eng.markSessionCaptureOptOut(sessionId, captureDeclared ? `declared on TaskSpec.memory.capture` : forkInherited ? `inherited capture opt-out from the forked source session` : `inherited capture opt-out floor from the spawning session`);
250
+ if (outcome === "unpersisted") {
251
+ deliverEngineNotice(deps.onNotice, memoryCaptureOptOutUnpersistedNotice({ sessionId, ingress: "declaration" }));
252
+ throw captureOptOutUnpersistedError(sessionId, "declaration");
253
+ }
254
+ if (outcome === "created")
255
+ deliverEngineNotice(deps.onNotice, memoryCaptureOptedOutNotice({ sessionId, origin: captureDeclared ? "declaration" : forkInherited ? "fork-origin" : "delegation-floor" }));
256
+ }
257
+ else if (!captureDeclared) {
258
+ deliverEngineNotice(deps.onNotice, memoryCaptureOptedOutNotice({ sessionId, origin: "standing-record" }));
259
+ }
260
+ captureOptedOut = true;
261
+ if (input.memoryCapturePolicyDeclared !== true && !undeclaredCapturePostureAnnounced) {
262
+ undeclaredCapturePostureAnnounced = true;
263
+ deps.onError?.(new Error(`a session memory-capture opt-out was honored under the DEFAULT capture posture (no RunnerDeps.memoryCapturePolicy configured) — ` +
264
+ `the switch is now in use; declare "open", "governed" or "capture-required" to make the deployment's stance explicit.`), { phase: "config", sessionId });
265
+ }
266
+ };
144
267
  const retrievalBackend = (b, planeRestricted) => planeRestricted
145
268
  ? (b.restrictedAdoptionView?.({ audit: false }) ??
146
269
  b.retrievalView?.() ??
@@ -164,9 +287,47 @@ export async function prepareMemory(input) {
164
287
  let injectFn;
165
288
  let harvestBoth;
166
289
  let toolPlanes;
167
- const admitNothingOpts = input.memoryPersistenceDeclared === false
168
- ? { admitNothing: { reason: "harvest admitted nothing: memory persistence is declared unavailable for this session (memoryPersistenceCapable: false)" } }
169
- : {};
290
+ const admitNothingOptsNow = () => {
291
+ if (input.memoryPersistenceDeclared === false) {
292
+ return { admitNothing: { reason: "harvest admitted nothing: memory persistence is declared unavailable for this session (memoryPersistenceCapable: false)" } };
293
+ }
294
+ if (captureLineageIndeterminate) {
295
+ return { admitNothing: { reason: `harvest admitted nothing: this session's fork-lineage metadata is unreadable — whether it continues an opted-out source cannot be established, so commits are suppressed fail-closed` } };
296
+ }
297
+ const own = captureOptedOut ? { record: { at: 0, reason: "in-run state" }, fault: false } : writeEngine.sessionCaptureOptOutOrFault(sessionId);
298
+ if (own.record !== undefined) {
299
+ return { admitNothing: { reason: `harvest admitted nothing: this session declared memory capture opt-out (memory.capture: "off")` } };
300
+ }
301
+ if (own.fault) {
302
+ return { admitNothing: { reason: `harvest admitted nothing: the capture opt-out record store is unreadable — the opt-out state is indeterminate and commits are suppressed fail-closed` } };
303
+ }
304
+ const ancestors = new Map();
305
+ for (const row of input.captureAncestors ?? [])
306
+ ancestors.set(row.sessionId, row.controlDir);
307
+ if (input.parentSessionId !== undefined && !ancestors.has(input.parentSessionId))
308
+ ancestors.set(input.parentSessionId, input.parentCaptureQueryDir);
309
+ if (input.captureForkOrigin !== undefined && !ancestors.has(input.captureForkOrigin))
310
+ ancestors.set(input.captureForkOrigin, undefined);
311
+ for (const [ancestorId, ancestorDir] of ancestors) {
312
+ const samePlane = writeEngine.sessionCaptureOptOutOrFault(ancestorId);
313
+ let crossPlane;
314
+ if (ancestorDir !== undefined && ancestorDir !== writeEngine.controlPlaneDir) {
315
+ try {
316
+ crossPlane = deps.memoryCaptureRecordStore !== undefined ? deps.memoryCaptureRecordStore({ controlDir: ancestorDir }).read(ancestorId) : readSessionCaptureOptOut(ancestorDir, ancestorId);
317
+ }
318
+ catch {
319
+ return { admitNothing: { reason: `harvest admitted nothing: a spawning session's capture record store is unreadable — the floor state is indeterminate and commits are suppressed fail-closed` } };
320
+ }
321
+ }
322
+ if (samePlane.record !== undefined || crossPlane !== undefined) {
323
+ return { admitNothing: { reason: `harvest admitted nothing: a spawning session declared memory capture opt-out — the opt-out is a delegation floor no descendant loosens`, cause: "capture_optout_inherited" } };
324
+ }
325
+ if (samePlane.fault) {
326
+ return { admitNothing: { reason: `harvest admitted nothing: the capture opt-out record store is unreadable while a spawning session's floor is in question — suppressed fail-closed` } };
327
+ }
328
+ }
329
+ return {};
330
+ };
170
331
  if (dual) {
171
332
  const personalBackendChosen = choosePersonalBackend();
172
333
  {
@@ -195,16 +356,19 @@ export async function prepareMemory(input) {
195
356
  controlDir: identityKey !== undefined ? deriveProjectControlDir(engineRoot, identityKey) : deriveRepoControlPlaneDir(engineRoot, repoRoot),
196
357
  onIncident: onEngineIncident,
197
358
  provenance: input.memoryProvenance,
359
+ ...captureStoreOpt(backend, identityKey !== undefined ? deriveProjectControlDir(engineRoot, identityKey) : deriveRepoControlPlaneDir(engineRoot, repoRoot)),
198
360
  ...(input.deps.memoryConsolidation !== undefined ? { consolidation: input.deps.memoryConsolidation } : {}),
199
361
  });
200
362
  const personal = createPersonalEngine(personalBackendChosen);
201
363
  const personalEngine = personal.engine;
202
364
  const p = planes;
203
- const projectHandle = await projectEngine.materialize(p.project, p.writePlane === "project" ? memorySpec.writeScope : null, { adoptionRestricted, sessionId });
365
+ armCaptureOptOut(p.writePlane === "personal" ? personalEngine : projectEngine);
366
+ const projectHandle = await projectEngine.materialize(p.project, p.writePlane === "project" ? memorySpec.writeScope : null, { adoptionRestricted: adoptionRestricted || captureOptedOut || captureIndeterminate, sessionId });
204
367
  materializedResidue.push(...planeScopes(p.project, p.writePlane === "project" ? memorySpec.writeScope : null));
205
- const personalHandle = await personalEngine.materialize(p.personal, p.writePlane === "personal" ? memorySpec.writeScope : null, { adoptionRestricted, sessionId });
368
+ const personalHandle = await personalEngine.materialize(p.personal, p.writePlane === "personal" ? memorySpec.writeScope : null, { adoptionRestricted: adoptionRestricted || captureOptedOut || captureIndeterminate, sessionId });
206
369
  materializedResidue.push(...planeScopes(p.personal, p.writePlane === "personal" ? memorySpec.writeScope : null));
207
370
  const writeIsPersonal = p.writePlane === "personal";
371
+ memoryMarkedEntriesServed = projectHandle.markedEntriesPresent === true || personalHandle.markedEntriesPresent === true;
208
372
  writeEngine = writeIsPersonal ? personalEngine : projectEngine;
209
373
  writeHandle = writeIsPersonal ? personalHandle : projectHandle;
210
374
  readOnlyEngine = writeIsPersonal ? projectEngine : personalEngine;
@@ -212,22 +376,24 @@ export async function prepareMemory(input) {
212
376
  injectFn = () => mergeInjections(projectEngine.inject(projectHandle, { writeToolMounted: input.writeToolsMounted, reminderMark: input.reminderMark }), personalEngine.inject(personalHandle, { writeToolMounted: input.writeToolsMounted, reminderMark: input.reminderMark }));
213
377
  toolPlanes = [
214
378
  {
215
- backend: retrievalBackend(backend, adoptionRestricted || p.writePlane !== "project"),
379
+ backend: retrievalBackend(backend, adoptionRestricted || captureOptedOut || captureIndeterminate || p.writePlane !== "project"),
216
380
  scopes: planeScopes(p.project, p.writePlane === "project" ? memorySpec.writeScope : null),
217
381
  recordRetrieved: (ids) => projectEngine.recordRetrieved(ids),
218
382
  challengeExclusions: () => projectEngine.readChallengeExclusions(),
383
+ enginePlaneProvenance: projectEngine.provenanceMode,
219
384
  },
220
385
  {
221
- backend: retrievalBackend(personal.backend, adoptionRestricted || p.writePlane !== "personal"),
386
+ backend: retrievalBackend(personal.backend, adoptionRestricted || captureOptedOut || captureIndeterminate || p.writePlane !== "personal"),
222
387
  scopes: planeScopes(p.personal, p.writePlane === "personal" ? memorySpec.writeScope : null),
223
388
  recordRetrieved: (ids) => personalEngine.recordRetrieved(ids),
224
389
  challengeExclusions: () => personalEngine.readChallengeExclusions(),
390
+ enginePlaneProvenance: personalEngine.provenanceMode,
225
391
  },
226
392
  ];
227
393
  harvestBoth = async () => {
228
394
  const writeFirst = writeIsPersonal ? [personalEngine, personalHandle] : [projectEngine, projectHandle];
229
395
  const readOther = writeIsPersonal ? [projectEngine, projectHandle] : [personalEngine, personalHandle];
230
- const writeReport = await writeFirst[0].harvest(writeFirst[1], { ...pollutedOpts(writeFirst[0]), sessionId, ...admitNothingOpts });
396
+ const writeReport = await writeFirst[0].harvest(writeFirst[1], { ...pollutedOpts(writeFirst[0]), sessionId, ...admitNothingOptsNow() });
231
397
  let readReport;
232
398
  let readFailure;
233
399
  try {
@@ -245,18 +411,21 @@ export async function prepareMemory(input) {
245
411
  else if (personalOnly) {
246
412
  const personal = createPersonalEngine(choosePersonalBackend());
247
413
  const personalEngine = personal.engine;
248
- const handle = await personalEngine.materialize(memorySpec.scopes, memorySpec.writeScope, { adoptionRestricted, sessionId });
414
+ armCaptureOptOut(personalEngine);
415
+ const handle = await personalEngine.materialize(memorySpec.scopes, memorySpec.writeScope, { adoptionRestricted: adoptionRestricted || captureOptedOut || captureIndeterminate, sessionId });
249
416
  materializedResidue.push(...planeScopes(memorySpec.scopes, memorySpec.writeScope));
417
+ memoryMarkedEntriesServed = handle.markedEntriesPresent === true;
250
418
  writeEngine = personalEngine;
251
419
  writeHandle = handle;
252
420
  injectFn = () => personalEngine.inject(handle, { writeToolMounted: input.writeToolsMounted, reminderMark: input.reminderMark });
253
- harvestBoth = () => personalEngine.harvest(handle, { ...pollutedOpts(personalEngine), sessionId, ...admitNothingOpts });
421
+ harvestBoth = () => personalEngine.harvest(handle, { ...pollutedOpts(personalEngine), sessionId, ...admitNothingOptsNow() });
254
422
  toolPlanes = [
255
423
  {
256
- backend: retrievalBackend(personal.backend, adoptionRestricted || memorySpec.writeScope === null),
424
+ backend: retrievalBackend(personal.backend, adoptionRestricted || captureOptedOut || captureIndeterminate || memorySpec.writeScope === null),
257
425
  scopes: planeScopes(memorySpec.scopes, memorySpec.writeScope),
258
426
  recordRetrieved: (ids) => personalEngine.recordRetrieved(ids),
259
427
  challengeExclusions: () => personalEngine.readChallengeExclusions(),
428
+ enginePlaneProvenance: personalEngine.provenanceMode,
260
429
  },
261
430
  ];
262
431
  }
@@ -267,20 +436,24 @@ export async function prepareMemory(input) {
267
436
  controlDir: identityKey !== undefined ? deriveProjectControlDir(engineRoot, identityKey) : deriveRepoControlPlaneDir(engineRoot, repoRoot),
268
437
  onIncident: onEngineIncident,
269
438
  provenance: input.memoryProvenance,
439
+ ...captureStoreOpt(backend, identityKey !== undefined ? deriveProjectControlDir(engineRoot, identityKey) : deriveRepoControlPlaneDir(engineRoot, repoRoot)),
270
440
  ...(input.deps.memoryConsolidation !== undefined ? { consolidation: input.deps.memoryConsolidation } : {}),
271
441
  });
272
- const handle = await engine.materialize(memorySpec.scopes, memorySpec.writeScope, { adoptionRestricted, sessionId });
442
+ armCaptureOptOut(engine);
443
+ const handle = await engine.materialize(memorySpec.scopes, memorySpec.writeScope, { adoptionRestricted: adoptionRestricted || captureOptedOut || captureIndeterminate, sessionId });
273
444
  materializedResidue.push(...planeScopes(memorySpec.scopes, memorySpec.writeScope));
445
+ memoryMarkedEntriesServed = handle.markedEntriesPresent === true;
274
446
  writeEngine = engine;
275
447
  writeHandle = handle;
276
448
  injectFn = () => engine.inject(handle, { writeToolMounted: input.writeToolsMounted, reminderMark: input.reminderMark });
277
- harvestBoth = () => engine.harvest(handle, { ...pollutedOpts(engine), sessionId, ...admitNothingOpts });
449
+ harvestBoth = () => engine.harvest(handle, { ...pollutedOpts(engine), sessionId, ...admitNothingOptsNow() });
278
450
  toolPlanes = [
279
451
  {
280
- backend: retrievalBackend(backend, adoptionRestricted || memorySpec.writeScope === null),
452
+ backend: retrievalBackend(backend, adoptionRestricted || captureOptedOut || captureIndeterminate || memorySpec.writeScope === null),
281
453
  scopes: planeScopes(memorySpec.scopes, memorySpec.writeScope),
282
454
  recordRetrieved: (ids) => engine.recordRetrieved(ids),
283
455
  challengeExclusions: () => engine.readChallengeExclusions(),
456
+ enginePlaneProvenance: engine.provenanceMode,
284
457
  },
285
458
  ];
286
459
  }
@@ -301,6 +474,22 @@ export async function prepareMemory(input) {
301
474
  };
302
475
  }
303
476
  }
477
+ {
478
+ const root = writeHandle.writableRoot;
479
+ if (w.key === root || w.key.startsWith(`${root}${sep}`)) {
480
+ const state = captureOptedOut ? { record: { at: 0, reason: "in-run state" }, fault: false } : writeEngine.sessionCaptureOptOutOrFault(sessionId);
481
+ if (state.record !== undefined || state.fault) {
482
+ return {
483
+ ok: false,
484
+ code: "read_only_layering",
485
+ reason: state.record !== undefined
486
+ ? `memory capture is off for this session at the user's request (memory.capture: "off") — writes into the memory store are refused. Nothing was written.`
487
+ : `the capture opt-out record store is unreadable, so this session's capture state is indeterminate — writes into the memory store are refused fail-closed. Nothing was written.`,
488
+ muted: false,
489
+ };
490
+ }
491
+ }
492
+ }
304
493
  return writeEngine.gateWrite(writeHandle, w.key, w.content);
305
494
  };
306
495
  let pollutionAnnounced = false;
@@ -326,7 +515,8 @@ export async function prepareMemory(input) {
326
515
  try {
327
516
  const report = await harvestBoth();
328
517
  if (!report.ok && report.incident) {
329
- deps.onError?.(new Error(`memory harvest refused (${report.incident.kind}): ${report.incident.detail}`), { phase: "memory", sessionId });
518
+ const named = report.reportId !== undefined ? `${report.incident.kind}, report ${report.reportId}` : report.incident.kind;
519
+ deps.onError?.(new Error(`memory harvest refused (${named}): ${report.incident.detail}`), { phase: "memory", sessionId });
330
520
  }
331
521
  announceHarvestContainment(report);
332
522
  try {
@@ -345,11 +535,76 @@ export async function prepareMemory(input) {
345
535
  return undefined;
346
536
  }
347
537
  };
538
+ const resolveCaptureEntitlementFresh = async () => {
539
+ if (deps.runtimeCapsResolver === undefined)
540
+ return { value: undefined, faulted: false };
541
+ try {
542
+ const caps = (await deps.runtimeCapsResolver(spec.principal)) ?? undefined;
543
+ const v = caps?.allowMemoryOptOut;
544
+ if (v !== undefined && typeof v !== "boolean")
545
+ return { value: undefined, faulted: true };
546
+ return { value: v, faulted: false };
547
+ }
548
+ catch {
549
+ return { value: undefined, faulted: true };
550
+ }
551
+ };
348
552
  memoryEngineSession = {
349
553
  engine: writeEngine,
350
554
  handle: writeHandle,
351
555
  inject: injectFn,
352
556
  harvest: harvestSafe,
557
+ captureOptOut: {
558
+ optedOut: () => captureOptedOut || writeEngine.sessionCaptureOptOutOrFault(sessionId).record !== undefined,
559
+ indeterminate: () => {
560
+ if (captureOptedOut)
561
+ return false;
562
+ const s = writeEngine.sessionCaptureOptOutOrFault(sessionId);
563
+ return s.record === undefined && (captureIndeterminate || s.fault);
564
+ },
565
+ flip: async (reason) => {
566
+ const entitlement = await resolveCaptureEntitlementFresh();
567
+ const verdict = adjudicateCaptureOptOut({ posture: capturePosture, entitlement, subject: "declaration" });
568
+ if (!verdict.allowed)
569
+ throw captureOptOutDeniedError(verdict.detail);
570
+ if (verdict.faultDisclosure !== undefined)
571
+ deps.onError?.(new Error(verdict.faultDisclosure), { phase: "config", sessionId });
572
+ if (input.captureCarrierUnsupported === true) {
573
+ const e = new Error(`memory capture opt-out cannot be honored on this deployment shape: the execution environment is remote/per-run ` +
574
+ `and no RunnerDeps.memoryCaptureRecordStore is supplied — the record would not survive to a resume replica. Nothing was recorded.`);
575
+ e.code = "config.memory_capture_unsupported";
576
+ throw e;
577
+ }
578
+ const trimmed = reason?.trim();
579
+ const outcome = writeEngine.markSessionCaptureOptOut(sessionId, trimmed !== undefined && trimmed !== "" ? `host flip verb: ${trimmed.slice(0, 200)}` : "host flip verb");
580
+ if (outcome === "unpersisted") {
581
+ deliverEngineNotice(deps.onNotice, memoryCaptureOptOutUnpersistedNotice({ sessionId, ingress: "flip-verb" }));
582
+ throw captureOptOutUnpersistedError(sessionId, "flip-verb");
583
+ }
584
+ captureOptedOut = true;
585
+ if (outcome === "created") {
586
+ deliverEngineNotice(deps.onNotice, memoryCaptureOptedOutNotice({ sessionId, origin: "flip-verb" }));
587
+ if (input.memoryCapturePolicyDeclared !== true && !undeclaredCapturePostureAnnounced) {
588
+ undeclaredCapturePostureAnnounced = true;
589
+ deps.onError?.(new Error(`a session memory-capture opt-out was honored under the DEFAULT capture posture (no RunnerDeps.memoryCapturePolicy configured) — ` +
590
+ `the switch is now in use; declare "open", "governed" or "capture-required" to make the deployment's stance explicit.`), { phase: "config", sessionId });
591
+ }
592
+ }
593
+ const sweep = await writeEngine.sweepSessionCaptureResidue(writeHandle);
594
+ if (sweep.failures.length > 0) {
595
+ const named = sweep.failures
596
+ .slice(0, 5)
597
+ .map((f) => `${f.path} (${f.detail})`)
598
+ .join("; ");
599
+ const e = new Error(`memory capture opt-out is ON (the one-way record stands) but the boundary sweep could not contain ` +
600
+ `${sweep.failures.length} path(s) on the writable memory root — the named residue stays on the authoritative ` +
601
+ `plane until a retry or host action clears it: ${named}${sweep.failures.length > 5 ? "; …" : ""}`);
602
+ e.code = "memory.capture_optout_sweep_failed";
603
+ throw e;
604
+ }
605
+ return { outcome };
606
+ },
607
+ },
353
608
  pollution: {
354
609
  polluted: () => writeEngine.sessionPollution(sessionId),
355
610
  markPolluted: (reason, cause) => {
@@ -409,7 +664,10 @@ export async function prepareMemory(input) {
409
664
  if (typeof err.code === "string" &&
410
665
  (err.code.startsWith("config.memory_scope") ||
411
666
  err.code.startsWith("config.memory_project") ||
412
- err.code === "config.memory_dual_root_overlap")) {
667
+ err.code === "config.memory_dual_root_overlap" ||
668
+ err.code === "memory.capture_optout_denied" ||
669
+ err.code === "memory.capture_optout_unpersisted" ||
670
+ err.code.startsWith("config.memory_capture"))) {
413
671
  throw err;
414
672
  }
415
673
  deps.onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "memory", sessionId });
@@ -427,6 +685,7 @@ export async function prepareMemory(input) {
427
685
  }
428
686
  let memoryBlock;
429
687
  let seedFiles;
688
+ let recallDisciplineSegment;
430
689
  if (memorySpec && memorySpec.enabled && !useMemoryEngine) {
431
690
  const detail = "TaskSpec.memory is enabled but RunnerDeps.memoryBackend is not configured; memory is inactive for this task (the legacy memoryStore fallback was retired in design/138 S4 and the dep was removed in design/157 B19).";
432
691
  deps.onError?.(new Error(detail), { phase: "config", sessionId });
@@ -434,7 +693,13 @@ export async function prepareMemory(input) {
434
693
  if (memoryEngineSession) {
435
694
  const injection = memoryEngineSession.inject();
436
695
  let blockBody = injection.block;
437
- if (!input.rosterCanPersist && injection.instruction === "" && injection.readOnlyNotice === undefined) {
696
+ if (captureOptedOut) {
697
+ blockBody = [MEMORY_CAPTURE_OPTOUT_NOTICE, injection.index, injection.announceBlock].filter((s) => Boolean(s && s.trim())).join("\n\n");
698
+ }
699
+ else if (captureIndeterminate && input.rosterCanPersist) {
700
+ blockBody = [MEMORY_CAPTURE_INDETERMINATE_NOTICE, injection.index, injection.announceBlock].filter((s) => Boolean(s && s.trim())).join("\n\n");
701
+ }
702
+ else if (!input.rosterCanPersist && injection.instruction === "" && injection.readOnlyNotice === undefined) {
438
703
  blockBody = blockBody.trim() ? `${MEMORY_READONLY_NOTICE}\n\n${blockBody}` : MEMORY_READONLY_NOTICE;
439
704
  }
440
705
  else if (!input.rosterCanPersist && injection.instruction !== "") {
@@ -449,16 +714,23 @@ export async function prepareMemory(input) {
449
714
  !blockBody.trimEnd().endsWith(MEMORY_ANNOUNCEMENT_READONLY_CODA)) {
450
715
  blockBody = `${blockBody}\n\n${MEMORY_ANNOUNCEMENT_READONLY_CODA}`;
451
716
  }
717
+ if (captureOptedOut &&
718
+ input.rosterCanPersist &&
719
+ (injection.announceBlock?.trim() ?? "") !== "" &&
720
+ blockBody.includes(injection.announceBlock) &&
721
+ !blockBody.trimEnd().endsWith(MEMORY_ANNOUNCEMENT_CAPTURE_OPTOUT_CODA)) {
722
+ blockBody = `${blockBody}\n\n${MEMORY_ANNOUNCEMENT_CAPTURE_OPTOUT_CODA}`;
723
+ }
452
724
  if (blockBody.trim())
453
725
  memoryBlock = blockBody;
454
726
  if (memoryTools !== undefined) {
455
- const segment = memoryRecallDisciplineSegment(input.memoryProvenance);
456
- memoryBlock = memoryBlock !== undefined ? `${memoryBlock}\n\n${segment}` : segment;
727
+ recallDisciplineSegment = memoryRecallDisciplineSegment(input.memoryProvenance, { markedEntriesServed: memoryMarkedEntriesServed });
728
+ memoryBlock = memoryBlock !== undefined ? `${memoryBlock}\n\n${recallDisciplineSegment}` : recallDisciplineSegment;
457
729
  }
458
- if (memoryEngineSession.handle.writeScope !== null && input.writeToolsMounted && input.rosterCanPersist) {
730
+ if (memoryEngineSession.handle.writeScope !== null && input.writeToolsMounted && input.rosterCanPersist && !captureOptedOut && !captureIndeterminate) {
459
731
  memoryBlock = memoryBlock !== undefined ? `${memoryBlock}\n\n${MEMORY_PREFERENCE_DISCIPLINE}` : MEMORY_PREFERENCE_DISCIPLINE;
460
732
  }
461
- if (memoryBlock !== undefined && injection.indexSeed !== undefined && input.rosterCanPersist)
733
+ if (memoryBlock !== undefined && injection.indexSeed !== undefined && input.rosterCanPersist && !captureOptedOut && !captureIndeterminate)
462
734
  seedFiles = [injection.indexSeed];
463
735
  }
464
736
  if (effectiveMemoryScopes === undefined) {
@@ -468,5 +740,14 @@ export async function prepareMemory(input) {
468
740
  : { state: "none", reason: "disabled", scopes: [], writeScope: null }
469
741
  : { state: "none", reason: "no-spec", scopes: [], writeScope: null };
470
742
  }
471
- return { memoryEngineSession, memoryBlock, admittedOrgScopes, ownOrgVerdict, effectiveMemoryScopes, ...(memoryTools !== undefined ? { memoryTools } : {}), ...(seedFiles !== undefined ? { seedFiles } : {}) };
743
+ return {
744
+ memoryEngineSession,
745
+ memoryBlock,
746
+ admittedOrgScopes,
747
+ ownOrgVerdict,
748
+ effectiveMemoryScopes,
749
+ ...(memoryTools !== undefined ? { memoryTools } : {}),
750
+ ...(recallDisciplineSegment !== undefined ? { recallDisciplineSegment } : {}),
751
+ ...(seedFiles !== undefined ? { seedFiles } : {}),
752
+ };
472
753
  }