@sema-agent/core 7.15.0 → 7.17.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 (287) hide show
  1. package/CHANGELOG.md +202 -0
  2. package/dist/agents/builtin-agents.d.ts +3 -3
  3. package/dist/agents/cross-session-envelope.d.ts +0 -1
  4. package/dist/agents/cross-session-envelope.js +0 -1
  5. package/dist/agents/peer-notices.d.ts +0 -9
  6. package/dist/agents/peer-notices.js +0 -63
  7. package/dist/agents/subagent.js +7 -15
  8. package/dist/brain/request-params.d.ts +2 -2
  9. package/dist/config/defaults.d.ts +0 -7
  10. package/dist/config/defaults.js +0 -2
  11. package/dist/core/artifact-host.d.ts +218 -0
  12. package/dist/core/artifact-host.js +92 -0
  13. package/dist/core/ask-origin.d.ts +29 -3
  14. package/dist/core/ask-origin.js +17 -9
  15. package/dist/core/ask-question.d.ts +25 -0
  16. package/dist/core/ask-question.js +4 -0
  17. package/dist/core/ask-unresolvable-notice.d.ts +30 -21
  18. package/dist/core/ask-unresolvable-notice.js +16 -8
  19. package/dist/core/auto-compaction.d.ts +10 -10
  20. package/dist/core/auto-mode-arming.d.ts +2 -2
  21. package/dist/core/background-agent-store.d.ts +5 -5
  22. package/dist/core/background-shell.d.ts +1 -1
  23. package/dist/core/checkpoint-execution-record.d.ts +0 -3
  24. package/dist/core/checkpoint-execution-record.js +0 -4
  25. package/dist/core/checkpoint-store.d.ts +71 -99
  26. package/dist/core/checkpoint-store.js +9 -16
  27. package/dist/core/context-edit.d.ts +36 -104
  28. package/dist/core/context-edit.js +37 -60
  29. package/dist/core/effective-path-target.d.ts +68 -8
  30. package/dist/core/effective-path-target.js +42 -30
  31. package/dist/core/engine-notice.d.ts +46 -32
  32. package/dist/core/env-budget.d.ts +31 -0
  33. package/dist/core/env-budget.js +39 -0
  34. package/dist/core/exec-output-tail.d.ts +5 -5
  35. package/dist/core/file-history-store.js +3 -6
  36. package/dist/core/fs-write-gate-policy.d.ts +1 -1
  37. package/dist/core/fs-write-gate-policy.js +3 -3
  38. package/dist/core/gate-lanes.js +9 -15
  39. package/dist/core/governance-codes.d.ts +1 -1
  40. package/dist/core/governance-codes.js +4 -0
  41. package/dist/core/hands-band.d.ts +1 -1
  42. package/dist/core/hooks.d.ts +18 -17
  43. package/dist/core/lsp-session.d.ts +1 -1
  44. package/dist/core/lsp.d.ts +1 -1
  45. package/dist/core/lsp.js +5 -4
  46. package/dist/core/mcp.d.ts +5 -5
  47. package/dist/core/mcp.js +4 -48
  48. package/dist/core/memory-engine/delegation-settlement.d.ts +37 -12
  49. package/dist/core/memory-engine/delegation-settlement.js +128 -39
  50. package/dist/core/memory-engine/engine.d.ts +9 -8
  51. package/dist/core/memory-engine/engine.js +115 -120
  52. package/dist/core/memory-engine/file-backend.d.ts +17 -5
  53. package/dist/core/memory-engine/file-backend.js +78 -34
  54. package/dist/core/memory-engine/index.d.ts +2 -2
  55. package/dist/core/memory-engine/index.js +2 -2
  56. package/dist/core/memory-engine/layout.d.ts +152 -54
  57. package/dist/core/memory-engine/layout.js +441 -104
  58. package/dist/core/memory-engine/types.d.ts +8 -3
  59. package/dist/core/memory-engine/types.js +5 -0
  60. package/dist/core/memory-recall.d.ts +0 -8
  61. package/dist/core/memory.d.ts +1 -1
  62. package/dist/core/oracle-isolation.js +2 -2
  63. package/dist/core/parent-spec-seat.d.ts +66 -0
  64. package/dist/core/parent-spec-seat.js +47 -0
  65. package/dist/core/permission-rule-model.d.ts +11 -6
  66. package/dist/core/permission-rule-model.js +53 -37
  67. package/dist/core/permission-rules.js +8 -3
  68. package/dist/core/protocol-naming.d.ts +2 -2
  69. package/dist/core/remote-env.d.ts +1 -1
  70. package/dist/core/retired-keys.d.ts +19 -0
  71. package/dist/core/retired-keys.js +15 -0
  72. package/dist/core/runner/abort-race.d.ts +3 -3
  73. package/dist/core/runner/active-skill-scope.d.ts +2 -2
  74. package/dist/core/runner/active-skill-scope.js +3 -3
  75. package/dist/core/runner/assemble-result.d.ts +42 -40
  76. package/dist/core/runner/assemble-result.js +5 -4
  77. package/dist/core/runner/checkpoint-scope.d.ts +4 -4
  78. package/dist/core/runner/clock-and-limits.d.ts +3 -3
  79. package/dist/core/runner/compaction-call-options.d.ts +35 -99
  80. package/dist/core/runner/compaction-knobs.d.ts +4 -4
  81. package/dist/core/runner/compaction-seams.d.ts +5 -5
  82. package/dist/core/runner/contracts.d.ts +232 -233
  83. package/dist/core/runner/decide-continuation.d.ts +6 -6
  84. package/dist/core/runner/denial-limit-arms.d.ts +8 -8
  85. package/dist/core/runner/derived-route-fallback.d.ts +2 -2
  86. package/dist/core/runner/gate-exit.d.ts +6 -6
  87. package/dist/core/runner/gate-exit.js +15 -1
  88. package/dist/core/runner/git-status-frame.d.ts +7 -7
  89. package/dist/core/runner/halt-attribution.d.ts +35 -0
  90. package/dist/core/runner/halt-attribution.js +16 -0
  91. package/dist/core/runner/inherited-ask-grants.d.ts +6 -6
  92. package/dist/core/runner/memory-capture-optout.d.ts +1 -1
  93. package/dist/core/runner/memory-consolidation-driver.d.ts +4 -4
  94. package/dist/core/runner/memory-consolidation.d.ts +6 -8
  95. package/dist/core/runner/memory-consolidation.js +0 -1
  96. package/dist/core/runner/park-commit.d.ts +6 -6
  97. package/dist/core/runner/permission-rule-lanes.d.ts +19 -19
  98. package/dist/core/runner/prepare-acquire-reconcile.d.ts +4 -4
  99. package/dist/core/runner/prepare-artifact.d.ts +53 -0
  100. package/dist/core/runner/prepare-artifact.js +77 -0
  101. package/dist/core/runner/prepare-ask-lane.d.ts +3 -3
  102. package/dist/core/runner/prepare-boundary-parks.d.ts +3 -3
  103. package/dist/core/runner/prepare-caps-and-workflow.d.ts +9 -7
  104. package/dist/core/runner/prepare-caps-and-workflow.js +2 -3
  105. package/dist/core/runner/prepare-config-doors.d.ts +24 -24
  106. package/dist/core/runner/prepare-config-doors.js +5 -2
  107. package/dist/core/runner/prepare-context-lane.d.ts +8 -8
  108. package/dist/core/runner/prepare-context-lane.js +2 -13
  109. package/dist/core/runner/prepare-defer-classify.d.ts +2 -2
  110. package/dist/core/runner/prepare-delegation-surface.d.ts +1 -1
  111. package/dist/core/runner/prepare-file-history.d.ts +3 -3
  112. package/dist/core/runner/prepare-file-history.js +3 -52
  113. package/dist/core/runner/prepare-gate-stations.d.ts +5 -5
  114. package/dist/core/runner/prepare-hands-readface.d.ts +35 -38
  115. package/dist/core/runner/prepare-hands-readface.js +3 -2
  116. package/dist/core/runner/prepare-inherited-gate.d.ts +9 -6
  117. package/dist/core/runner/prepare-inherited-gate.js +7 -8
  118. package/dist/core/runner/prepare-listings.d.ts +3 -3
  119. package/dist/core/runner/prepare-lsp.d.ts +3 -3
  120. package/dist/core/runner/prepare-memory-engine-session.d.ts +5 -5
  121. package/dist/core/runner/prepare-memory.d.ts +4 -6
  122. package/dist/core/runner/prepare-memory.js +10 -9
  123. package/dist/core/runner/prepare-offload-wrappers.d.ts +2 -2
  124. package/dist/core/runner/prepare-park-ask.d.ts +6 -6
  125. package/dist/core/runner/prepare-policy-chain.d.ts +7 -7
  126. package/dist/core/runner/prepare-policy-chain.js +15 -30
  127. package/dist/core/runner/prepare-prompt-assembly.js +1 -1
  128. package/dist/core/runner/prepare-prompt-inputs.d.ts +5 -5
  129. package/dist/core/runner/prepare-protocol-tools.d.ts +1 -1
  130. package/dist/core/runner/prepare-question-face.d.ts +4 -4
  131. package/dist/core/runner/prepare-run-refs.d.ts +11 -11
  132. package/dist/core/runner/prepare-safety-scan.d.ts +2 -2
  133. package/dist/core/runner/prepare-suspend-saga.d.ts +4 -4
  134. package/dist/core/runner/prepare-suspend-saga.js +0 -2
  135. package/dist/core/runner/prepare-task.d.ts +12 -12
  136. package/dist/core/runner/prepare-task.js +10 -9
  137. package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +2 -3
  138. package/dist/core/runner/prepare-wiring-manifest.d.ts +4 -4
  139. package/dist/core/runner/prepare-wiring-manifest.js +9 -7
  140. package/dist/core/runner/prepare-workspace-restore.d.ts +1 -1
  141. package/dist/core/runner/remote-env-retry.d.ts +3 -3
  142. package/dist/core/runner/resume-admission.d.ts +1 -1
  143. package/dist/core/runner/resume-admission.js +2 -1
  144. package/dist/core/runner/resume-claim.d.ts +1 -1
  145. package/dist/core/runner/resume-internals-and-config.d.ts +1 -1
  146. package/dist/core/runner/resume-internals-and-config.js +4 -0
  147. package/dist/core/runner/resume-policy-outcome.d.ts +1 -1
  148. package/dist/core/runner/resume-preflight.d.ts +1 -1
  149. package/dist/core/runner/resume-review-outcome.d.ts +1 -1
  150. package/dist/core/runner/retired-runner-deps.d.ts +3 -0
  151. package/dist/core/runner/retired-runner-deps.js +7 -0
  152. package/dist/core/runner/run-brain-sinks.d.ts +1 -1
  153. package/dist/core/runner/run-clock-and-content.d.ts +4 -4
  154. package/dist/core/runner/run-compaction-machinery.d.ts +3 -3
  155. package/dist/core/runner/run-git-lane.d.ts +1 -1
  156. package/dist/core/runner/run-harness-handlers.d.ts +2 -2
  157. package/dist/core/runner/run-harness-handlers.js +2 -3
  158. package/dist/core/runner/run-identity-wiring.d.ts +4 -4
  159. package/dist/core/runner/run-leg.d.ts +3 -3
  160. package/dist/core/runner/run-notification-lane.d.ts +2 -2
  161. package/dist/core/runner/run-reasoning-seat.d.ts +2 -2
  162. package/dist/core/runner/run-settle-and-teardown.d.ts +5 -5
  163. package/dist/core/runner/run-settle-and-teardown.js +3 -1
  164. package/dist/core/runner/run-telemetry-and-budget-seats.d.ts +2 -2
  165. package/dist/core/runner/run-terminal-adoption.d.ts +5 -5
  166. package/dist/core/runner/runtask.d.ts +14 -14
  167. package/dist/core/runner/session-file-state-replay.d.ts +3 -3
  168. package/dist/core/runner/session-file-state-replay.js +3 -3
  169. package/dist/core/runner/session-rule-policy.d.ts +2 -2
  170. package/dist/core/runner/session-rule-policy.js +3 -4
  171. package/dist/core/runner/steer-admission.d.ts +2 -2
  172. package/dist/core/runner/stream-halt-verbs.d.ts +1 -1
  173. package/dist/core/runner/stream-halt-verbs.js +1 -1
  174. package/dist/core/runner/stream-reap.d.ts +1 -1
  175. package/dist/core/runner/stream-steer-verb.js +1 -4
  176. package/dist/core/runner/synthetic-tools.d.ts +8 -8
  177. package/dist/core/runner/teardown-bounded.d.ts +2 -2
  178. package/dist/core/runner/tool-defer-gate.d.ts +1 -1
  179. package/dist/core/runner/tool-disclosure.d.ts +33 -33
  180. package/dist/core/runner/tool-end-body.d.ts +3 -3
  181. package/dist/core/runner/tool-face-overlay.d.ts +2 -2
  182. package/dist/core/runner/tool-output-projection.d.ts +2 -2
  183. package/dist/core/runner/turn-attachments.d.ts +53 -53
  184. package/dist/core/runner/usage-accounting.d.ts +4 -4
  185. package/dist/core/runner/workspace-path.d.ts +1 -1
  186. package/dist/core/runner/workspace-path.js +3 -0
  187. package/dist/core/runner-deps.d.ts +30 -49
  188. package/dist/core/safe-notify.d.ts +1 -1
  189. package/dist/core/sensitive-path-policy.d.ts +1 -1
  190. package/dist/core/sensitive-path-policy.js +4 -4
  191. package/dist/core/session.d.ts +2 -2
  192. package/dist/core/skill-tool-specifier.d.ts +1 -1
  193. package/dist/core/skills-directory.d.ts +1 -1
  194. package/dist/core/store-contracts/checkpoint-store-contract.js +1 -1
  195. package/dist/core/strategy-store.d.ts +8 -11
  196. package/dist/core/task-event.d.ts +20 -17
  197. package/dist/core/task-limits.d.ts +1 -1
  198. package/dist/core/task-notification.d.ts +5 -5
  199. package/dist/core/task-registry-agent.d.ts +5 -5
  200. package/dist/core/task-registry-monitor.d.ts +2 -2
  201. package/dist/core/task-registry-shared.d.ts +27 -27
  202. package/dist/core/task-registry-shared.js +3 -13
  203. package/dist/core/task-registry.d.ts +8 -8
  204. package/dist/core/task-result.d.ts +37 -37
  205. package/dist/core/task-spec.d.ts +5 -5
  206. package/dist/core/task-stream.d.ts +16 -11
  207. package/dist/core/tool-catalog-entries.d.ts +1 -0
  208. package/dist/core/tool-catalog-entries.js +5 -0
  209. package/dist/core/tool-conformance.d.ts +0 -2
  210. package/dist/core/tool-conformance.js +1 -3
  211. package/dist/core/tool-errors.d.ts +5 -0
  212. package/dist/core/tool-errors.js +1 -0
  213. package/dist/core/tool-policy.d.ts +51 -22
  214. package/dist/core/tool-policy.js +28 -5
  215. package/dist/core/tool-result-budget.d.ts +1 -1
  216. package/dist/core/tool-result-store.d.ts +11 -11
  217. package/dist/core/tool-spec.d.ts +34 -38
  218. package/dist/core/tools.d.ts +2 -2
  219. package/dist/core/trace.d.ts +12 -15
  220. package/dist/core/untrusted-text.d.ts +1 -1
  221. package/dist/core/wiring-manifest.d.ts +4 -3
  222. package/dist/core/workflow-journal-store.d.ts +11 -11
  223. package/dist/engine/execution-env/node-execution-env.d.ts +3 -2
  224. package/dist/engine/execution-env/node-execution-env.js +4 -2
  225. package/dist/engine/harness/agent-harness.d.ts +47 -4
  226. package/dist/engine/harness/agent-harness.js +52 -9
  227. package/dist/engine/llm/types.d.ts +1 -1
  228. package/dist/engine/loop/agent-loop.js +15 -8
  229. package/dist/engine/loop/types.d.ts +5 -0
  230. package/dist/index.d.ts +21 -16
  231. package/dist/index.js +17 -12
  232. package/dist/internal/harness.d.ts +1 -0
  233. package/dist/orchestration/run-spec.d.ts +3 -0
  234. package/dist/orchestration/run-spec.js +3 -3
  235. package/dist/orchestration/run-workflow-tool.d.ts +12 -6
  236. package/dist/orchestration/run-workflow-tool.js +23 -5
  237. package/dist/orchestration/workflow-primitives.d.ts +4 -2
  238. package/dist/orchestration/workflow-primitives.js +1 -4
  239. package/dist/orchestration/workflow-types.d.ts +34 -4
  240. package/dist/orchestration/workflow-types.js +2 -0
  241. package/dist/orchestration/workflow.d.ts +39 -7
  242. package/dist/orchestration/workflow.js +234 -113
  243. package/dist/prompt-assembly/assemble.d.ts +2 -2
  244. package/dist/prompt-assembly/assemble.js +9 -9
  245. package/dist/prompt-assembly/composer.d.ts +6 -7
  246. package/dist/prompt-assembly/composer.js +10 -10
  247. package/dist/prompt-assembly/epoch.js +1 -1
  248. package/dist/prompt-assembly/event-registry.d.ts +0 -21
  249. package/dist/prompt-assembly/packs/sema-default.js +30 -30
  250. package/dist/prompt-assembly/types.d.ts +14 -14
  251. package/dist/prompts/coordinator.d.ts +0 -13
  252. package/dist/prompts/coordinator.js +0 -3
  253. package/dist/stores/file/checkpoint-store.js +1 -5
  254. package/dist/stores/file/file-history-store.js +2 -2
  255. package/dist/stores/file/permission-rule-store.d.ts +4 -35
  256. package/dist/stores/file/permission-rule-store.js +4 -148
  257. package/dist/tools/artifact/artifact-text.d.ts +62 -0
  258. package/dist/tools/artifact/artifact-text.js +68 -0
  259. package/dist/tools/artifact/artifact-tool.d.ts +52 -0
  260. package/dist/tools/artifact/artifact-tool.js +351 -0
  261. package/dist/tools/artifact/local-stub.d.ts +26 -0
  262. package/dist/tools/artifact/local-stub.js +296 -0
  263. package/dist/tools/fs/bash-readonly-classifier.d.ts +114 -48
  264. package/dist/tools/fs/bash-readonly-classifier.js +171 -60
  265. package/dist/tools/fs/encoding.d.ts +31 -40
  266. package/dist/tools/fs/fs-bash.d.ts +49 -45
  267. package/dist/tools/fs/fs-bash.js +19 -27
  268. package/dist/tools/fs/fs-pdf.d.ts +8 -8
  269. package/dist/tools/fs/fs-search-tools.d.ts +4 -4
  270. package/dist/tools/fs/fs-search-tools.js +5 -2
  271. package/dist/tools/fs/fs-shared.d.ts +146 -158
  272. package/dist/tools/fs/fs-shared.js +3 -11
  273. package/dist/tools/fs/fs-write.d.ts +8 -8
  274. package/dist/tools/fs/gh-rate-limit.d.ts +13 -14
  275. package/dist/tools/fs/index.d.ts +78 -78
  276. package/dist/tools/fs/index.js +4 -1
  277. package/dist/tools/fs/notebook.d.ts +24 -25
  278. package/dist/tools/fs/pdf.d.ts +3 -3
  279. package/dist/tools/fs/read-deny.d.ts +22 -22
  280. package/dist/tools/fs/read-face.d.ts +21 -24
  281. package/dist/tools/fs/repo-map.d.ts +8 -8
  282. package/dist/tools/fs/safety.d.ts +233 -130
  283. package/dist/tools/fs/safety.js +131 -31
  284. package/dist/tools/fs/search.d.ts +63 -71
  285. package/dist/tools/fs/search.js +8 -6
  286. package/package.json +6 -2
  287. package/test/export-surface.snapshot.json +119 -39
@@ -168,7 +168,7 @@ export interface MaterializedMcp {
168
168
  * A consumer (service) surfacing it as session status must understand that. `toolNames` are the namespaced
169
169
  * (`<server>__<tool>`) names exposed.
170
170
  *
171
- * RB-437-a — READ THIS AS A SNAPSHOT, NOT A HEARTBEAT. `status` is the connect-time verdict and is never
171
+ * READ THIS AS A SNAPSHOT, NOT A HEARTBEAT. `status` is the connect-time verdict and is never
172
172
  * rewritten: a server that dies mid-task still reads `connected` here, because that IS what happened at
173
173
  * materialize. The one live bit is {@link McpServerStatus.transportClosed} (set if and when core observes
174
174
  * the transport go away — see its own note for the reach). For an authoritative answer about a server's
@@ -231,7 +231,7 @@ export interface McpRefreshResult {
231
231
  */
232
232
  error?: string;
233
233
  /**
234
- * #381 — present when this entry's `tools/list` walk did NOT reach the end of pagination. `tools` is
234
+ * Present when this entry's `tools/list` walk did NOT reach the end of pagination. `tools` is
235
235
  * then a MERGE: everything the partial listing did return, plus the previously-mounted entries whose
236
236
  * absence it cannot prove (so the consumer's replace-the-whole-prefix swap withdraws nothing, and
237
237
  * `removed: []` describes a roster that really lost nothing). Absent on a complete walk and on every
@@ -267,7 +267,7 @@ export interface McpServerStatus {
267
267
  delivered?: McpDelivered;
268
268
  httpStatus?: number;
269
269
  /**
270
- * RB-437-a — set to `true` once core OBSERVES this server's transport close during the task; absent
270
+ * Set to `true` once core OBSERVES this server's transport close during the task; absent
271
271
  * otherwise (never `false`: absence means "no close was observed", which is not the same as "alive").
272
272
  * It mirrors exactly the flag core's own fail-fast path keys on, so `transportClosed === true` also
273
273
  * means every further call to this server's tools/resources is refused without dialing.
@@ -376,7 +376,7 @@ export declare function mcpStartupTimeoutMs(): number | undefined;
376
376
  * (see {@link MCP_SPEC_ERROR_CODE_NAMES}). */
377
377
  export declare function describeMcpSpecErrorCode(code: unknown): string | undefined;
378
378
  /**
379
- * RB-437-c — the SDK stamps `MCP error <code>: ` onto an McpError's message IN ITS CONSTRUCTOR, and it
379
+ * The SDK stamps `MCP error <code>: ` onto an McpError's message IN ITS CONSTRUCTOR, and it
380
380
  * does so at BOTH ends of the wire: a server built on an MCP SDK throws an McpError, its already-stamped
381
381
  * `.message` goes out as the JSON-RPC `error.message`, and the client constructs its own McpError over
382
382
  * that string — so the failure reaches the model as `MCP error -32602: MCP error -32602: …`. Collapse a
@@ -582,7 +582,7 @@ export declare function classifyDirReadInvalidParams(message: string): "not_foun
582
582
  */
583
583
  export declare function listEntryFingerprint(entry: unknown): string | undefined;
584
584
  /**
585
- * #381 — walk a paginated MCP list method to the END (`{cursor?}` → `{items, nextCursor?}`), bounded.
585
+ * Walk a paginated MCP list method to the END (`{cursor?}` → `{items, nextCursor?}`), bounded.
586
586
  *
587
587
  * Every spec-standard MCP list method is paginated the same way: absence of `nextCursor` means "that
588
588
  * was the last page". A single request therefore returns the WHOLE list only for servers that choose
package/dist/core/mcp.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { redactSecrets } from "./untrusted-egress.js";
2
- import { announceEnvKnob, __resetEnvKnobAnnouncements } from "./env-knob-announce.js";
2
+ import { __resetEnvKnobAnnouncements } from "./env-knob-announce.js";
3
+ import { resolveEnvBudget } from "./env-budget.js";
3
4
  import { MCP_NAMESPACE } from "./protocol-table.js";
4
5
  import { findNamespacePrefixCollision, mintNamespacePrefix, mintNamespacedToolName, normalizeNameSegment } from "./protocol-naming.js";
5
6
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
@@ -31,18 +32,7 @@ export function resolveMcpDeclaredResultSize(meta) {
31
32
  return Math.min(declared, MCP_META_RESULT_SIZE_CAP);
32
33
  }
33
34
  function mcpMaxOutputTokens() {
34
- const rawEnv = process.env.MAX_MCP_OUTPUT_TOKENS;
35
- if (rawEnv === undefined)
36
- return MCP_OUTPUT_TOKENS_DEFAULT;
37
- const raw = rawEnv.trim();
38
- if (raw === "")
39
- return MCP_OUTPUT_TOKENS_DEFAULT;
40
- const n = parseWholeNumber(raw);
41
- if (n === undefined || n <= 0) {
42
- announceEnvKnob(`MAX_MCP_OUTPUT_TOKENS=${rawEnv} was ignored — it is not a positive whole number of tokens. Using ${MCP_OUTPUT_TOKENS_DEFAULT} instead.`);
43
- return MCP_OUTPUT_TOKENS_DEFAULT;
44
- }
45
- return n;
35
+ return resolveEnvBudget(process.env.MAX_MCP_OUTPUT_TOKENS, { name: "MAX_MCP_OUTPUT_TOKENS", unit: "tokens", default: MCP_OUTPUT_TOKENS_DEFAULT });
46
36
  }
47
37
  function mcpTruncationNote(limitTokens) {
48
38
  return (`\n\n[OUTPUT TRUNCATED - exceeded ${limitTokens} token limit]\n\n` +
@@ -114,45 +104,11 @@ export function truncateMcpErrorText(s) {
114
104
  export const MCP_TOOL_TIMEOUT_DEFAULT_MS = 100_000_000;
115
105
  export const MCP_ENV_MS_MIN = 1_000;
116
106
  export const MCP_ENV_MS_MAX = 2_147_483_647;
117
- const ENV_SCIENTIFIC_RE = /^[+-]?(\d+(\.\d*)?|\.\d+)[eE][+-]?\d+$/;
118
- const ENV_GROUPED_RE = /^[+-]?\d{1,3}([_,\u00A0\u202F ])\d{3}(?:\1\d{3})*$/;
119
- const ENV_GROUP_SEPARATORS_RE = /[_,\u00A0\u202F ]/g;
120
- const ENV_NUMERIC_MAX_LEN = 32;
121
- function parseWholeNumber(raw) {
122
- if (/^[+-]?\d+$/.test(raw)) {
123
- const n = Number(raw);
124
- return Number.isSafeInteger(n) ? n : undefined;
125
- }
126
- if (raw.length > ENV_NUMERIC_MAX_LEN)
127
- return undefined;
128
- if (ENV_SCIENTIFIC_RE.test(raw)) {
129
- const n = Number(raw);
130
- return Number.isSafeInteger(n) ? n : undefined;
131
- }
132
- if (ENV_GROUPED_RE.test(raw))
133
- return parseInt(raw.replace(ENV_GROUP_SEPARATORS_RE, ""), 10);
134
- return undefined;
135
- }
136
107
  export function __resetMcpEnvAnnouncements() {
137
108
  __resetEnvKnobAnnouncements();
138
109
  }
139
110
  function parseEnvMs(name) {
140
- const rawEnv = process.env[name];
141
- if (rawEnv === undefined)
142
- return undefined;
143
- const raw = rawEnv.trim();
144
- if (raw === "")
145
- return undefined;
146
- const n = parseWholeNumber(raw);
147
- if (n === undefined || n <= 0) {
148
- announceEnvKnob(`${name}=${rawEnv} was ignored — it is not a positive whole number of milliseconds. Using the built-in default instead.`);
149
- return undefined;
150
- }
151
- const clamped = Math.min(Math.max(n, MCP_ENV_MS_MIN), MCP_ENV_MS_MAX);
152
- if (clamped !== n) {
153
- announceEnvKnob(`${name}=${rawEnv} is outside the range this runtime can honor (${MCP_ENV_MS_MIN}..${MCP_ENV_MS_MAX} ms). Using ${clamped}ms instead.`);
154
- }
155
- return clamped;
111
+ return resolveEnvBudget(process.env[name], { name, unit: "milliseconds", default: undefined, min: MCP_ENV_MS_MIN, max: MCP_ENV_MS_MAX });
156
112
  }
157
113
  export function mcpToolTimeoutMs() {
158
114
  return parseEnvMs("MCP_TOOL_TIMEOUT") ?? MCP_TOOL_TIMEOUT_DEFAULT_MS;
@@ -144,32 +144,54 @@ export declare function replayExternalSettlementEffects(controlDir: string, inpu
144
144
  sessionId: string;
145
145
  }>;
146
146
  export declare const SESSION_ACCOUNTS_FILE = "session-accounts.json";
147
+ /** One member of the sticky set: the ledger form `(scope, rel)` — `rel` relative to the scope's
148
+ * home, `/`-separated. Location-free, so a home move never re-keys the membership. */
149
+ export interface UnattributedMember {
150
+ scope: string;
151
+ rel: string;
152
+ }
147
153
  export interface SessionAccountRow {
148
154
  sessionId: string;
149
155
  openedAt: number;
150
- /** The STICKY unattributed set (rel paths), frozen at materialize (§3.6 归属判据): pre-existing,
151
- * uncommitted files seen while a FOREIGN dangling open row stood. Membership never shrinks on
152
- * later writes (the r3-5 sticky law); it clears only when the row is re-opened with a fresh
153
- * classification (the residue was adjudicated by an intervening harvest). */
154
- unattributed: string[];
156
+ /** The STICKY unattributed set, frozen at materialize (§3.6 归属判据): pre-existing, uncommitted
157
+ * files seen while a FOREIGN dangling open row stood. Membership never shrinks on later writes
158
+ * (the sticky law); it clears only when the row is re-opened with a fresh classification (the
159
+ * residue was adjudicated by an intervening harvest). */
160
+ unattributed: UnattributedMember[];
161
+ /** The v1 members (memory-dir-relative paths) the registry could not attribute at the one-time
162
+ * conversion. Kept, never dropped; while any stands, EVERY membership question about
163
+ * this session answers "unattributed" (over-marking is the safe direction). */
164
+ unattributable?: string[];
155
165
  closedAt?: number;
156
166
  /** r7-3 — the host valve closed this row WITHOUT adjudication: it stops dangling but keeps
157
167
  * triggering the residue arm until the next FULL-domain harvest closes an account normally
158
168
  * (an unadjudicated close must not launder the residue window it covered). */
159
169
  unadjudicated?: true;
160
170
  }
171
+ /** The one-time v1 key-form conversions of the three engine sidecars, in one call (each in its own
172
+ * lock, each idempotent). The layout adoption runs this before any later scope registration can
173
+ * change what a v1 key's first segment names. */
174
+ export declare function convertV1SidecarKeys(controlDir: string): void;
161
175
  /** The dangling-trigger read: open rows (and unadjudicated closes) belonging to OTHER sessions.
162
176
  * These are what arm the unattributed residue classification at a session's materialize. */
163
177
  export declare function foreignDanglingSessionAccounts(controlDir: string, selfSessionId: string): SessionAccountRow[];
178
+ /** The membership question the harvest asks per file: is `(scope, rel)` in the session's sticky
179
+ * set? A session carrying members the conversion could not attribute answers yes for every seat. */
180
+ export interface SessionUnattributedSet {
181
+ has(scope: string, rel: string): boolean;
182
+ /** True when no seat can answer yes (no members, none unattributable). */
183
+ readonly empty: boolean;
184
+ }
185
+ export declare function emptyUnattributedSet(): SessionUnattributedSet;
164
186
  /** Open (or re-open, on a same-session resume) the session's account row, persisting the sticky
165
187
  * unattributed classification frozen at THIS materialize. */
166
188
  export declare function openSessionAccount(controlDir: string, input: {
167
189
  sessionId: string;
168
190
  now: () => number;
169
- unattributed: readonly string[];
191
+ unattributed: readonly UnattributedMember[];
170
192
  }): void;
171
193
  /** The current session's sticky unattributed set (the harvest's residue-arm input). */
172
- export declare function sessionUnattributedSet(controlDir: string, sessionId: string): Set<string>;
194
+ export declare function sessionUnattributedSet(controlDir: string, sessionId: string): SessionUnattributedSet;
173
195
  /** Close the session's account row — called ONLY after a FULL-domain harvest (zero BUDGET-deferred
174
196
  * files, i.e. `HarvestReport.degraded` absent): a partial harvest's close would launder the deferred
175
197
  * residue window (§3.6 序则②). A normal full close also clears every standing `unadjudicated` flag
@@ -209,8 +231,10 @@ export interface HoldRow {
209
231
  op: "add" | "update";
210
232
  /** CAS anchor at capture time (update form): release refuses to blind-write over a later edit. */
211
233
  baseRev?: string;
212
- /** Path relative to the memory dir (the plane seat the file was removed from). */
213
- relPath: string;
234
+ /** The plane seat the file was removed from, in the ledger form: relative to the write scope's
235
+ * home (`<memoryDir>/<label>`), `/`-separated — `${slug}.md`. Location-free, so a home move never
236
+ * re-keys a hold. */
237
+ rel: string;
214
238
  /** The entry slug (path relative to the scope dir, sans `.md`) — the release leg's projection
215
239
  * identity and its filename-scan input. */
216
240
  slug: string;
@@ -263,7 +287,8 @@ export declare function noReplaceRestore(stagingPath: string, destPath: string):
263
287
  */
264
288
  export declare function openInstructionHold(controlDir: string, input: {
265
289
  sessionId: string;
266
- relPath: string;
290
+ /** The seat in the ledger form (relative to the write scope's home). */
291
+ rel: string;
267
292
  absPath: string;
268
293
  content: string;
269
294
  entryId?: string;
@@ -290,11 +315,11 @@ export declare function openInstructionHold(controlDir: string, input: {
290
315
  * (the file stays on the plane and walks the ordinary/§3.6 arms; never "capture whatever is
291
316
  * there now" — that would hold a third writer's bytes under the first writer's account);
292
317
  * - `capturing` + custody absent + plane file gone ⇒ `capture_lost` (nothing to hold).
293
- * - `held` + plane file REAPPEARED at the relPath ⇒ left alone (a fresh write is a fresh file —
318
+ * - `held` + plane file REAPPEARED at the seat ⇒ left alone (a fresh write is a fresh file —
294
319
  * the next harvest judges it on its own; the hold's custody bytes stay the hold's).
295
320
  */
296
321
  export declare function reconcileHolds(controlDir: string, input: {
297
- memoryDir: string;
322
+ homeOf: (scope: string) => string;
298
323
  now: () => number;
299
324
  }): void;
300
325
  /** Dispose one held row: custody bytes move to the quarantine directory (host-auditable), the row
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { existsSync, linkSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
- import { QUARANTINE_DIR, appendChallengeEvents, enqueueMemoryAnnouncement, ensureDirExists, lineageContributionsOfSession, lockedStrictUpdate, markSessionPolluted, readSessionPollution, readStrictSidecar, ControlPlaneCorruptError } from "./layout.js";
4
+ import { QUARANTINE_DIR, appendChallengeEvents, enqueueMemoryAnnouncement, ensureDirExists, v1MemoryPathKeyResolver, lineageContributionsOfSession, lockedStrictUpdate, markSessionPolluted, readProjectionDebts, readSessionPollution, readStrictSidecar, ControlPlaneCorruptError } from "./layout.js";
5
5
  export const DELEGATION_SETTLEMENT_FILE = "delegation-settlement.json";
6
6
  function reqStr(v) {
7
7
  return typeof v === "string" && v.length > 0;
@@ -233,36 +233,101 @@ export function replayExternalSettlementEffects(controlDir, input) {
233
233
  return applied;
234
234
  }
235
235
  export const SESSION_ACCOUNTS_FILE = "session-accounts.json";
236
+ function malformedAccountRow(row) {
237
+ return (typeof row !== "object" ||
238
+ row === null ||
239
+ !reqStr(row.sessionId) ||
240
+ typeof row.openedAt !== "number" ||
241
+ !Number.isFinite(row.openedAt) ||
242
+ !Array.isArray(row.unattributed) ||
243
+ (row.closedAt !== undefined && (typeof row.closedAt !== "number" || !Number.isFinite(row.closedAt))) ||
244
+ (row.unadjudicated !== undefined && row.unadjudicated !== true));
245
+ }
246
+ function isUnattributedMember(x) {
247
+ return typeof x === "object" && x !== null && reqStr(x.scope) && safeRelPath(x.rel);
248
+ }
236
249
  function coerceSessionAccounts(raw) {
237
250
  if (raw === undefined)
238
- return { version: 1, rows: [] };
251
+ return { version: 2, rows: [] };
239
252
  const rec = raw;
240
253
  if (typeof rec !== "object" || rec === null || !Array.isArray(rec.rows))
241
254
  throw new ControlPlaneCorruptError("memory session-account ledger has the wrong shape");
242
- if (rec.version !== 1)
255
+ if (rec.version !== 1 && rec.version !== 2)
243
256
  throw new ControlPlaneCorruptError(`memory session-account ledger has an unrecognized version (${String(rec.version)}) — refusing (fail-closed)`);
244
- for (const r of rec.rows) {
257
+ const bad = () => {
258
+ throw new ControlPlaneCorruptError("memory session-account ledger: a row is malformed — refusing (fail-closed; an unevaluable row must not read as adjudicated)");
259
+ };
260
+ const rows = rec.rows;
261
+ for (const r of rows) {
245
262
  const row = r;
246
- if (typeof row !== "object" ||
247
- row === null ||
248
- !reqStr(row.sessionId) ||
249
- typeof row.openedAt !== "number" ||
250
- !Number.isFinite(row.openedAt) ||
251
- !Array.isArray(row.unattributed) ||
252
- row.unattributed.some((x) => typeof x !== "string") ||
253
- (row.closedAt !== undefined && (typeof row.closedAt !== "number" || !Number.isFinite(row.closedAt))) ||
254
- (row.unadjudicated !== undefined && row.unadjudicated !== true)) {
255
- throw new ControlPlaneCorruptError("memory session-account ledger: a row is malformed — refusing (fail-closed; an unevaluable row must not read as adjudicated)");
263
+ if (malformedAccountRow(row))
264
+ bad();
265
+ const members = Array.isArray(row.unattributed) ? row.unattributed : bad();
266
+ if (rec.version === 1) {
267
+ if (members.some((x) => typeof x !== "string"))
268
+ bad();
256
269
  }
257
- }
258
- return rec;
270
+ else {
271
+ if (!members.every(isUnattributedMember))
272
+ bad();
273
+ if (row.unattributable !== undefined && (!Array.isArray(row.unattributable) || row.unattributable.some((x) => typeof x !== "string")))
274
+ bad();
275
+ }
276
+ }
277
+ return rec.version === 1 ? { version: 1, rows: rows } : { version: 2, rows: rows };
278
+ }
279
+ function convertSessionAccounts(read, controlDir) {
280
+ if (read.version === 2)
281
+ return { rec: read, converted: false };
282
+ const resolve = v1MemoryPathKeyResolver(controlDir);
283
+ const rows = read.rows.map((r) => {
284
+ const unattributed = [];
285
+ const unattributable = [];
286
+ for (const relPath of r.unattributed) {
287
+ const key = resolve(relPath);
288
+ if (key === undefined)
289
+ unattributable.push(relPath);
290
+ else
291
+ unattributed.push(key);
292
+ }
293
+ return {
294
+ sessionId: r.sessionId,
295
+ openedAt: r.openedAt,
296
+ unattributed,
297
+ ...(unattributable.length > 0 ? { unattributable } : {}),
298
+ ...(r.closedAt !== undefined ? { closedAt: r.closedAt } : {}),
299
+ ...(r.unadjudicated !== undefined ? { unadjudicated: r.unadjudicated } : {}),
300
+ };
301
+ });
302
+ return { rec: { version: 2, rows }, converted: true };
303
+ }
304
+ export function convertV1SidecarKeys(controlDir) {
305
+ readProjectionDebts(controlDir);
306
+ readHolds(controlDir);
307
+ readSessionAccountsConverted(controlDir);
308
+ }
309
+ function readSessionAccountsConverted(controlDir) {
310
+ const read = coerceSessionAccounts(readStrictSidecar(controlDir, SESSION_ACCOUNTS_FILE, "memory session-account ledger"));
311
+ if (read.version === 2)
312
+ return read;
313
+ return lockedStrictUpdate(controlDir, SESSION_ACCOUNTS_FILE, "memory session-account ledger", coerceSessionAccounts, (again) => {
314
+ const { rec, converted } = convertSessionAccounts(again, controlDir);
315
+ return converted ? { next: rec, result: rec } : { result: rec };
316
+ });
259
317
  }
260
318
  export function foreignDanglingSessionAccounts(controlDir, selfSessionId) {
261
- const rec = coerceSessionAccounts(readStrictSidecar(controlDir, SESSION_ACCOUNTS_FILE, "memory session-account ledger"));
319
+ const rec = readSessionAccountsConverted(controlDir);
262
320
  return rec.rows.filter((r) => r.sessionId !== selfSessionId && (r.closedAt === undefined || r.unadjudicated === true));
263
321
  }
322
+ export function emptyUnattributedSet() {
323
+ return { has: () => false, empty: true };
324
+ }
325
+ function memberKey(scope, rel) {
326
+ return JSON.stringify([scope, rel]);
327
+ }
264
328
  export function openSessionAccount(controlDir, input) {
265
- lockedStrictUpdate(controlDir, SESSION_ACCOUNTS_FILE, "memory session-account ledger", coerceSessionAccounts, (rec) => {
329
+ lockedStrictUpdate(controlDir, SESSION_ACCOUNTS_FILE, "memory session-account ledger", coerceSessionAccounts, (read) => {
330
+ const { rec } = convertSessionAccounts(read, controlDir);
266
331
  const row = rec.rows.find((r) => r.sessionId === input.sessionId);
267
332
  if (row === undefined) {
268
333
  rec.rows.push({ sessionId: input.sessionId, openedAt: input.now(), unattributed: [...input.unattributed] });
@@ -276,17 +341,25 @@ export function openSessionAccount(controlDir, input) {
276
341
  row.openedAt = input.now();
277
342
  delete row.closedAt;
278
343
  delete row.unadjudicated;
279
- row.unattributed = [...new Set([...row.unattributed, ...input.unattributed])];
344
+ const merged = new Map(row.unattributed.map((m) => [memberKey(m.scope, m.rel), m]));
345
+ for (const m of input.unattributed)
346
+ merged.set(memberKey(m.scope, m.rel), { scope: m.scope, rel: m.rel });
347
+ row.unattributed = [...merged.values()];
280
348
  }
281
349
  return { next: rec, result: undefined };
282
350
  });
283
351
  }
284
352
  export function sessionUnattributedSet(controlDir, sessionId) {
285
- const rec = coerceSessionAccounts(readStrictSidecar(controlDir, SESSION_ACCOUNTS_FILE, "memory session-account ledger"));
286
- return new Set(rec.rows.find((r) => r.sessionId === sessionId)?.unattributed ?? []);
353
+ const row = readSessionAccountsConverted(controlDir).rows.find((r) => r.sessionId === sessionId);
354
+ if (row === undefined)
355
+ return emptyUnattributedSet();
356
+ const all = (row.unattributable?.length ?? 0) > 0;
357
+ const keys = new Set(row.unattributed.map((m) => memberKey(m.scope, m.rel)));
358
+ return { has: (scope, rel) => all || keys.has(memberKey(scope, rel.split("\\").join("/"))), empty: !all && keys.size === 0 };
287
359
  }
288
360
  export function closeSessionAccount(controlDir, input) {
289
- return lockedStrictUpdate(controlDir, SESSION_ACCOUNTS_FILE, "memory session-account ledger", coerceSessionAccounts, (rec) => {
361
+ return lockedStrictUpdate(controlDir, SESSION_ACCOUNTS_FILE, "memory session-account ledger", coerceSessionAccounts, (read) => {
362
+ const { rec } = convertSessionAccounts(read, controlDir);
290
363
  const row = rec.rows.find((r) => r.sessionId === input.sessionId);
291
364
  if (row === undefined)
292
365
  return { result: false };
@@ -303,7 +376,8 @@ export function resolveSessionAccountRecord(controlDir, input) {
303
376
  e.code = "memory.session_account_resolve_unattributed";
304
377
  throw e;
305
378
  }
306
- lockedStrictUpdate(controlDir, SESSION_ACCOUNTS_FILE, "memory session-account ledger", coerceSessionAccounts, (rec) => {
379
+ lockedStrictUpdate(controlDir, SESSION_ACCOUNTS_FILE, "memory session-account ledger", coerceSessionAccounts, (read) => {
380
+ const { rec } = convertSessionAccounts(read, controlDir);
307
381
  const row = rec.rows.find((r) => r.sessionId === input.sessionId);
308
382
  if (row === undefined) {
309
383
  const e = new Error(`resolveSessionAccount: unknown sessionId ${JSON.stringify(input.sessionId)}`);
@@ -321,21 +395,23 @@ const HOLD_STATUSES = new Set(["capturing", "held", "released", "disposed"]);
321
395
  const HOLD_TERMINALS = new Set(["dirty", "expired", "conflict", "discarded", "capture_lost"]);
322
396
  function coerceHolds(raw) {
323
397
  if (raw === undefined)
324
- return { version: 1, rows: [] };
398
+ return { version: 2, rows: [] };
325
399
  const rec = raw;
326
400
  if (typeof rec !== "object" || rec === null || !Array.isArray(rec.rows))
327
401
  throw new ControlPlaneCorruptError("memory instruction-hold ledger has the wrong shape");
328
- if (rec.version !== 1)
402
+ if (rec.version !== 1 && rec.version !== 2)
329
403
  throw new ControlPlaneCorruptError(`memory instruction-hold ledger has an unrecognized version (${String(rec.version)}) — refusing (fail-closed)`);
330
- for (const r of rec.rows) {
404
+ const v1 = rec.version === 1;
405
+ const rows = [];
406
+ const raws = rec.rows;
407
+ for (const r of raws) {
331
408
  const row = r;
332
409
  if (typeof row !== "object" ||
333
410
  row === null ||
334
411
  !reqStr(row.holdId) ||
335
412
  !reqStr(row.sessionId) ||
336
413
  (row.op !== "add" && row.op !== "update") ||
337
- !reqStr(row.relPath) ||
338
- !reqStr(row.slug) ||
414
+ !safeRelPath(row.slug) ||
339
415
  !reqStr(row.scope) ||
340
416
  !reqStr(row.captureName) ||
341
417
  !reqStr(row.contentDigest) ||
@@ -350,15 +426,28 @@ function coerceHolds(raw) {
350
426
  (row.entryId !== undefined && typeof row.entryId !== "string") ||
351
427
  (row.baseRev !== undefined && typeof row.baseRev !== "string") ||
352
428
  (row.op === "update" && (!reqStr(row.entryId) || !reqStr(row.baseRev))) ||
353
- !safeRelPath(row.relPath) ||
429
+ (v1 ? !safeRelPath(row.relPath) : !safeRelPath(row.rel)) ||
354
430
  !safeBasename(row.captureName)) {
355
431
  throw new ControlPlaneCorruptError("memory instruction-hold ledger: a row is malformed — refusing (fail-closed; an unevaluable hold must not read as settled)");
356
432
  }
433
+ if (v1) {
434
+ const { relPath: _relPath, ...rest } = row;
435
+ rows.push({ ...rest, rel: `${row.slug}.md` });
436
+ }
437
+ else {
438
+ rows.push(row);
439
+ }
357
440
  }
358
- return rec;
441
+ return { version: 2, rows };
442
+ }
443
+ function holdsOnDiskAreV1(raw) {
444
+ return typeof raw === "object" && raw !== null && raw.version === 1;
359
445
  }
360
446
  export function readHolds(controlDir) {
361
- return coerceHolds(readStrictSidecar(controlDir, HOLDS_FILE, "memory instruction-hold ledger")).rows;
447
+ const raw = readStrictSidecar(controlDir, HOLDS_FILE, "memory instruction-hold ledger");
448
+ if (!holdsOnDiskAreV1(raw))
449
+ return coerceHolds(raw).rows;
450
+ return lockedStrictUpdate(controlDir, HOLDS_FILE, "memory instruction-hold ledger", coerceHolds, (again) => ({ next: again, result: again.rows }));
362
451
  }
363
452
  function sha256(buf) {
364
453
  return createHash("sha256").update(buf).digest("hex");
@@ -397,7 +486,7 @@ export function noReplaceRestore(stagingPath, destPath) {
397
486
  return "restored";
398
487
  }
399
488
  export function openInstructionHold(controlDir, input) {
400
- const holdId = `hold-${input.now()}-${createHash("sha256").update(`${input.sessionId}\n${input.relPath}\n${input.content}`).digest("hex").slice(0, 12)}`;
489
+ const holdId = `hold-${input.now()}-${createHash("sha256").update(`${input.sessionId}\n${input.rel}\n${input.content}`).digest("hex").slice(0, 12)}`;
401
490
  const buf = Buffer.from(input.content, "utf8");
402
491
  const digest = sha256(buf);
403
492
  const captureName = `${holdId}.md`;
@@ -420,7 +509,7 @@ export function openInstructionHold(controlDir, input) {
420
509
  ...(input.entryId !== undefined ? { entryId: input.entryId } : {}),
421
510
  op: input.op,
422
511
  ...(input.baseRev !== undefined ? { baseRev: input.baseRev } : {}),
423
- relPath: input.relPath,
512
+ rel: input.rel,
424
513
  slug: input.slug,
425
514
  scope: input.scope,
426
515
  captureName,
@@ -490,12 +579,12 @@ export function openInstructionHold(controlDir, input) {
490
579
  }
491
580
  export function reconcileHolds(controlDir, input) {
492
581
  const holdDir = join(controlDir, HOLD_DIR);
493
- const discloseStranded = (holdId, relPath, stagingName) => {
582
+ const discloseStranded = (holdId, rel, stagingName) => {
494
583
  try {
495
584
  enqueueMemoryAnnouncement(controlDir, {
496
585
  kind: "gate",
497
586
  at: input.now(),
498
- items: [`instruction hold ${holdId}: a concurrent writer's bytes for ${JSON.stringify(relPath)} could not be restored onto the plane — they are preserved at hold/${stagingName} in the memory control plane for host recovery`],
587
+ items: [`instruction hold ${holdId}: a concurrent writer's bytes for ${JSON.stringify(rel)} could not be restored onto the plane — they are preserved at hold/${stagingName} in the memory control plane for host recovery`],
499
588
  });
500
589
  }
501
590
  catch {
@@ -512,7 +601,7 @@ export function reconcileHolds(controlDir, input) {
512
601
  terminalLost = true;
513
602
  }
514
603
  else {
515
- const plane = join(input.memoryDir, row.relPath);
604
+ const plane = join(input.homeOf(row.scope), row.rel);
516
605
  if (existsSync(plane)) {
517
606
  try {
518
607
  const stagingName = uniqueStagingName(row.holdId, input.now);
@@ -522,7 +611,7 @@ export function reconcileHolds(controlDir, input) {
522
611
  unlinkSync(staging);
523
612
  }
524
613
  else if (noReplaceRestore(staging, plane) === "stranded") {
525
- discloseStranded(row.holdId, row.relPath, stagingName);
614
+ discloseStranded(row.holdId, row.rel, stagingName);
526
615
  }
527
616
  }
528
617
  catch {
@@ -531,7 +620,7 @@ export function reconcileHolds(controlDir, input) {
531
620
  }
532
621
  }
533
622
  else {
534
- const plane = join(input.memoryDir, row.relPath);
623
+ const plane = join(input.homeOf(row.scope), row.rel);
535
624
  if (existsSync(plane)) {
536
625
  const bytes = readFileSync(plane);
537
626
  if (sha256(bytes) === row.contentDigest) {
@@ -549,7 +638,7 @@ export function reconcileHolds(controlDir, input) {
549
638
  }
550
639
  }
551
640
  else if (noReplaceRestore(staging, plane) === "stranded") {
552
- discloseStranded(row.holdId, row.relPath, stagingName);
641
+ discloseStranded(row.holdId, row.rel, stagingName);
553
642
  }
554
643
  }
555
644
  catch {
@@ -1186,6 +1186,10 @@ export declare class MemoryEngine {
1186
1186
  * eventId anchors on the pollution record (sessionId + marked-at), so however many times this
1187
1187
  * settlement replays the same pollution event, the generation is allocated once. */
1188
1188
  private settlePromotions;
1189
+ /** The layout-adoption seat of a mount: a backend pinning its own directory root adopts under its
1190
+ * transaction lock; a copy-out projection tree and a backend without the face are adopted by the
1191
+ * engine directly (registry lock only — nothing else writes those trees during a mount). */
1192
+ private adoptLayoutBeforeMount;
1189
1193
  materialize(scopes: readonly string[], writeScope: string | null, opts?: {
1190
1194
  adoptionRestricted?: boolean;
1191
1195
  sessionId?: string;
@@ -1242,6 +1246,9 @@ export declare class MemoryEngine {
1242
1246
  * Throws only on a corrupt hold/settlement ledger — the caller's fail-closed reconciliation arm
1243
1247
  * refuses the harvest (integrity unknown must not read as "no holds").
1244
1248
  */
1249
+ /** A hold's plane seat as a memory-dir-relative path (the report/warning spelling every other
1250
+ * harvest path uses), derived from the ledger-form row. */
1251
+ private holdPlaneRel;
1245
1252
  private settleDueHolds;
1246
1253
  /**
1247
1254
  * The release leg: re-walk the captured bytes through the FULL gate set and commit them under
@@ -1663,8 +1670,6 @@ export declare class MemoryEngine {
1663
1670
  * @returns the rejection to report, or `undefined` when the index is clean/absent.
1664
1671
  */
1665
1672
  private gateDerivedIndex;
1666
- /** Names of read-only sibling scope dirs directly under the writable root (excluded from scans; public since #691 — the runner refuses to admit a writable root that contains another scope's directory to the file tools' allowlist). */
1667
- readonlyDirNamesUnderRoot(handle: MemorySessionHandle): Set<string>;
1668
1673
  /** Projection write: reads the target to skip a no-op, then writes — the same check-then-use shape
1669
1674
  * as the containment restores, so it takes the same no-follow write. A link at a projection path
1670
1675
  * is never a supported layout (the harvest scan refuses symlinked entry files outright), and this
@@ -1699,12 +1704,8 @@ export declare class MemoryEngine {
1699
1704
  * `getByIds` there would sync-adopt mid-harvest.
1700
1705
  */
1701
1706
  private debtCommittedProjection;
1702
- /** Sibling scope subdir names under `dir` (excluded from a scope-tree walk when `dir` is the root
1703
- * a root-owning layer's chmod/restore must never touch another scope's home). */
1704
- private siblingScopeDirNames;
1705
- /** chmod a scope's entry tree (best-effort; skipRoot leaves the top dir writable — used when a
1706
- * read-only layer's dir IS the shared root / an ancestor of the writable root, which must stay
1707
- * traversable+writable; excludeTopDirs skips sibling scope homes under a root-owning layer).
1707
+ /** chmod a scope's entry tree (best-effort). The tree is the scope's own home and nothing else
1708
+ * no other scope's directory can lie under it.
1708
1709
  *
1709
1710
  * `opts.announced` is the CALLER's per-materialize ledger of paths already disclosed (#495 M-1).
1710
1711
  * One materialize sweeps each inherited tree TWICE — restore write perms at the top, re-apply