@sema-agent/core 7.12.0 → 7.14.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 (122) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/dist/agents/subagent.d.ts +4 -9
  3. package/dist/agents/subagent.js +16 -12
  4. package/dist/agents/teacher.js +1 -1
  5. package/dist/brain/reasoning.d.ts +23 -0
  6. package/dist/brain/reasoning.js +35 -4
  7. package/dist/brain/stream-engine.js +35 -17
  8. package/dist/brain/timeout.d.ts +25 -4
  9. package/dist/brain/timeout.js +1 -1
  10. package/dist/core/ask-origin.d.ts +83 -9
  11. package/dist/core/ask-origin.js +31 -5
  12. package/dist/core/auto-mode.d.ts +3 -3
  13. package/dist/core/checkpoint-seat.d.ts +18 -0
  14. package/dist/core/checkpoint-seat.js +6 -0
  15. package/dist/core/checkpoint-store.d.ts +9 -16
  16. package/dist/core/checkpoint-store.js +7 -4
  17. package/dist/core/engine-notice.d.ts +21 -1
  18. package/dist/core/env-knob-announce.d.ts +16 -0
  19. package/dist/core/env-knob-announce.js +14 -0
  20. package/dist/core/gate-fold.js +2 -0
  21. package/dist/core/gate-lanes.d.ts +0 -40
  22. package/dist/core/gate-lanes.js +24 -29
  23. package/dist/core/gate-outcome.d.ts +42 -3
  24. package/dist/core/gate-outcome.js +22 -0
  25. package/dist/core/governance-codes.d.ts +1 -1
  26. package/dist/core/governance-codes.js +3 -1
  27. package/dist/core/hooks.d.ts +6 -17
  28. package/dist/core/hooks.js +7 -5
  29. package/dist/core/mcp.d.ts +3 -2
  30. package/dist/core/mcp.js +5 -11
  31. package/dist/core/memory-engine/engine.d.ts +2 -2
  32. package/dist/core/runner/advertised-writable-dirs.d.ts +74 -0
  33. package/dist/core/runner/advertised-writable-dirs.js +48 -0
  34. package/dist/core/runner/assemble-result.d.ts +50 -1
  35. package/dist/core/runner/assemble-result.js +4 -1
  36. package/dist/core/runner/compaction-seams.d.ts +42 -0
  37. package/dist/core/runner/compaction-seams.js +80 -0
  38. package/dist/core/runner/contracts.d.ts +44 -6
  39. package/dist/core/runner/denial-limit-arms.d.ts +7 -9
  40. package/dist/core/runner/denial-limit-arms.js +11 -12
  41. package/dist/core/runner/gate-exit.d.ts +15 -3
  42. package/dist/core/runner/gate-exit.js +6 -4
  43. package/dist/core/runner/permission-rule-lanes.d.ts +2 -1
  44. package/dist/core/runner/permission-rule-lanes.js +2 -1
  45. package/dist/core/runner/prepare-caps-and-workflow.d.ts +34 -4
  46. package/dist/core/runner/prepare-caps-and-workflow.js +47 -12
  47. package/dist/core/runner/prepare-gate-stations.d.ts +1 -1
  48. package/dist/core/runner/prepare-hands-readface.d.ts +10 -4
  49. package/dist/core/runner/prepare-hands-readface.js +5 -14
  50. package/dist/core/runner/prepare-inherited-gate.d.ts +5 -3
  51. package/dist/core/runner/prepare-inherited-gate.js +1 -1
  52. package/dist/core/runner/prepare-memory.d.ts +31 -34
  53. package/dist/core/runner/prepare-memory.js +73 -51
  54. package/dist/core/runner/prepare-policy-chain.js +8 -4
  55. package/dist/core/runner/prepare-prompt-inputs.d.ts +4 -0
  56. package/dist/core/runner/prepare-prompt-inputs.js +2 -2
  57. package/dist/core/runner/prepare-protocol-tools.js +5 -0
  58. package/dist/core/runner/prepare-question-face.d.ts +10 -8
  59. package/dist/core/runner/prepare-question-face.js +1 -3
  60. package/dist/core/runner/prepare-safety-scan.js +11 -16
  61. package/dist/core/runner/prepare-task.d.ts +9 -1
  62. package/dist/core/runner/prepare-task.js +63 -49
  63. package/dist/core/runner/prepare-wiring-manifest.d.ts +2 -2
  64. package/dist/core/runner/prepare-wiring-manifest.js +1 -1
  65. package/dist/core/runner/resume-claim.d.ts +2 -2
  66. package/dist/core/runner/resume-preflight.d.ts +2 -2
  67. package/dist/core/runner/run-attachment-seats.d.ts +2 -2
  68. package/dist/core/runner/run-compaction-machinery.js +2 -0
  69. package/dist/core/runner/run-git-lane.d.ts +1 -1
  70. package/dist/core/runner/run-harness-handlers.js +5 -1
  71. package/dist/core/runner/run-identity-wiring.d.ts +7 -0
  72. package/dist/core/runner/run-identity-wiring.js +3 -1
  73. package/dist/core/runner/run-leg.d.ts +7 -5
  74. package/dist/core/runner/run-leg.js +256 -5
  75. package/dist/core/runner/run-notification-lane.d.ts +4 -3
  76. package/dist/core/runner/run-recovery-lanes.d.ts +4 -17
  77. package/dist/core/runner/run-recovery-lanes.js +5 -4
  78. package/dist/core/runner/run-settle-and-teardown.d.ts +14 -10
  79. package/dist/core/runner/run-settle-and-teardown.js +112 -4
  80. package/dist/core/runner/run-stop-and-final-verify.d.ts +2 -2
  81. package/dist/core/runner/run-terminal-adoption.d.ts +16 -16
  82. package/dist/core/runner/run-terminal-adoption.js +87 -7
  83. package/dist/core/runner/run-turn-boundary.js +5 -4
  84. package/dist/core/runner/runtask.d.ts +10 -90
  85. package/dist/core/runner/runtask.js +77 -596
  86. package/dist/core/runner/stream-lifecycle-verbs.js +10 -1
  87. package/dist/core/runner/stream-settle-backstop.js +1 -1
  88. package/dist/core/runtime-caps.d.ts +21 -0
  89. package/dist/core/runtime-caps.js +5 -1
  90. package/dist/core/task-event.d.ts +11 -2
  91. package/dist/core/task-registry-shared.js +8 -6
  92. package/dist/core/task-result.d.ts +15 -0
  93. package/dist/core/task-stream.d.ts +10 -1
  94. package/dist/core/terminal-cause.d.ts +6 -2
  95. package/dist/core/tool-face.d.ts +8 -0
  96. package/dist/core/tool-face.js +1 -0
  97. package/dist/core/tool-policy.d.ts +39 -31
  98. package/dist/core/tool-policy.js +29 -5
  99. package/dist/core/tool-registry.d.ts +11 -3
  100. package/dist/core/tool-registry.js +7 -1
  101. package/dist/core/tool-roster.d.ts +26 -0
  102. package/dist/core/tool-roster.js +40 -7
  103. package/dist/core/tool-spec.d.ts +13 -12
  104. package/dist/core/types.d.ts +2 -1
  105. package/dist/core/types.js +1 -0
  106. package/dist/core/wiring-manifest.d.ts +16 -9
  107. package/dist/core/wiring-manifest.js +8 -3
  108. package/dist/index.d.ts +8 -7
  109. package/dist/index.js +7 -6
  110. package/dist/orchestration/run-workflow-tool.d.ts +8 -8
  111. package/dist/orchestration/run-workflow-tool.js +1 -1
  112. package/dist/orchestration/workflow-primitives.d.ts +4 -3
  113. package/dist/orchestration/workflow-primitives.js +3 -3
  114. package/dist/orchestration/workflow-types.d.ts +14 -0
  115. package/dist/orchestration/workflow.d.ts +41 -4
  116. package/dist/orchestration/workflow.js +23 -5
  117. package/dist/server/http.js +2 -1
  118. package/dist/tools/fs/fs-shared.js +9 -3
  119. package/dist/tools/fs/read-deny.d.ts +24 -8
  120. package/dist/tools/fs/read-deny.js +20 -1
  121. package/package.json +2 -1
  122. package/test/export-surface.snapshot.json +35 -1
@@ -25,28 +25,33 @@ import type { PromptEpochArtifact } from "../../prompt-assembly/artifact.js";
25
25
  import { type WorkflowSizeGuideline } from "../../orchestration/workflow-size-guideline.js";
26
26
  import { type AutoModeArmingRecipe } from "../auto-mode-arming.js";
27
27
  import { type AutoModeDecider, type AutoModeDenialTracker } from "../auto-mode.js";
28
+ import { type CheckpointSeat } from "../checkpoint-seat.js";
28
29
  import { type ComplianceCapability } from "../compliance.js";
29
30
  import type { LockedPreflight } from "../locked-config.js";
30
31
  import type { StoredSession } from "../session.js";
31
32
  import type { OnAsk } from "../tool-policy.js";
32
33
  import type { RunnerDeps, RuntimeCaps, TaskEvent, TaskSpec, ToolExecuteContext } from "../types.js";
33
- import type { AutoModeArmReason } from "../wiring-manifest.js";
34
+ import type { AutoModeArmFact } from "../wiring-manifest.js";
35
+ import type { AssertAllKeysHandled } from "../ask-origin.js";
34
36
  import type { ReadFace } from "../../tools/fs/index.js";
35
37
  import type { InheritedGate, Prepared, RunInternals, RunnerSelfSeat, ToolFaceSnapshot } from "./contracts.js";
36
38
  import { type BlockedRef } from "./synthetic-tools.js";
37
39
  import { RosterBuilder } from "../tool-roster.js";
38
40
  export interface PrepareCapsAndWorkflowInput {
39
41
  /** borrowed-readonly — the REBOUND spec, whole: the roster map reads `tools`; the mounts read `enableBlockedReport`,
40
- * `enablePlanMode`, `interactiveTools`, `checkpointStore`; the caps/compliance stations read `principal`, `selfOrchestration`,
42
+ * `enablePlanMode`, `interactiveTools`; the caps/compliance stations read `principal`, `selfOrchestration`,
41
43
  * `enableFork`, `getApiKeyAndHeaders`; the Workflow mount forwards `oneShot`, `handsReadOnly`, `interactiveTools`. The
42
44
  * self-orchestration predicate takes the whole spec. Never mutated. */
43
45
  spec: TaskSpec;
44
46
  /** borrowed-readonly — the deployment seats, whole: `runtimeCapsResolver`, `compliancePostureResolver`, `onError`, `onNotice`,
45
- * `autoMode`, `brain`, `models`, `checkpointStore` (the plan-pair face), and the Workflow mount's dozen deps (`workflowScriptRunner`,
47
+ * `autoMode`, `brain`, `models`, and the Workflow mount's dozen deps (`workflowScriptRunner`,
46
48
  * `workflowGovernanceBaseline`, `workflowRunStore`, `workflowJournalStore`, `workflowScriptStore`, `builtinWorkflows`,
47
49
  * `onWorkflowAgentSpawn`, `workflowCompletionNotifier`, `workflowLimits`, `agents`, `builtinAgents`, `onBackgroundChildEvent`).
48
50
  * The self-orchestration predicate takes the whole record. */
49
51
  deps: RunnerDeps;
52
+ /** borrowed-readonly — the seat this run hands its children (`checkpointSeatOf`, minted once by the driver right after the
53
+ * config doors): the plan-pair face reads the store it names, the Workflow mount hands the seat itself to its children. */
54
+ checkpointSeat: CheckpointSeat | undefined;
50
55
  /** borrowed-readonly — the trusted spawn-side channel; the Workflow mount forwards `rootSessionId`, `placementRoot`,
51
56
  * `onTaskNotification`, `workflowDepth`, and the capture-floor getter reads `memoryCaptureAncestors`. */
52
57
  internals: Pick<RunInternals, "rootSessionId" | "placementRoot" | "onTaskNotification" | "workflowDepth" | "memoryCaptureAncestors" | "workflowParkedResume"> | undefined;
@@ -145,7 +150,8 @@ export interface PrepareCapsAndWorkflowResult {
145
150
  /** owned — the observers grant (`allowObservers === true`, explicit opt-in). */
146
151
  observersActive: boolean;
147
152
  /** owned — the arming outcome as a closed reason (the manifest's auto-mode face). */
148
- autoModeArmReason: AutoModeArmReason;
153
+ /** #529 / #692 C-d — the leg's auto-mode arming fact (`armed` ⇔ `reason === "armed"`; `deniedSource` only beside a STATED `denied`). */
154
+ autoModeArmReason: AutoModeArmFact;
149
155
  /** owned — the armed classifier, or undefined when any arming arm failed. */
150
156
  autoModeDecider: AutoModeDecider | undefined;
151
157
  /** owned — the denial tracker built beside the decider (same face, same arming). */
@@ -162,5 +168,29 @@ export interface PrepareCapsAndWorkflowResult {
162
168
  current: () => WorkflowSizeGuideline;
163
169
  } | undefined;
164
170
  }
171
+ /**
172
+ * The resolved-caps value screen (the caps-side twin of `refuseInvalidAutoModeSeat`): the arming arm reads
173
+ * `autoMode !== false`, so a resolver that returns a MARSHALLED value — a non-boolean member (`"false"`,
174
+ * `0`, `null`) or a non-record container (`false`, `"false"`, `[]`; the resolver is an HTTP-backed seam the
175
+ * type system does not validate at runtime) — would pass a value meant as a denial straight into arming.
176
+ * Either shape is treated as a disclosed resolver fault: announced through onError (phase config), and
177
+ * coined fail-closed — a bad MEMBER to `autoMode:false` (the rest of the record kept), a bad CONTAINER to
178
+ * the same degrade object the resolver-throw arm uses (every `!== false`-gated cap denied), with
179
+ * `faulted` raised so the posture-keyed consumers read it as an outage, not as a verdict. `undefined`, or
180
+ * a record whose `autoMode` is `undefined`/`true`/`false`, passes untouched (byte-identical).
181
+ */
182
+ /** The supported capability keys, fenced total over {@link RuntimeCaps}: a key added to the type without a row here
183
+ * is a red build, so the snapshot below can never silently drop a new restriction. */
184
+ declare const RUNTIME_CAP_KEYS: {
185
+ readonly allowWorkflows: true;
186
+ readonly allowFork: true;
187
+ readonly allowObservers: true;
188
+ readonly forceDurableGate: true;
189
+ readonly autoMode: true;
190
+ readonly autoModeDenySource: true;
191
+ readonly allowMemoryOptOut: true;
192
+ };
193
+ export type RuntimeCapKeysCoverEveryCap = AssertAllKeysHandled<Exclude<keyof RuntimeCaps, keyof typeof RUNTIME_CAP_KEYS>>;
165
194
  /** The M4 phase body — prepareTask's caps-and-workflow stretch, verbatim (see the module header). */
166
195
  export declare function prepareCapsAndWorkflow(input: PrepareCapsAndWorkflowInput): Promise<PrepareCapsAndWorkflowResult>;
196
+ export {};
@@ -10,7 +10,7 @@ import { autoModeArmingRecipeOf } from "../auto-mode-arming.js";
10
10
  import { AUTO_MODE_CLASSIFIER_LOW_REASONING_BUDGET_TOKENS, AUTO_MODE_CLASSIFIER_MAX_TOKENS } from "../auto-mode-defaults.js";
11
11
  import { buildAutoModePrompt, renderAutoModeAction, renderAutoModeWindow } from "../auto-mode-prompt.js";
12
12
  import { createAutoModeDecider, createAutoModeDenialTracker } from "../auto-mode.js";
13
- import { resolveCheckpointStore } from "../checkpoint-store.js";
13
+ import { checkpointStoreOfSeat } from "../checkpoint-seat.js";
14
14
  import { COMPLIANCE_CAPABILITIES, WEB_FETCH_TOOL_NAME, resolveComplianceDenies } from "../compliance.js";
15
15
  import { createPresentPlanTool, createEnterPlanModeTool } from "../present-plan-tool.js";
16
16
  import { resolveTaskModel } from "../roles.js";
@@ -19,6 +19,7 @@ import { defaultTaskRegistry } from "../task-registry.js";
19
19
  import { emitTrace } from "../trace.js";
20
20
  import { defineTool, isDefineToolProduct, rebindDefineToolCtx } from "../tools.js";
21
21
  const MOUNT_REMEDY = "author it as a plain-object ToolSpec (own `name` and `execute`), hand the defineTool product itself, or wrap a product through the supported wrapper form (a fresh object re-stamped with `stampDefineToolBrand` whose rebind re-wraps the rebound product)";
22
+ import { AUTO_MODE_DENY_SOURCES, isAutoModeDenySource } from "../runtime-caps.js";
22
23
  import { derivedRouteFallsBack } from "./derived-route-fallback.js";
23
24
  import { REPORT_FINDINGS_TOOL_NAME, createReportBlockedTool, createReportFindingsTool } from "./synthetic-tools.js";
24
25
  function classifierReplyText(reply) {
@@ -42,6 +43,24 @@ function assembleParentCaptureState(o, i, ctl, ancestors) {
42
43
  });
43
44
  return o instanceof Promise || i instanceof Promise ? Promise.all([o, i]).then(([ov, iv]) => build(ov, iv)) : build(o, i);
44
45
  }
46
+ const RUNTIME_CAP_KEYS = {
47
+ allowWorkflows: true,
48
+ allowFork: true,
49
+ allowObservers: true,
50
+ forceDurableGate: true,
51
+ autoMode: true,
52
+ autoModeDenySource: true,
53
+ allowMemoryOptOut: true,
54
+ };
55
+ function snapshotRuntimeCaps(record) {
56
+ const out = {};
57
+ for (const key of Object.keys(RUNTIME_CAP_KEYS)) {
58
+ const v = record[key];
59
+ if (v !== undefined)
60
+ out[key] = v;
61
+ }
62
+ return out;
63
+ }
45
64
  function screenAutoModeCap(caps, onError, sessionId) {
46
65
  if (caps === undefined)
47
66
  return { caps, faulted: false };
@@ -49,7 +68,20 @@ function screenAutoModeCap(caps, onError, sessionId) {
49
68
  onError?.(new Error(`runtimeCapsResolver returned a non-record value (${JSON.stringify(caps)}) — read as a resolver fault: every per-principal capability is DENIED for this run (auto mode is not armed; asks flow the original chain). Return a RuntimeCaps object or undefined.`), { phase: "config", sessionId });
50
69
  return { caps: { allowWorkflows: false, allowFork: false, autoMode: false }, faulted: true };
51
70
  }
52
- const record = caps;
71
+ let record;
72
+ try {
73
+ record = snapshotRuntimeCaps(caps);
74
+ }
75
+ catch (err) {
76
+ onError?.(new Error(`runtimeCapsResolver's record threw while being read (${err instanceof Error ? err.message : String(err)}) — read as a resolver fault: every per-principal capability is DENIED for this run (auto mode is not armed; asks flow the original chain).`), { phase: "config", sessionId });
77
+ return { caps: { allowWorkflows: false, allowFork: false, autoMode: false }, faulted: true };
78
+ }
79
+ const { autoMode, autoModeDenySource } = record;
80
+ if (autoModeDenySource !== undefined && (!isAutoModeDenySource(autoModeDenySource) || autoMode !== false)) {
81
+ onError?.(new Error(`runtimeCapsResolver returned autoModeDenySource=${JSON.stringify(autoModeDenySource)} beside autoMode=${JSON.stringify(autoMode)} — read as a resolver fault: auto mode is DENIED for this run (the classifier is not armed; asks flow the original chain). Name a source only beside autoMode:false, and only one of ${AUTO_MODE_DENY_SOURCES.join("|")}.`), { phase: "config", sessionId });
82
+ const { autoModeDenySource: _refused, ...rest } = record;
83
+ return { caps: { ...rest, autoMode: false }, faulted: true };
84
+ }
53
85
  if (record.autoMode === undefined || typeof record.autoMode === "boolean")
54
86
  return { caps: record, faulted: false };
55
87
  onError?.(new Error(`runtimeCapsResolver returned a non-boolean autoMode (${JSON.stringify(record.autoMode)}) — read as a resolver fault: auto mode is DENIED for this run (the classifier is not armed; asks flow the original chain). Return true, false, or omit the key.`), { phase: "config", sessionId });
@@ -67,17 +99,20 @@ function mintPersistedArming(am, laneRule, classifierSystemPrompt, onError, sess
67
99
  }
68
100
  return arming;
69
101
  }
70
- function autoModeArmReasonOf(intent, facePresent, capsAutoMode, capsFaulted) {
102
+ function autoModeArmReasonOf(intent, facePresent, caps, capsFaulted) {
71
103
  if (!intent)
72
- return "no_intent";
104
+ return { armed: false, reason: "no_intent" };
73
105
  if (!facePresent)
74
- return "no_face";
75
- if (capsAutoMode === false)
76
- return capsFaulted ? "resolver_fault" : "denied";
77
- return "armed";
106
+ return { armed: false, reason: "no_face" };
107
+ if (caps?.autoMode === false) {
108
+ if (capsFaulted)
109
+ return { armed: false, reason: "resolver_fault" };
110
+ return { armed: false, reason: "denied", ...(caps.autoModeDenySource !== undefined ? { deniedSource: caps.autoModeDenySource } : {}) };
111
+ }
112
+ return { armed: true, reason: "armed" };
78
113
  }
79
114
  export async function prepareCapsAndWorkflow(input) {
80
- const { spec, deps, internals, session, sessionId, runId, hostTaskId, taskScope, taskRootFinal, model, thinking, lockedPreflight, toolEffects, toolFaceSnapshot, promptProfile, resolvedInteractionPosture, harnessRef, frozenOnAsk, carrierReadFace, forwardEvent, inheritedGateForChildren, enrichSpecToolCtx, maybeOffload, requestReview, enterPlanMode, autoModeIntent, peerLaneActive, peerSendMessageBuiltIn, runnerSelf } = input;
115
+ const { spec, deps, checkpointSeat, internals, session, sessionId, runId, hostTaskId, taskScope, taskRootFinal, model, thinking, lockedPreflight, toolEffects, toolFaceSnapshot, promptProfile, resolvedInteractionPosture, harnessRef, frozenOnAsk, carrierReadFace, forwardEvent, inheritedGateForChildren, enrichSpecToolCtx, maybeOffload, requestReview, enterPlanMode, autoModeIntent, peerLaneActive, peerSendMessageBuiltIn, runnerSelf } = input;
81
116
  const roster = new RosterBuilder(toolEffects);
82
117
  for (const t of spec.tools ?? []) {
83
118
  const mounted = mountCallerTool(t);
@@ -112,7 +147,7 @@ export async function prepareCapsAndWorkflow(input) {
112
147
  roster.mount(createReportFindingsTool(), { source: "builtin", mountedBy: "always" });
113
148
  }
114
149
  if (spec.enablePlanMode === true && spec.interactiveTools !== false) {
115
- const planReviewFace = resolveCheckpointStore(spec, deps) !== undefined;
150
+ const planReviewFace = checkpointStoreOfSeat(checkpointSeat) !== undefined;
116
151
  if (spec.interactiveTools === true || planReviewFace) {
117
152
  roster.mount(defineTool(createPresentPlanTool(requestReview)), { source: "builtin", mountedBy: "optional" });
118
153
  if (planReviewFace) {
@@ -173,7 +208,7 @@ export async function prepareCapsAndWorkflow(input) {
173
208
  let autoModeDecider;
174
209
  let autoModeDenialTracking;
175
210
  let autoModeArming;
176
- const autoModeArmReason = autoModeArmReasonOf(autoModeIntent, deps.autoMode !== undefined, runtimeCaps?.autoMode, runtimeCapsFaulted);
211
+ const autoModeArmReason = autoModeArmReasonOf(autoModeIntent, deps.autoMode !== undefined, runtimeCaps, runtimeCapsFaulted);
177
212
  if (autoModeIntent && deps.autoMode !== undefined && runtimeCaps?.autoMode !== false) {
178
213
  const am = deps.autoMode;
179
214
  let classifierModel;
@@ -297,7 +332,7 @@ export async function prepareCapsAndWorkflow(input) {
297
332
  },
298
333
  ...(spec.handsReadOnly === true ? { parentHandsReadOnly: true } : {}), ...(spec.interactiveTools === false ? { parentInteractiveTools: false } : {}),
299
334
  onNotice: deps.onNotice,
300
- parentCheckpointStoreDisabled: spec.checkpointStore === "disabled",
335
+ ...(checkpointSeat !== undefined ? { parentCheckpointStore: checkpointSeat } : {}),
301
336
  parentCenterArtifactDigest: () => input.centerAdoption()?.artifact.artifactDigest,
302
337
  parentCenterSourceRevision: () => input.centerAdoption()?.sourceRevision,
303
338
  ...(forwardEvent ? { forwardEvent } : {}),
@@ -19,7 +19,7 @@
19
19
  * gate-machinery block, kept as the block's one guard here); the `tool_result` station keys off the hook record alone, as
20
20
  * it always did.
21
21
  *
22
- * Every station this file holds is a `denySource` / settlement WRITE site the decomposition's next slice maps onto one
22
+ * Every station this file holds is a `deniedBy` / settlement WRITE site the decomposition's next slice maps onto one
23
23
  * gate outcome; nothing here changes a word, a source or an order — the stations are moved, not re-adjudicated.
24
24
  */
25
25
  import type { AgentHarness, AgentTool } from "../../internal/harness.js";
@@ -3,6 +3,7 @@ import { type ReadFileState } from "../../tools/fs/safety.js";
3
3
  import type { RunnerDeps, TaskSpec } from "../types.js";
4
4
  import type { Model } from "../../internal/llm.js";
5
5
  import type { AgentTool, ExecutionEnv, WorkspaceState } from "../../internal/harness.js";
6
+ import type { AdvertisedWritableDirs } from "./advertised-writable-dirs.js";
6
7
  import type { StoredSession } from "../session.js";
7
8
  import type { IrreversibilityTier, PrepareResume, ReversibilityProbes, RunInternals, ToolFaceSnapshot } from "./contracts.js";
8
9
  import { RosterBuilder } from "../tool-roster.js";
@@ -161,13 +162,18 @@ export interface PrepareHandsReadFaceInput {
161
162
  * when no file restore ran. Fail-closed by construction — dropping a read record only ever
162
163
  * produces refusals. */
163
164
  restoredFilePaths?: readonly string[];
164
- /** borrowed-readonly — (ref cell; the REF itself is never replaced) design/138 S2-C late-bound memory
165
- * write gate: the band's beforeWrite closure reads `.current` call-time; prepareMemory (driver,
166
- * after this phase) installs the gate the one writer; this phase only reads. Declared in the driver because the SAME ref feeds
167
- * prepareMemory. */
165
+ /** borrowed-readonly — (ref cell; the REF itself is never replaced) design/138 S2-C memory write gate:
166
+ * the band's beforeWrite closure reads `.current` call-time; prepareMemory (driver, BEFORE this phase
167
+ * since #691 the memory root must be known before the toolkit compiles) installs the gate the one
168
+ * writer; this phase only reads. Declared in the driver because the SAME ref feeds prepareMemory. */
168
169
  memoryWriteGateRef: {
169
170
  current?: BeforeWriteHook;
170
171
  };
172
+ /** borrowed-mutable — #691: the advertised-writable-directory seat (see its module header for the one
173
+ * rule). Present on the hands leg only (a fence exists). The memory phase, which runs BEFORE this one,
174
+ * has already admitted its write root through it; this phase admits the deployment's scratchpad and
175
+ * folds `roots` into the write-capable containment allowlist before the toolkit compiles. */
176
+ advertisedWritableDirs?: AdvertisedWritableDirs;
171
177
  /** borrowed-readonly — the driver's per-tool offload wrapper (design/158 posture + policy); the
172
178
  * band rides through it exactly as caller/MCP tools do. */
173
179
  firstPartyOffload: (tool: AgentTool) => AgentTool;
@@ -124,7 +124,7 @@ export function resolveHandsLessReadFace(input) {
124
124
  }
125
125
  export async function prepareHandsMount(input) {
126
126
  const envHandToolNames = new Set();
127
- const { handsEnabled, executionEnv, taskRootFinal, rebaseRestoredPath, resume, session, sessionId, hostTaskId, taskScope, fullShellReachable, effectiveShellGate, toolFaceSnapshot, model, spec, deps, internals, memoryWriteGateRef, firstPartyOffload, roster, egressTools, irreversibilityTier, irreversibleTools, reversibilityProbes } = input;
127
+ const { handsEnabled, executionEnv, taskRootFinal, rebaseRestoredPath, resume, session, sessionId, hostTaskId, taskScope, fullShellReachable, effectiveShellGate, toolFaceSnapshot, model, spec, deps, internals, memoryWriteGateRef, advertisedWritableDirs, firstPartyOffload, roster, egressTools, irreversibilityTier, irreversibleTools, reversibilityProbes } = input;
128
128
  assertStringArraySeat(spec.additionalDirectories, "TaskSpec.additionalDirectories", "strings");
129
129
  assertStringArraySeat(spec.additionalReadDirectories, "TaskSpec.additionalReadDirectories", "strings");
130
130
  assertStringArraySeat(deps.readDenyPatterns, "RunnerDeps.readDenyPatterns", "compiler-owned");
@@ -178,19 +178,10 @@ export async function prepareHandsMount(input) {
178
178
  };
179
179
  await canonicalizeExtraDirs(spec.additionalDirectories, "additionalDirectories", additionalRootsCanonical);
180
180
  await canonicalizeExtraDirs(spec.additionalReadDirectories, "additionalReadDirectories", additionalReadRootsCanonical);
181
- if (spec.envFacts?.scratchpadDir) {
182
- let c = await executionEnv.canonicalPath(spec.envFacts.scratchpadDir);
183
- if (!c.ok && c.error.code === "not_found") {
184
- const marker = await executionEnv.joinPath([spec.envFacts.scratchpadDir, ".sema-scratchpad"]);
185
- const exclusiveCreate = executionEnv.writeFileExclusive?.bind(executionEnv);
186
- if (marker.ok && exclusiveCreate) {
187
- const w = await exclusiveCreate(marker.value, "");
188
- if (w.ok || w.error.code === "already_exists")
189
- c = await executionEnv.canonicalPath(spec.envFacts.scratchpadDir);
190
- }
191
- }
192
- if (c.ok)
193
- additionalRootsCanonical.push(c.value);
181
+ if (advertisedWritableDirs !== undefined) {
182
+ if (spec.envFacts?.scratchpadDir)
183
+ await advertisedWritableDirs.admit({ path: spec.envFacts.scratchpadDir, advertiser: "scratchpad", materializeMarker: ".sema-scratchpad" });
184
+ additionalRootsCanonical.push(...advertisedWritableDirs.roots);
194
185
  }
195
186
  for (const a of [additionalRootsCanonical, additionalReadRootsCanonical])
196
187
  a.splice(0, a.length, ...new Set(a));
@@ -51,9 +51,11 @@ export interface PrepareInheritedGateInput {
51
51
  /** borrowed-readonly — the REBOUND spec. Read: `shellGate` (the own doctrine the chain fold tightens), `durableApproval`
52
52
  * (the mandate tests + the children's copy), `onAsk`/`onQuestion` (the frozen seats), `handsReadOnly` / `interactiveTools`
53
53
  * / `oneShot` / `clientContext` / `principal` / `additionalDirectories` / `additionalReadDirectories` / `envFacts` /
54
- * `memoryPersistenceCapable` / `getApiKeyAndHeaders` / `checkpointStore` / `backgroundScope` / `enablePlanMode` (the ctx
55
- * enrichment's trusted seats). Never mutated. */
56
- spec: Pick<TaskSpec, "shellGate" | "durableApproval" | "onAsk" | "onQuestion" | "handsReadOnly" | "interactiveTools" | "oneShot" | "clientContext" | "principal" | "additionalDirectories" | "additionalReadDirectories" | "envFacts" | "memoryPersistenceCapable" | "getApiKeyAndHeaders" | "checkpointStore" | "backgroundScope" | "enablePlanMode" | "excludeAllTools">;
54
+ * `memoryPersistenceCapable` / `getApiKeyAndHeaders` / `backgroundScope` / `enablePlanMode` (the ctx enrichment's
55
+ * trusted seats). Never mutated. */
56
+ spec: Pick<TaskSpec, "shellGate" | "durableApproval" | "onAsk" | "onQuestion" | "handsReadOnly" | "interactiveTools" | "oneShot" | "clientContext" | "principal" | "additionalDirectories" | "additionalReadDirectories" | "envFacts" | "memoryPersistenceCapable" | "getApiKeyAndHeaders" | "backgroundScope" | "enablePlanMode" | "excludeAllTools">;
57
+ /** borrowed-readonly — the seat this run hands its children (`checkpointSeatOf`, minted once by the driver after the config doors): the ctx's `checkpointStoreForChildren`, absent when the run has no seat. */
58
+ checkpointSeat: import("../checkpoint-seat.js").CheckpointSeat | undefined;
57
59
  /** borrowed-readonly — the deployment seats: `autoMode.denialLimit` (the rebuilt trackers' bounds), `memoryScopeAdmission` /
58
60
  * `deploymentMemoryScopes` / `compliancePostureResolver` (the governed-provenance bit), `onError` (the screening-face crash
59
61
  * lane), `onNotice` (the stop seat's notice), `executionEnv` (the hook-env identity predicate), `delegationEntryCaps` (resolved
@@ -222,7 +222,7 @@ export function prepareInheritedGate(input) {
222
222
  delegationSettlement: delegationSettlementForChildren,
223
223
  ...(autoModeDecider ? { autoModeReview: { decider: autoModeDecider } } : {}),
224
224
  ...(spec.durableApproval !== undefined ? { durableApprovalForChildren: { ...spec.durableApproval } } : {}),
225
- ...(spec.checkpointStore === "disabled" ? { checkpointStoreDisabledForChildren: true } : {}),
225
+ ...(input.checkpointSeat !== undefined ? { checkpointStoreForChildren: input.checkpointSeat } : {}),
226
226
  taskId: hostTaskId,
227
227
  ...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}),
228
228
  ...(internals?.parentSessionId !== undefined ? { parentSessionId: internals.parentSessionId } : {}),
@@ -1,6 +1,7 @@
1
1
  import type { BeforeWriteHook, EffectiveMemoryScopes, RunnerDeps, TaskSpec, ToolSpec } from "../types.js";
2
2
  import { type CaptureEntitlementInput, type MemoryCapturePosture } from "./memory-capture-optout.js";
3
3
  import type { Prepared } from "./contracts.js";
4
+ import type { AdvertisedWritableDirs } from "./advertised-writable-dirs.js";
4
5
  export interface PrepareMemoryInput {
5
6
  /** borrowed-readonly — the REBOUND spec. Read: `memory` (the scope declaration this phase
6
7
  * normalizes) and `principal` (the org-admission subject). Never mutated. */
@@ -33,23 +34,23 @@ export interface PrepareMemoryInput {
33
34
  * arms — a hands-less run that mounts its own `Write` counts, and an `excludeTools: ["Write"]`
34
35
  * run reads as unmounted whatever the hands band did. Threaded into `engine.inject` so a run with
35
36
  * a writable memory scope but no write channel is not instructed to call a tool that is not on
36
- * its roster (or cannot reach the store); the RB-276 index seed follows the same gate.
37
+ * its roster (or cannot reach the store); the RB-276 index seed follows the same gate. Since #691 this is
38
+ * the FIRST conjunct of the write channel; the second is the root fence's admission of the write plane's
39
+ * root through {@link advertisedWritableDirs} — the channel is open only when both hold.
37
40
  */
38
41
  writeToolsMounted: boolean;
42
+ /**
43
+ * borrowed-mutable — #691: the root fence's advertised-writable-directory seat (its module header carries the
44
+ * one rule). Present iff the hands band mounts this run (a fence exists between the roster's `Write` and the
45
+ * store); absent ⇒ a `Write` on the roster is a caller tool running in the host process, which no fence
46
+ * stands before, so admission holds trivially. This phase admits the WRITE plane's root when the channel's
47
+ * first conjunct holds (`writeToolsMounted`) and a write face exists; a read-only layering admits nothing.
48
+ */
49
+ advertisedWritableDirs?: AdvertisedWritableDirs;
39
50
  /** borrowed-readonly — design/319 (A ticket): the run's reminder provenance mark: the memory engine's announcement
40
51
  * block is an engine-authored `<system-reminder>`, so its open tag must carry the same mark the
41
52
  * system-prompt declaration names. Threaded into every `engine.inject` call this phase builds. */
42
53
  reminderMark: string;
43
- /**
44
- * borrowed-readonly — the session-wide persistence verdict: `TaskSpec.memoryPersistenceCapable` (the deployment's own
45
- * statement — the only honest channel for a custom memory writer persisting through its closure,
46
- * which no inference can see) when set, else the KNOWN-store-path inference: a mounted,
47
- * non-excluded file-write tool (Write/Edit/NotebookEdit) or a write-capable shell. A generic
48
- * write-effect tool does NOT count — a mail sender's side effect is not a memory store, and
49
- * counting it re-opened the silent-confabulation hole. `false` ⇒ this phase mounts
50
- * {@link MEMORY_READONLY_NOTICE} where the engine is silent.
51
- */
52
- rosterCanPersist: boolean;
53
54
  /**
54
55
  * borrowed-readonly — the raw `TaskSpec.memoryPersistenceCapable` DECLARATION, kept separate from the composed
55
56
  * {@link rosterCanPersist} verdict because the two drive different arms:
@@ -184,35 +185,31 @@ export interface PrepareMemoryInput {
184
185
  governedProvenance: boolean;
185
186
  };
186
187
  }
187
- export interface PrepareMemoryResult {
188
- memoryEngineSession: Prepared["memoryEngineSession"];
188
+ /** #691 — what {@link PrepareMemoryResult.composeInjection} hands back: the composed `# Memory` block (undefined
189
+ * when nothing mounts), the recall-discipline segment EXACTLY as appended (prepare-task's retraction strips this
190
+ * returned string — #493 单铸), and the RB-276 index seed files whose full disk text rides in the block. */
191
+ export interface MemoryInjectionComposition {
189
192
  memoryBlock: string | undefined;
190
- /**
191
- * design/178 ②-1 — the `memory_search`/`memory_get` pair's ToolSpecs, present iff the engine
192
- * session mounted AND `memorySearchToolsPlanned` was true. Built HERE because this phase owns the
193
- * planes (backend + mounted scopes + per-plane retrieved-account recorder); prepare-task mounts
194
- * them (atomic pair) and runs the remaining conjuncts.
195
- */
196
- memoryTools?: ToolSpec[];
197
- /**
198
- * #493 单铸 — the recall-discipline segment EXACTLY as this phase appended it to
199
- * {@link memoryBlock} (present iff {@link memoryTools} is). prepare-task's support-name
200
- * retraction strips THIS string: the segment's composition now depends on the marked-presence
201
- * signal only this phase holds, so a strip-site re-composition could mint a string that was
202
- * never appended and leave the real one standing.
203
- */
204
193
  recallDisciplineSegment?: string;
205
- /**
206
- * RB-276 — engine-materialized files whose FULL disk text rides in {@link memoryBlock}, for the
207
- * caller to pre-mark READ through the parity-204 seeding lane (`prepare-task`'s `seedContextFiles`,
208
- * which applies the SAME containment as a real Read — a memory dir outside the task root is simply
209
- * skipped, exactly as the Write tool would refuse it there). Today this is the materialized
210
- * MEMORY.md index; see `MemoryInjection.indexSeed` for the conditions that qualify it.
211
- */
212
194
  seedFiles?: ReadonlyArray<{
213
195
  path: string;
214
196
  content: string;
215
197
  }>;
198
+ }
199
+ export interface PrepareMemoryResult {
200
+ memoryEngineSession: Prepared["memoryEngineSession"];
201
+ memoryTools?: ToolSpec[];
202
+ /**
203
+ * owned — #691: the injection's COMPOSITION step, run by the driver AFTER the hands phase (the mount above runs
204
+ * BEFORE it). It performs the one destructive read this phase has — `inject()` drains the announcement queue —
205
+ * so a hands-phase refusal cannot consume announcements a later prepare should deliver, and it takes the
206
+ * roster-wide persistence verdict the driver can only read off the MOUNTED roster (`rosterCanPersist`: the
207
+ * `TaskSpec.memoryPersistenceCapable` declaration when set, else the known-store-path inference over the
208
+ * assembled tools). Call exactly once per prepare.
209
+ */
210
+ composeInjection: (facts: {
211
+ rosterCanPersist: boolean;
212
+ }) => MemoryInjectionComposition;
216
213
  /**
217
214
  * The FROZEN org-admission verdict of this prepare: every org scope the task actually mounts
218
215
  * (deployment-origin kept + request-origin admitted). Threaded into the run's child chain
@@ -74,6 +74,7 @@ export async function prepareMemory(input) {
74
74
  let captureLineageIndeterminate = false;
75
75
  let memoryEngineSession;
76
76
  let memoryTools;
77
+ let writeChannelOpen = false;
77
78
  let memoryMarkedEntriesServed = false;
78
79
  let effectiveMemoryScopes;
79
80
  const originOf = (scope) => scopeOriginsSnapshot !== undefined
@@ -373,7 +374,7 @@ export async function prepareMemory(input) {
373
374
  writeHandle = writeIsPersonal ? personalHandle : projectHandle;
374
375
  readOnlyEngine = writeIsPersonal ? projectEngine : personalEngine;
375
376
  readOnlyHandle = writeIsPersonal ? projectHandle : personalHandle;
376
- injectFn = () => mergeInjections(projectEngine.inject(projectHandle, { writeToolMounted: input.writeToolsMounted, reminderMark: input.reminderMark }), personalEngine.inject(personalHandle, { writeToolMounted: input.writeToolsMounted, reminderMark: input.reminderMark }));
377
+ injectFn = () => mergeInjections(projectEngine.inject(projectHandle, { writeToolMounted: writeChannelOpen, reminderMark: input.reminderMark }), personalEngine.inject(personalHandle, { writeToolMounted: writeChannelOpen, reminderMark: input.reminderMark }));
377
378
  toolPlanes = [
378
379
  {
379
380
  backend: retrievalBackend(backend, adoptionRestricted || captureOptedOut || captureIndeterminate || p.writePlane !== "project"),
@@ -417,7 +418,7 @@ export async function prepareMemory(input) {
417
418
  memoryMarkedEntriesServed = handle.markedEntriesPresent === true;
418
419
  writeEngine = personalEngine;
419
420
  writeHandle = handle;
420
- injectFn = () => personalEngine.inject(handle, { writeToolMounted: input.writeToolsMounted, reminderMark: input.reminderMark });
421
+ injectFn = () => personalEngine.inject(handle, { writeToolMounted: writeChannelOpen, reminderMark: input.reminderMark });
421
422
  harvestBoth = async () => personalEngine.harvest(handle, { ...pollutedOpts(personalEngine), sessionId, ...(await admitNothingOptsNow()) });
422
423
  toolPlanes = [
423
424
  {
@@ -445,7 +446,7 @@ export async function prepareMemory(input) {
445
446
  memoryMarkedEntriesServed = handle.markedEntriesPresent === true;
446
447
  writeEngine = engine;
447
448
  writeHandle = handle;
448
- injectFn = () => engine.inject(handle, { writeToolMounted: input.writeToolsMounted, reminderMark: input.reminderMark });
449
+ injectFn = () => engine.inject(handle, { writeToolMounted: writeChannelOpen, reminderMark: input.reminderMark });
449
450
  harvestBoth = async () => engine.harvest(handle, { ...pollutedOpts(engine), sessionId, ...(await admitNothingOptsNow()) });
450
451
  toolPlanes = [
451
452
  {
@@ -457,6 +458,22 @@ export async function prepareMemory(input) {
457
458
  },
458
459
  ];
459
460
  }
461
+ const foreignUnderRoot = [];
462
+ if (input.writeToolsMounted && writeHandle.writeScope !== null && input.advertisedWritableDirs !== undefined) {
463
+ for (const d of [...writeEngine.readonlyDirNamesUnderRoot(writeHandle)].sort())
464
+ foreignUnderRoot.push(`another scope's directory ${d}`);
465
+ const controlPlane = canonicalize(writeEngine.controlPlaneDir);
466
+ if (isContainedIn(writeHandle.writableRoot, controlPlane))
467
+ foreignUnderRoot.push(`the engine's control plane ${controlPlane}`);
468
+ }
469
+ if (foreignUnderRoot.length > 0) {
470
+ deps.onError?.(new Error(`memory advertised writable directory refused: the write scope's root ${writeHandle.writableRoot} contains what is not the model's to touch (${foreignUnderRoot.join("; ")}) — the file tools' root fence is directory-granular, so admitting it would expose those files; memory writes through the file tools stay closed this session and the write instruction is not taught.`), { phase: "config", sessionId });
471
+ }
472
+ writeChannelOpen =
473
+ input.writeToolsMounted &&
474
+ writeHandle.writeScope !== null &&
475
+ foreignUnderRoot.length === 0 &&
476
+ (input.advertisedWritableDirs === undefined || (await input.advertisedWritableDirs.admit({ path: writeHandle.writableRoot, advertiser: "memory", expectCanonical: writeHandle.writableRoot })));
460
477
  memoryWriteGateRef.current = async (w) => {
461
478
  if (readOnlyEngine !== undefined && readOnlyHandle !== undefined) {
462
479
  const ro = readOnlyEngine.gateWrite(readOnlyHandle, w.key, w.content);
@@ -690,56 +707,63 @@ export async function prepareMemory(input) {
690
707
  }
691
708
  }
692
709
  }
693
- let memoryBlock;
694
- let seedFiles;
695
- let recallDisciplineSegment;
696
710
  if (memorySpec && memorySpec.enabled && !useMemoryEngine) {
697
711
  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).";
698
712
  deps.onError?.(new Error(detail), { phase: "config", sessionId });
699
713
  }
700
- if (memoryEngineSession) {
701
- const injection = memoryEngineSession.inject();
702
- let blockBody = injection.block;
703
- if (captureOptedOut) {
704
- blockBody = [MEMORY_CAPTURE_OPTOUT_NOTICE, injection.index, injection.announceBlock].filter((s) => Boolean(s && s.trim())).join("\n\n");
705
- }
706
- else if (captureIndeterminate && input.rosterCanPersist) {
707
- blockBody = [MEMORY_CAPTURE_INDETERMINATE_NOTICE, injection.index, injection.announceBlock].filter((s) => Boolean(s && s.trim())).join("\n\n");
708
- }
709
- else if (!input.rosterCanPersist && injection.instruction === "" && injection.readOnlyNotice === undefined) {
710
- blockBody = blockBody.trim() ? `${MEMORY_READONLY_NOTICE}\n\n${blockBody}` : MEMORY_READONLY_NOTICE;
711
- }
712
- else if (!input.rosterCanPersist && injection.instruction !== "") {
713
- blockBody = [MEMORY_READONLY_NOTICE, injection.index, injection.announceBlock].filter((s) => Boolean(s && s.trim())).join("\n\n");
714
- }
715
- else if (input.memoryPersistenceDeclared === true && injection.readOnlyNotice !== undefined) {
716
- blockBody = [injection.index, injection.announceBlock].filter((s) => Boolean(s && s.trim())).join("\n\n");
717
- }
718
- if (!input.rosterCanPersist &&
719
- (injection.announceBlock?.trim() ?? "") !== "" &&
720
- blockBody.includes(injection.announceBlock) &&
721
- !blockBody.trimEnd().endsWith(MEMORY_ANNOUNCEMENT_READONLY_CODA)) {
722
- blockBody = `${blockBody}\n\n${MEMORY_ANNOUNCEMENT_READONLY_CODA}`;
723
- }
724
- if (captureOptedOut &&
725
- input.rosterCanPersist &&
726
- (injection.announceBlock?.trim() ?? "") !== "" &&
727
- blockBody.includes(injection.announceBlock) &&
728
- !blockBody.trimEnd().endsWith(MEMORY_ANNOUNCEMENT_CAPTURE_OPTOUT_CODA)) {
729
- blockBody = `${blockBody}\n\n${MEMORY_ANNOUNCEMENT_CAPTURE_OPTOUT_CODA}`;
730
- }
731
- if (blockBody.trim())
732
- memoryBlock = blockBody;
733
- if (memoryTools !== undefined) {
734
- recallDisciplineSegment = memoryRecallDisciplineSegment(input.memoryProvenance, { markedEntriesServed: memoryMarkedEntriesServed });
735
- memoryBlock = memoryBlock !== undefined ? `${memoryBlock}\n\n${recallDisciplineSegment}` : recallDisciplineSegment;
736
- }
737
- if (memoryEngineSession.handle.writeScope !== null && input.writeToolsMounted && input.rosterCanPersist && !captureOptedOut && !captureIndeterminate) {
738
- memoryBlock = memoryBlock !== undefined ? `${memoryBlock}\n\n${MEMORY_PREFERENCE_DISCIPLINE}` : MEMORY_PREFERENCE_DISCIPLINE;
714
+ const composeInjection = (facts) => {
715
+ let memoryBlock;
716
+ let seedFiles;
717
+ let recallDisciplineSegment;
718
+ if (memoryEngineSession) {
719
+ const injection = memoryEngineSession.inject();
720
+ let blockBody = injection.block;
721
+ if (captureOptedOut) {
722
+ blockBody = [MEMORY_CAPTURE_OPTOUT_NOTICE, injection.index, injection.announceBlock].filter((s) => Boolean(s && s.trim())).join("\n\n");
723
+ }
724
+ else if (captureIndeterminate && facts.rosterCanPersist) {
725
+ blockBody = [MEMORY_CAPTURE_INDETERMINATE_NOTICE, injection.index, injection.announceBlock].filter((s) => Boolean(s && s.trim())).join("\n\n");
726
+ }
727
+ else if (!facts.rosterCanPersist && injection.instruction === "" && injection.readOnlyNotice === undefined) {
728
+ blockBody = blockBody.trim() ? `${MEMORY_READONLY_NOTICE}\n\n${blockBody}` : MEMORY_READONLY_NOTICE;
729
+ }
730
+ else if (!facts.rosterCanPersist && injection.instruction !== "") {
731
+ blockBody = [MEMORY_READONLY_NOTICE, injection.index, injection.announceBlock].filter((s) => Boolean(s && s.trim())).join("\n\n");
732
+ }
733
+ else if (input.memoryPersistenceDeclared === true && injection.readOnlyNotice !== undefined) {
734
+ blockBody = [injection.index, injection.announceBlock].filter((s) => Boolean(s && s.trim())).join("\n\n");
735
+ }
736
+ if (!facts.rosterCanPersist &&
737
+ (injection.announceBlock?.trim() ?? "") !== "" &&
738
+ blockBody.includes(injection.announceBlock) &&
739
+ !blockBody.trimEnd().endsWith(MEMORY_ANNOUNCEMENT_READONLY_CODA)) {
740
+ blockBody = `${blockBody}\n\n${MEMORY_ANNOUNCEMENT_READONLY_CODA}`;
741
+ }
742
+ if (captureOptedOut &&
743
+ facts.rosterCanPersist &&
744
+ (injection.announceBlock?.trim() ?? "") !== "" &&
745
+ blockBody.includes(injection.announceBlock) &&
746
+ !blockBody.trimEnd().endsWith(MEMORY_ANNOUNCEMENT_CAPTURE_OPTOUT_CODA)) {
747
+ blockBody = `${blockBody}\n\n${MEMORY_ANNOUNCEMENT_CAPTURE_OPTOUT_CODA}`;
748
+ }
749
+ if (blockBody.trim())
750
+ memoryBlock = blockBody;
751
+ if (memoryTools !== undefined) {
752
+ recallDisciplineSegment = memoryRecallDisciplineSegment(input.memoryProvenance, { markedEntriesServed: memoryMarkedEntriesServed });
753
+ memoryBlock = memoryBlock !== undefined ? `${memoryBlock}\n\n${recallDisciplineSegment}` : recallDisciplineSegment;
754
+ }
755
+ if (memoryEngineSession.handle.writeScope !== null && writeChannelOpen && facts.rosterCanPersist && !captureOptedOut && !captureIndeterminate) {
756
+ memoryBlock = memoryBlock !== undefined ? `${memoryBlock}\n\n${MEMORY_PREFERENCE_DISCIPLINE}` : MEMORY_PREFERENCE_DISCIPLINE;
757
+ }
758
+ if (memoryBlock !== undefined && injection.indexSeed !== undefined && facts.rosterCanPersist && !captureOptedOut && !captureIndeterminate)
759
+ seedFiles = [injection.indexSeed];
739
760
  }
740
- if (memoryBlock !== undefined && injection.indexSeed !== undefined && input.rosterCanPersist && !captureOptedOut && !captureIndeterminate)
741
- seedFiles = [injection.indexSeed];
742
- }
761
+ return {
762
+ memoryBlock,
763
+ ...(recallDisciplineSegment !== undefined ? { recallDisciplineSegment } : {}),
764
+ ...(seedFiles !== undefined ? { seedFiles } : {}),
765
+ };
766
+ };
743
767
  if (effectiveMemoryScopes === undefined) {
744
768
  effectiveMemoryScopes = memorySpec
745
769
  ? memorySpec.enabled
@@ -749,12 +773,10 @@ export async function prepareMemory(input) {
749
773
  }
750
774
  return {
751
775
  memoryEngineSession,
752
- memoryBlock,
753
776
  admittedOrgScopes,
754
777
  ownOrgVerdict,
755
778
  effectiveMemoryScopes,
756
779
  ...(memoryTools !== undefined ? { memoryTools } : {}),
757
- ...(recallDisciplineSegment !== undefined ? { recallDisciplineSegment } : {}),
758
- ...(seedFiles !== undefined ? { seedFiles } : {}),
780
+ composeInjection,
759
781
  };
760
782
  }