@sema-agent/core 7.16.0 → 7.17.1

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 (207) hide show
  1. package/CHANGELOG.md +209 -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 +2 -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/engine.js +2 -1
  49. package/dist/core/memory-engine/layout.d.ts +18 -6
  50. package/dist/core/memory-engine/layout.js +40 -21
  51. package/dist/core/memory-recall.d.ts +0 -8
  52. package/dist/core/memory.d.ts +1 -1
  53. package/dist/core/oracle-isolation.js +2 -2
  54. package/dist/core/parent-spec-seat.d.ts +66 -0
  55. package/dist/core/parent-spec-seat.js +47 -0
  56. package/dist/core/permission-rule-model.d.ts +11 -6
  57. package/dist/core/permission-rule-model.js +53 -37
  58. package/dist/core/permission-rules.js +8 -3
  59. package/dist/core/physical-path.d.ts +37 -0
  60. package/dist/core/physical-path.js +30 -0
  61. package/dist/core/protocol-naming.d.ts +2 -2
  62. package/dist/core/remote-env.d.ts +1 -1
  63. package/dist/core/retired-keys.d.ts +19 -0
  64. package/dist/core/retired-keys.js +15 -0
  65. package/dist/core/runner/active-skill-scope.js +3 -3
  66. package/dist/core/runner/assemble-result.d.ts +15 -13
  67. package/dist/core/runner/assemble-result.js +5 -4
  68. package/dist/core/runner/compaction-call-options.d.ts +25 -89
  69. package/dist/core/runner/contracts.d.ts +11 -10
  70. package/dist/core/runner/gate-exit.js +15 -1
  71. package/dist/core/runner/halt-attribution.d.ts +35 -0
  72. package/dist/core/runner/halt-attribution.js +16 -0
  73. package/dist/core/runner/memory-consolidation.d.ts +0 -2
  74. package/dist/core/runner/memory-consolidation.js +0 -1
  75. package/dist/core/runner/prepare-artifact.d.ts +52 -0
  76. package/dist/core/runner/prepare-artifact.js +63 -0
  77. package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -2
  78. package/dist/core/runner/prepare-caps-and-workflow.js +2 -3
  79. package/dist/core/runner/prepare-config-doors.d.ts +3 -3
  80. package/dist/core/runner/prepare-config-doors.js +5 -2
  81. package/dist/core/runner/prepare-context-lane.d.ts +2 -2
  82. package/dist/core/runner/prepare-context-lane.js +2 -13
  83. package/dist/core/runner/prepare-file-history.d.ts +1 -1
  84. package/dist/core/runner/prepare-file-history.js +3 -52
  85. package/dist/core/runner/prepare-hands-readface.d.ts +4 -7
  86. package/dist/core/runner/prepare-hands-readface.js +3 -2
  87. package/dist/core/runner/prepare-inherited-gate.d.ts +5 -2
  88. package/dist/core/runner/prepare-inherited-gate.js +7 -8
  89. package/dist/core/runner/prepare-memory.d.ts +4 -6
  90. package/dist/core/runner/prepare-memory.js +4 -2
  91. package/dist/core/runner/prepare-policy-chain.d.ts +3 -3
  92. package/dist/core/runner/prepare-policy-chain.js +15 -30
  93. package/dist/core/runner/prepare-prompt-assembly.js +1 -1
  94. package/dist/core/runner/prepare-question-face.js +2 -1
  95. package/dist/core/runner/prepare-safety-scan.d.ts +1 -1
  96. package/dist/core/runner/prepare-suspend-saga.js +0 -2
  97. package/dist/core/runner/prepare-task.js +12 -11
  98. package/dist/core/runner/prepare-wiring-manifest.js +9 -7
  99. package/dist/core/runner/resume-admission.js +2 -1
  100. package/dist/core/runner/resume-internals-and-config.js +4 -0
  101. package/dist/core/runner/retired-runner-deps.d.ts +3 -0
  102. package/dist/core/runner/retired-runner-deps.js +7 -0
  103. package/dist/core/runner/run-harness-handlers.js +5 -3
  104. package/dist/core/runner/run-settle-and-teardown.d.ts +1 -1
  105. package/dist/core/runner/run-settle-and-teardown.js +3 -1
  106. package/dist/core/runner/session-file-state-replay.js +3 -3
  107. package/dist/core/runner/session-rule-policy.js +3 -4
  108. package/dist/core/runner/stream-halt-verbs.js +1 -1
  109. package/dist/core/runner/stream-steer-verb.js +1 -4
  110. package/dist/core/runner/workspace-path.js +3 -0
  111. package/dist/core/runner-deps.d.ts +30 -49
  112. package/dist/core/safe-notify.d.ts +1 -1
  113. package/dist/core/sensitive-path-policy.d.ts +1 -1
  114. package/dist/core/sensitive-path-policy.js +11 -12
  115. package/dist/core/session.d.ts +2 -2
  116. package/dist/core/skill-tool-specifier.d.ts +1 -1
  117. package/dist/core/skills-directory.d.ts +1 -1
  118. package/dist/core/skills-directory.js +4 -3
  119. package/dist/core/spec-contract.js +5 -4
  120. package/dist/core/store-contracts/checkpoint-store-contract.js +1 -1
  121. package/dist/core/strategy-store.d.ts +8 -11
  122. package/dist/core/task-event.d.ts +20 -17
  123. package/dist/core/task-limits.d.ts +1 -1
  124. package/dist/core/task-notification.d.ts +5 -5
  125. package/dist/core/task-registry-agent.d.ts +5 -5
  126. package/dist/core/task-registry-monitor.d.ts +2 -2
  127. package/dist/core/task-registry-shared.d.ts +32 -28
  128. package/dist/core/task-registry-shared.js +4 -13
  129. package/dist/core/task-registry.d.ts +8 -8
  130. package/dist/core/task-result.d.ts +37 -37
  131. package/dist/core/task-spec.d.ts +5 -5
  132. package/dist/core/task-stream.d.ts +16 -11
  133. package/dist/core/tool-catalog-entries.d.ts +1 -0
  134. package/dist/core/tool-catalog-entries.js +6 -1
  135. package/dist/core/tool-conformance.d.ts +0 -2
  136. package/dist/core/tool-conformance.js +1 -3
  137. package/dist/core/tool-errors.d.ts +5 -0
  138. package/dist/core/tool-errors.js +1 -0
  139. package/dist/core/tool-policy.d.ts +66 -22
  140. package/dist/core/tool-policy.js +31 -5
  141. package/dist/core/tool-result-budget.d.ts +1 -1
  142. package/dist/core/tool-result-store.d.ts +11 -11
  143. package/dist/core/tool-spec.d.ts +34 -38
  144. package/dist/core/tools.d.ts +2 -2
  145. package/dist/core/trace.d.ts +12 -15
  146. package/dist/core/untrusted-text.d.ts +1 -1
  147. package/dist/core/wiring-manifest.d.ts +4 -3
  148. package/dist/core/workflow-journal-store.d.ts +11 -11
  149. package/dist/engine/execution-env/node-execution-env.d.ts +3 -2
  150. package/dist/engine/execution-env/node-execution-env.js +8 -5
  151. package/dist/engine/harness/agent-harness.d.ts +47 -4
  152. package/dist/engine/harness/agent-harness.js +52 -9
  153. package/dist/engine/loop/agent-loop.js +15 -8
  154. package/dist/engine/loop/types.d.ts +5 -0
  155. package/dist/index.d.ts +20 -14
  156. package/dist/index.js +16 -10
  157. package/dist/internal/harness.d.ts +1 -0
  158. package/dist/orchestration/run-spec.d.ts +3 -0
  159. package/dist/orchestration/run-spec.js +3 -3
  160. package/dist/orchestration/run-workflow-tool.d.ts +12 -6
  161. package/dist/orchestration/run-workflow-tool.js +23 -5
  162. package/dist/orchestration/workflow-primitives.d.ts +4 -2
  163. package/dist/orchestration/workflow-primitives.js +1 -4
  164. package/dist/orchestration/workflow-script-store.js +9 -25
  165. package/dist/orchestration/workflow-types.d.ts +34 -4
  166. package/dist/orchestration/workflow-types.js +2 -0
  167. package/dist/orchestration/workflow.d.ts +39 -7
  168. package/dist/orchestration/workflow.js +237 -113
  169. package/dist/prompt-assembly/assemble.d.ts +2 -2
  170. package/dist/prompt-assembly/assemble.js +9 -9
  171. package/dist/prompt-assembly/composer.d.ts +6 -7
  172. package/dist/prompt-assembly/composer.js +10 -10
  173. package/dist/prompt-assembly/epoch.js +1 -1
  174. package/dist/prompt-assembly/event-registry.d.ts +0 -21
  175. package/dist/prompt-assembly/packs/sema-default.js +30 -30
  176. package/dist/prompt-assembly/types.d.ts +14 -14
  177. package/dist/prompts/coordinator.d.ts +0 -13
  178. package/dist/prompts/coordinator.js +0 -3
  179. package/dist/stores/cc/task-list-store.js +2 -10
  180. package/dist/stores/file/checkpoint-store.js +1 -5
  181. package/dist/stores/file/file-history-store.js +2 -2
  182. package/dist/stores/file/fs-atomic.d.ts +15 -18
  183. package/dist/stores/file/fs-atomic.js +4 -14
  184. package/dist/stores/file/mailbox-store.d.ts +7 -11
  185. package/dist/stores/file/mailbox-store.js +4 -11
  186. package/dist/stores/file/permission-rule-store.d.ts +4 -35
  187. package/dist/stores/file/permission-rule-store.js +4 -148
  188. package/dist/tools/artifact/artifact-text.d.ts +62 -0
  189. package/dist/tools/artifact/artifact-text.js +68 -0
  190. package/dist/tools/artifact/artifact-tool.d.ts +52 -0
  191. package/dist/tools/artifact/artifact-tool.js +351 -0
  192. package/dist/tools/artifact/local-stub.d.ts +26 -0
  193. package/dist/tools/artifact/local-stub.js +297 -0
  194. package/dist/tools/fs/bash-readonly-classifier.d.ts +90 -8
  195. package/dist/tools/fs/bash-readonly-classifier.js +294 -70
  196. package/dist/tools/fs/fs-bash.d.ts +14 -10
  197. package/dist/tools/fs/fs-bash.js +50 -33
  198. package/dist/tools/fs/fs-search-tools.js +5 -2
  199. package/dist/tools/fs/fs-shared.d.ts +2 -0
  200. package/dist/tools/fs/fs-shared.js +3 -11
  201. package/dist/tools/fs/index.d.ts +4 -0
  202. package/dist/tools/fs/index.js +4 -1
  203. package/dist/tools/fs/safety.d.ts +117 -1
  204. package/dist/tools/fs/safety.js +131 -31
  205. package/dist/tools/fs/search.js +8 -6
  206. package/package.json +2 -1
  207. package/test/export-surface.snapshot.json +119 -39
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;
@@ -4444,7 +4444,8 @@ export class MemoryEngine {
4444
4444
  let wrote = false;
4445
4445
  try {
4446
4446
  mkdirSync(dirname(indexPath), { recursive: true });
4447
- writeFileNoFollow(indexPath, text);
4447
+ if (readNoFollowSafe(indexPath) !== text)
4448
+ writeFileNoFollow(indexPath, text);
4448
4449
  wrote = true;
4449
4450
  }
4450
4451
  catch {
@@ -1,5 +1,6 @@
1
1
  import { type MemoryAnnouncement } from "./types.js";
2
2
  import type { AssertAllKeysHandled } from "../ask-origin.js";
3
+ import { physicalPathOfExisting } from "../physical-path.js";
3
4
  /** Cursor sidecar for the design/84 Seam B pair on the FileBackend (`{ [scope]: cursor }`). B3: control plane. */
4
5
  export declare const CURSORS_FILE = "cursors.json";
5
6
  /** The control-plane subdir name under a key dir — ONE spelling for every derivation and for the
@@ -32,10 +33,11 @@ export declare class ControlPlaneCorruptError extends Error {
32
33
  * Its input is the HOST-declared repo root; a model-reachable directory is keyed by
33
34
  * {@link deriveDirKey} instead, and that asymmetry is deliberate (see there).
34
35
  *
35
- * Known limit, registered rather than fixed (ruled 2026-08-03): on a case-INSENSITIVE volume two
36
- * spellings that differ only in case still mint two keys realpath does not case-fold, and folding
37
- * here would MERGE two genuinely distinct repos on a case-SENSITIVE volume. This round closes the
38
- * symlink axis only; the case axis predates it and is unchanged.
36
+ * The case axis (once a known limit here) closes with the same call: the mint answers with the ON-DISK
37
+ * spelling, so two case-variant spellings name ONE key on a case-INSENSITIVE volume, while on a
38
+ * case-SENSITIVE volume they are two distinct directories and keep two (FOLDING would merge that pair).
39
+ * Registered, not fixed: the PREVIOUS key was this realpath with the CALLER's casing = `deriveDirKey`'s
40
+ * spelling, which the arm below adopts, EXCEPT under a case-varying symlink on a non-folding library.
39
41
  */
40
42
  export declare function deriveRepoKey(repoRoot: string): string;
41
43
  /** Sink for the mount-time partition-key incidents (`memory.partition_split` /
@@ -48,6 +50,9 @@ export type MemoryPartitionIncidentSink = (err: Error & {
48
50
  * when the two mints differ its `memory/` + `.engine/` + siblings live under {@link deriveDirKey}'s
49
51
  * dir. Move the whole partition onto the canonical key (see {@link migrateKeyedPartition}). */
50
52
  export declare function adoptLegacyRepoDirs(configRoot: string, repoRoot: string, onIncident?: MemoryPartitionIncidentSink): void;
53
+ /** Every spelling an earlier release keyed this repo's partition by, minus today's: the resolve-only key and the
54
+ * link-resolved, caller-cased key (`/var/TMP` keyed `-private-var-TMP`; today `-private-var-tmp`). */
55
+ export declare function earlierRepoKeysOf(repoRoot: string): string[];
51
56
  /**
52
57
  * The REVERSE one-shot migration (ruled 2026-08-03): for the width of ONE release the control-plane
53
58
  * key canonicalized like the repo key does, which put a directory-pinned mount's `.engine` under the
@@ -248,8 +253,10 @@ export declare function layoutNeedsAdoption(root: string, controlDir: string): b
248
253
  * 6. commit B: the marker removed; `memory.layout_migrated` disclosed.
249
254
  */
250
255
  export declare function adoptRootScopeLayout(rootPath: string, controlDir: string, opts?: AdoptRootScopeLayoutOptions): void;
251
- /** Canonicalize a path for containment checks (resolves symlinks on the EXISTING prefix). */
252
- export declare function canonicalize(p: string): string;
256
+ /** The memory layout's name for the tree's one physical-path mint (symlinks on the EXISTING prefix
257
+ * resolved, the missing tail rejoined). ONE implementation behind both names — a second canonicalizer
258
+ * here is what let a key minted by this layout name a directory the fence resolved differently. */
259
+ export declare const canonicalize: typeof physicalPathOfExisting;
253
260
  /** True iff `child` (canonicalized) is `root` or inside it. */
254
261
  export declare function isContainedIn(root: string, child: string): boolean;
255
262
  /** mkdir -p convenience used by the engine/backend (0o700 like the file stores). */
@@ -976,4 +983,9 @@ export declare function writeAllSync(fd: number, data: string): void;
976
983
  * or re-derived); do not route data that can't through here. Throws on failure (fail-closed: a
977
984
  * sidecar that cannot be written must not be silently skipped). */
978
985
  export declare function atomicWriteFileSync(path: string, data: string): void;
986
+ /** Content-gated {@link atomicWriteFileSync} for a DERIVED/control-plane file: a byte-identical
987
+ * rewrite (the common case on a repeat mount over an unchanged store) is skipped entirely, so the
988
+ * file's mtime stays put. ONLY for files whose mtime carries no meaning of its own — a lock or a
989
+ * heartbeat file's mtime IS its signal and must keep calling {@link atomicWriteFileSync} directly. */
990
+ export declare function writeIfChanged(path: string, data: string): void;
979
991
  export {};
@@ -1,9 +1,10 @@
1
- import { chmodSync, closeSync, constants as fsConstants, copyFileSync, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, utimesSync, writeFileSync, writeSync } from "node:fs";
1
+ import { chmodSync, closeSync, constants as fsConstants, copyFileSync, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, utimesSync, writeFileSync, writeSync } from "node:fs";
2
2
  const { O_WRONLY, O_CREAT, O_TRUNC, O_NOFOLLOW, O_EXCL } = fsConstants;
3
3
  import { homedir } from "node:os";
4
4
  import { createHash } from "node:crypto";
5
5
  import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
6
6
  import { MEMORY_ORIGIN_CAUSES } from "./types.js";
7
+ import { physicalPathOfExisting, priorCallerCasedPathOf } from "../physical-path.js";
7
8
  const SCOPES_FILE = "scopes.json";
8
9
  export const CURSORS_FILE = "cursors.json";
9
10
  export const CONTROL_PLANE_DIR = ".engine";
@@ -84,7 +85,7 @@ function migrateKeyedPartition(m) {
84
85
  };
85
86
  const splitText = `memory partition split detected for ${m.subject}: BOTH ${m.fromDir} and ${m.toDir} hold content. This session uses ${m.toDir}; the other dir is left untouched (never merged — CAS baselines would collide). If it holds the newer memory, move its rows by hand and remove it.`;
86
87
  try {
87
- if (m.fromDir === m.toDir)
88
+ if (m.fromDir === m.toDir || canonicalize(m.fromDir) === canonicalize(m.toDir))
88
89
  return;
89
90
  if (!existsSync(m.fromDir))
90
91
  return;
@@ -113,12 +114,20 @@ function migrateKeyedPartition(m) {
113
114
  }
114
115
  }
115
116
  export function adoptLegacyRepoDirs(configRoot, repoRoot, onIncident) {
116
- migrateKeyedPartition({
117
- fromDir: join(configRoot, deriveDirKey(repoRoot)),
118
- toDir: join(configRoot, deriveRepoKey(repoRoot)),
119
- subject: repoRoot,
120
- onIncident,
121
- });
117
+ const toDir = join(configRoot, deriveRepoKey(repoRoot));
118
+ for (const key of earlierRepoKeysOf(repoRoot)) {
119
+ migrateKeyedPartition({ fromDir: join(configRoot, key), toDir, subject: repoRoot, onIncident });
120
+ }
121
+ }
122
+ export function earlierRepoKeysOf(repoRoot) {
123
+ const today = deriveRepoKey(repoRoot);
124
+ const keys = [deriveDirKey(repoRoot)];
125
+ try {
126
+ keys.push(slugifyPath(priorCallerCasedPathOf(resolve(repoRoot))));
127
+ }
128
+ catch {
129
+ }
130
+ return [...new Set(keys)].filter((k) => k !== today);
122
131
  }
123
132
  export function adoptCanonicalKeyedControlDir(configRoot, dir, onIncident) {
124
133
  migrateKeyedPartition({
@@ -403,6 +412,15 @@ function commitScopesRecord(controlDir, lock, token, next) {
403
412
  const data = `${JSON.stringify(next, null, 2)}\n`;
404
413
  const fence = () => assertSidecarLockOwnership(lock, token, "scope registry", "strict");
405
414
  fence();
415
+ let current;
416
+ try {
417
+ current = readFileSync(file, "utf8");
418
+ }
419
+ catch {
420
+ current = undefined;
421
+ }
422
+ if (current === data)
423
+ return;
406
424
  atomicWriteFileSync(journal, data);
407
425
  fence();
408
426
  atomicWriteFileSync(file, data);
@@ -701,18 +719,7 @@ function moveLooseContent(root, controlDir, rec, owner, label, renew, sink, resu
701
719
  writeIndexRevs(controlDir, {});
702
720
  discloseLayoutIncident(sink, "memory.layout_migrated", `memory layout adopted at ${root}: ${moved} loose entr${moved === 1 ? "y" : "ies"} moved into the home of ${JSON.stringify(owner)} (${label})${resumed ? " — an interrupted move was finished" : ""}`);
703
721
  }
704
- export function canonicalize(p) {
705
- try {
706
- return realpathSync(p);
707
- }
708
- catch {
709
- const parent = resolve(p, "..");
710
- if (parent === p)
711
- return p;
712
- const rest = p.slice(parent.endsWith(sep) ? parent.length : parent.length + 1);
713
- return join(canonicalize(parent), rest);
714
- }
715
- }
722
+ export const canonicalize = physicalPathOfExisting;
716
723
  export function isContainedIn(root, child) {
717
724
  const r = canonicalize(root);
718
725
  const c = canonicalize(child);
@@ -1034,7 +1041,7 @@ export function readIndexRevs(controlDir) {
1034
1041
  export function writeIndexRevs(controlDir, revs) {
1035
1042
  try {
1036
1043
  ensureDirExists(controlDir);
1037
- atomicWriteFileSync(join(controlDir, INDEX_REVS_FILE), `${JSON.stringify(revs, null, 2)}\n`);
1044
+ writeIfChanged(join(controlDir, INDEX_REVS_FILE), `${JSON.stringify(revs, null, 2)}\n`);
1038
1045
  }
1039
1046
  catch {
1040
1047
  }
@@ -2056,3 +2063,15 @@ export function atomicWriteFileSync(path, data) {
2056
2063
  throw err;
2057
2064
  }
2058
2065
  }
2066
+ export function writeIfChanged(path, data) {
2067
+ let current;
2068
+ try {
2069
+ current = readFileSync(path, "utf8");
2070
+ }
2071
+ catch {
2072
+ current = undefined;
2073
+ }
2074
+ if (current === data)
2075
+ return;
2076
+ atomicWriteFileSync(path, data);
2077
+ }
@@ -177,14 +177,6 @@ export declare function composeSelectiveBody(manifestText: string, selected: Mem
177
177
  * widen the public surface (and invite callers to parse the opaque format) for zero consumer. Treat the key
178
178
  * as an opaque token: persist it, feed it back, compare for equality — never split it. */
179
179
  export declare function encodeSurfacedKey(scope: string, id: string): string;
180
- /** A manifest header tagged with the scope it came from (design/84 Seam A decision 5). */
181
- export interface ScopedNoteHeader extends MemoryNoteHeader {
182
- scope: string;
183
- }
184
- /** A full record tagged with its scope (for cross-scope getByIds). */
185
- export interface ScopedNoteRecord extends MemoryNoteRecord {
186
- scope: string;
187
- }
188
180
  export interface LayeredRecallOptions {
189
181
  store: MemoryStore;
190
182
  /** Ordered scopes (design/84 Seam A): list order = priority; the last is highest. */
@@ -233,7 +233,7 @@ export type ConsolidationCapableStore = MemoryStore & Required<Pick<MemoryStore,
233
233
  * (design/41): {@link MemoryStore.searchScored}/{@link MemoryStore.update}/{@link MemoryStore.delete}.
234
234
  * When false, the Runner skips consolidation entirely (graceful no-op).
235
235
  *
236
- * RB-436-g: a TYPE PREDICATE, so the guard's conclusion travels to the call sites. A bare boolean left
236
+ * A TYPE PREDICATE, so the guard's conclusion travels to the call sites. A bare boolean left
237
237
  * every post-guard call site writing `store.update!(…)`, and a guard that later forgot one of the three
238
238
  * would have been invisible — the `!`s keep compiling. Now dropping a method from the check turns the
239
239
  * call sites red.
@@ -1,5 +1,5 @@
1
1
  import { isRemoteExecutionEnv, isIsolated } from "./remote-env.js";
2
- import { canonicalizeTarget } from "../tools/fs/safety.js";
2
+ import { canonicalizeTarget, pathFamilyOf } from "../tools/fs/safety.js";
3
3
  function probeCommand(canonOraclePath) {
4
4
  const q = `'${canonOraclePath.replace(/'/g, `'\\''`)}'`;
5
5
  return `touch -- ${q}/.oracle-isolation-probe 2>/dev/null`;
@@ -31,7 +31,7 @@ export async function assertOracleIsolation(workerEnv, grader, opts) {
31
31
  }
32
32
  let writable;
33
33
  try {
34
- const canon = await canonicalizeTarget(workerEnv, oraclePath, opts?.signal);
34
+ const canon = await canonicalizeTarget(workerEnv, oraclePath, opts?.signal, pathFamilyOf({ root: oraclePath }));
35
35
  if (!canon.ok) {
36
36
  return { protected: false, isolationClass, reason: `cannot canonicalize oracle path "${oraclePath}": ${canon.message}` };
37
37
  }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The parent SPEC seat a run hands its children — the projection of the fields of the parent's own REBOUND spec
3
+ * that inherit copy-at-spawn (identity, process facts, deployment facts, the frozen question face) onto ONE value
4
+ * both delegation lanes copy onto a child spec: `ToolExecuteContext.parentSpecSeat` on the Task lane,
5
+ * `RunWorkflowToolDeps.parentSpecSeat` → `RunWorkflowOptions.parentSpecSeat` on the workflow lane. Minted once per
6
+ * prepare (the inherited-gate phase); applied by ONE function ({@link applyParentSpecSeat}) so the two lanes
7
+ * cannot drift on which fields travel or on the priority between a child's own value and the parent's.
8
+ *
9
+ * The checkpoint seat (`./checkpoint-seat.ts`) is the same shape for one field; this seat carries the rest.
10
+ */
11
+ import type { OnQuestion } from "./ask-question.js";
12
+ import type { ToolExecuteContext } from "./tool-spec.js";
13
+ import type { TaskSpec } from "./types.js";
14
+ /** The fields of a spec the seat carries — one `Pick`, shared by the mint's input and the apply's target. */
15
+ export type ParentSpecSeatFields = Pick<TaskSpec, "principal" | "oneShot" | "clientContext" | "onQuestion" | "additionalDirectories" | "additionalReadDirectories" | "envFacts" | "memoryPersistenceCapable">;
16
+ /**
17
+ * The seat a run hands its children. Every member is the parent's own value, frozen at mint (arrays copied and
18
+ * frozen; the facts object copied). Members present iff the parent carries them — an absent member is "nothing to
19
+ * inherit", never a value.
20
+ * · `principal` — the end-user identity (design/62: inherits verbatim, a worker cannot delegate under another).
21
+ * · `oneShot` — the process fact ("no later turn"): only `true` travels; it is a REMOVAL (see the apply law).
22
+ * · `clientContext` — the user's TZ/email facts (the child's environment block localizes the same way).
23
+ * · `onQuestion` — the parent's FROZEN question face (`spec.onQuestion ?? deps.onQuestion`, the value the parent's
24
+ * own AskUserQuestion resolves at): the child's question tool MOUNTS on it, so without it the child has no tool.
25
+ * · `additionalDirectories` / `additionalReadDirectories` — the fs-fence widening (deployment property).
26
+ * · `envFacts` — the sandbox facts (profile / egress / scratchpad).
27
+ * · `memoryPersistenceCapable` — the deployment's DECLARED persistence statement: `false` is a REMOVAL (floor),
28
+ * `true` a default a child's own declaration may narrow.
29
+ */
30
+ export interface ParentSpecSeat {
31
+ readonly principal?: string;
32
+ readonly oneShot?: true;
33
+ readonly clientContext?: TaskSpec["clientContext"];
34
+ readonly onQuestion?: OnQuestion;
35
+ readonly additionalDirectories?: readonly string[];
36
+ readonly additionalReadDirectories?: readonly string[];
37
+ readonly envFacts?: TaskSpec["envFacts"];
38
+ readonly memoryPersistenceCapable?: boolean;
39
+ }
40
+ /** Mint the seat off the parent's REBOUND spec and its frozen question face (spec over deps — the caller resolves it once,
41
+ * the same value the parent's own question routing reads). Absent fields stay absent; `oneShot` only when `true`. */
42
+ export declare function parentSpecSeatOf(spec: Omit<ParentSpecSeatFields, "onQuestion">, frozenOnQuestion: OnQuestion | undefined): ParentSpecSeat;
43
+ /**
44
+ * Apply the seat to a child spec — ONE law for every member, both lanes:
45
+ * a value the child spec already carries WINS; the seat FILLS an unset one; the two REMOVAL values
46
+ * (`oneShot: true`, `memoryPersistenceCapable: false`) land UNCONDITIONALLY.
47
+ * "Already carries" is the child's own authorship — a deployment baseline, a trusted script, an agent definition's
48
+ * fold (the Task lane folds the definition's `memoryPersistenceCapable` before applying; the workflow lane applies at
49
+ * the launch site AFTER the agentType fold) — so a definition may narrow a parent `true` to `false`, a baseline-pinned
50
+ * principal stands, and nothing a child can author lifts a removal. Arrays and the facts object are copied per child
51
+ * (a retained child's frozen snapshot must not alias the parent's seat). Returns a NEW object; `child` is not mutated.
52
+ * No seat (a tool running outside a Runner task) ⇒ the child spec is returned as is.
53
+ */
54
+ export declare function applyParentSpecSeat<S extends ParentSpecSeatFields>(child: S, seat: ParentSpecSeat | undefined): S;
55
+ /** The ctx keys that restate a seat member as THIS run's own fact (tools read them for scope / receipts / clamps). */
56
+ declare const SEAT_FACTS_ON_CTX: readonly ["principal", "oneShot", "onQuestion", "memoryPersistenceCapable"];
57
+ /**
58
+ * The seat a delegation boundary reads off a trusted ctx — ONE reader for both delegation tools. A Runner-filled ctx
59
+ * always carries the seat beside the run's own facts, and the two agree by construction (one mint). A HAND-BUILT ctx
60
+ * (a caller invoking a delegation tool's `execute` directly) that carries any of those facts WITHOUT the seat, or with
61
+ * a seat that disagrees with them, is REFUSED (`config.parent_spec_seat_missing`): spawning a principal-less child from a
62
+ * principal-bearing caller would offload into the shared namespace and re-key its journal — a silent tenant narrowing,
63
+ * never a fallback. A ctx with neither (a tool running outside a Runner task) hands down no seat.
64
+ */
65
+ export declare function parentSpecSeatOfCtx(ctx: Pick<ToolExecuteContext, "parentSpecSeat" | (typeof SEAT_FACTS_ON_CTX)[number]>): ParentSpecSeat | undefined;
66
+ export {};
@@ -0,0 +1,47 @@
1
+ export function parentSpecSeatOf(spec, frozenOnQuestion) {
2
+ return Object.freeze({
3
+ ...(spec.principal !== undefined ? { principal: spec.principal } : {}),
4
+ ...(spec.oneShot === true ? { oneShot: true } : {}),
5
+ ...(spec.clientContext !== undefined ? { clientContext: spec.clientContext } : {}),
6
+ ...(frozenOnQuestion !== undefined ? { onQuestion: frozenOnQuestion } : {}),
7
+ ...(spec.additionalDirectories !== undefined ? { additionalDirectories: Object.freeze([...spec.additionalDirectories]) } : {}),
8
+ ...(spec.additionalReadDirectories !== undefined ? { additionalReadDirectories: Object.freeze([...spec.additionalReadDirectories]) } : {}),
9
+ ...(spec.envFacts !== undefined ? { envFacts: Object.freeze({ ...spec.envFacts }) } : {}),
10
+ ...(spec.memoryPersistenceCapable !== undefined ? { memoryPersistenceCapable: spec.memoryPersistenceCapable } : {}),
11
+ });
12
+ }
13
+ export function applyParentSpecSeat(child, seat) {
14
+ if (seat === undefined)
15
+ return child;
16
+ const fill = (key, value) => child[key] === undefined && value !== undefined ? { [key]: value } : {};
17
+ return {
18
+ ...child,
19
+ ...fill("principal", seat.principal),
20
+ ...fill("clientContext", seat.clientContext),
21
+ ...fill("onQuestion", seat.onQuestion),
22
+ ...fill("additionalDirectories", (seat.additionalDirectories !== undefined ? [...seat.additionalDirectories] : undefined)),
23
+ ...fill("additionalReadDirectories", (seat.additionalReadDirectories !== undefined ? [...seat.additionalReadDirectories] : undefined)),
24
+ ...fill("envFacts", (seat.envFacts !== undefined ? { ...seat.envFacts } : undefined)),
25
+ ...(seat.oneShot === true ? { oneShot: true } : {}),
26
+ ...(seat.memoryPersistenceCapable === false ? { memoryPersistenceCapable: false } : fill("memoryPersistenceCapable", seat.memoryPersistenceCapable)),
27
+ };
28
+ }
29
+ const SEAT_FACTS_ON_CTX = ["principal", "oneShot", "onQuestion", "memoryPersistenceCapable"];
30
+ export function parentSpecSeatOfCtx(ctx) {
31
+ const seat = ctx.parentSpecSeat;
32
+ const facts = SEAT_FACTS_ON_CTX.filter((k) => ctx[k] !== undefined);
33
+ if (seat === undefined) {
34
+ if (facts.length === 0)
35
+ return undefined;
36
+ throw seatRefusal(`the tool context carries ${facts.join(" / ")} but no \`parentSpecSeat\` — a delegating caller hands its children the seat it was minted (a Runner-filled ctx always carries it); spawning without it would silently drop the identity`);
37
+ }
38
+ const disagree = facts.filter((k) => (k === "oneShot" ? (ctx.oneShot === true) !== (seat.oneShot === true) : ctx[k] !== seat[k]));
39
+ if (disagree.length > 0)
40
+ throw seatRefusal(`the tool context's ${disagree.join(" / ")} disagree with its \`parentSpecSeat\` — one mint, one value`);
41
+ return seat;
42
+ }
43
+ function seatRefusal(message) {
44
+ const e = new Error(`parentSpecSeat: ${message}`);
45
+ e.code = "config.parent_spec_seat_missing";
46
+ return e;
47
+ }
@@ -73,6 +73,8 @@
73
73
  * `Bash(git status:*)`) — the last two are strict-side divergences from upstream, registered as such.
74
74
  */
75
75
  import type { AssertAllKeysHandled } from "./ask-origin.js";
76
+ import { type PathFamily, type PathIdentity } from "../tools/fs/safety.js";
77
+ import type { PathTargetReading } from "./effective-path-target.js";
76
78
  /**
77
79
  * The closed set of rule BEHAVIORS — what a matching rule says about the call. The upstream vocabulary
78
80
  * verbatim (`allow` / `deny` / `ask`, the three settings lists), in PRECEDENCE order: when rules of more
@@ -538,7 +540,7 @@ export type PathRuleBaseLabelCoversEveryBase = AssertAllKeysHandled<Exclude<Path
538
540
  export declare function ruleBasesNeeded(rule: Pick<PersistedRule, "match" | "command">): readonly PathRuleBase[];
539
541
  /** Is this base usable by {@link resolvePathPattern} — an absolute path with a lexical normal form? A rule
540
542
  * compiler asks this at compile time so a pattern is refused rather than compiled inert. */
541
- export declare function isUsablePathBase(base: string | undefined): boolean;
543
+ export declare function isUsablePathBase(base: string | undefined, family: PathFamily | undefined): boolean;
542
544
  /**
543
545
  * The PATH family's tightening reach — three-valued like the command family's ({@link programRunReachOf}):
544
546
  * does this deny/ask rule reach the call's target path? The target is the caller's ALREADY-RESOLVED
@@ -555,7 +557,7 @@ export declare function isUsablePathBase(base: string | undefined): boolean;
555
557
  * defect (a `~/` deny falling back to the engine host's home guarded the wrong directory; a `/…` deny with
556
558
  * no root reached nothing at all).
557
559
  */
558
- export declare function pathRuleReachOf(rule: Pick<PersistedRule, "match" | "command" | "behavior">, target: string, bases: PathRuleBases): ProgramRunReachOutcome;
560
+ export declare function pathRuleReachOf(rule: Pick<PersistedRule, "match" | "command" | "behavior">, target: PathIdentity, bases: PathRuleBases): ProgramRunReachOutcome;
559
561
  /**
560
562
  * Does this rule's command pattern admit `command`?
561
563
  *
@@ -692,7 +694,7 @@ export declare function pathWithinRoot(path: string, root: string): boolean;
692
694
  * bodies never come through here (their canonical spelling is unique and non-normal spellings are
693
695
  * refused at the validator); this is for the comparison INPUT a caller derives from a live call.
694
696
  */
695
- export declare function lexicalNormalAbsolutePathOf(path: string): string | undefined;
697
+ export declare function lexicalNormalAbsolutePathOf(path: string, family: PathFamily | undefined, anchor?: string): PathIdentity | undefined;
696
698
  /**
697
699
  * design/382 §2.5 — the ONE admission predicate of the Read directory rule family, beside
698
700
  * {@link ruleAdmitsCommand} as its path-lane sibling. Four consumers share it (the minting round
@@ -715,7 +717,10 @@ export declare function lexicalNormalAbsolutePathOf(path: string): string | unde
715
717
  * there is nothing here for it to clear). As a DENY/ASK the same containment refuses or questions every
716
718
  * path under the directory, on whichever path tool the rule names.
717
719
  */
718
- export declare function directoryRuleAdmits(rule: Pick<PersistedRule, "match" | "command">, path: string): boolean;
720
+ export declare function directoryRuleAdmits(rule: Pick<PersistedRule, "match" | "command">, path: PathIdentity, bases: PathRuleBases): boolean;
721
+ /** The identity of a directory SPELLING (the cd resolver's `/`-rooted output) under the call's tree, for the
722
+ * consumers that hand {@link directoryRuleAdmits} a spelling; a rule minted from it stores this form. */
723
+ export declare function directoryIdentityOf(spelling: string, bases: PathRuleBases): PathIdentity | undefined;
719
724
  /** Does a rule's scope cover a task running in `cwd` (and, for the session dimension, in the session
720
725
  * named by `sessionId`)? A project rule needs a cwd to compare against; without one it covers nothing
721
726
  * (fail-closed). A session rule (design/382 §4.3) covers a call iff the call's `sessionId` equals the
@@ -872,8 +877,8 @@ export declare function adjudicatePersistedPathRules(rules: readonly PersistedRu
872
877
  cwd: string | undefined;
873
878
  sessionId?: string;
874
879
  }, targets: {
875
- readonly tighten?: string;
876
- readonly allow?: string;
880
+ readonly tighten?: PathTargetReading;
881
+ readonly allow?: PathTargetReading;
877
882
  }, bases: PathRuleBases): PersistedRuleVerdict | undefined;
878
883
  /**
879
884
  * design/375 §5.2 — the per-segment coverage table for `command`: which segments an eligible rule