@sema-agent/core 5.0.1 → 5.2.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 (71) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/dist/agents/roster-store.d.ts +9 -2
  3. package/dist/agents/roster-store.js +32 -6
  4. package/dist/agents/subagent.js +8 -3
  5. package/dist/bin/sema-tb.d.ts +1 -2
  6. package/dist/bin/sema-tb.js +14 -27
  7. package/dist/brain/anthropic.js +2 -3
  8. package/dist/brain/degrading.d.ts +2 -8
  9. package/dist/brain/degrading.js +3 -3
  10. package/dist/brain/openai.js +3 -5
  11. package/dist/brain/terminal-cause.d.ts +1 -1
  12. package/dist/brain/terminal-cause.js +2 -8
  13. package/dist/core/hooks.js +12 -7
  14. package/dist/core/lsp.js +2 -3
  15. package/dist/core/mcp.d.ts +1 -1
  16. package/dist/core/mcp.js +2 -1
  17. package/dist/core/memory-engine/dual-root.js +2 -1
  18. package/dist/core/memory-engine/engine.d.ts +4 -0
  19. package/dist/core/memory-engine/engine.js +25 -6
  20. package/dist/core/memory-engine/file-backend.d.ts +7 -5
  21. package/dist/core/memory-engine/file-backend.js +3 -1
  22. package/dist/core/memory-engine/index.d.ts +1 -1
  23. package/dist/core/memory-engine/index.js +1 -1
  24. package/dist/core/memory-engine/layout.d.ts +7 -0
  25. package/dist/core/memory-engine/layout.js +98 -5
  26. package/dist/core/memory.d.ts +0 -1
  27. package/dist/core/memory.js +7 -1
  28. package/dist/core/permission-rules.js +8 -7
  29. package/dist/core/protocol-table.d.ts +17 -0
  30. package/dist/core/protocol-table.js +37 -0
  31. package/dist/core/runner/active-skill-scope.js +7 -7
  32. package/dist/core/runner/prepare-memory.js +7 -4
  33. package/dist/core/runner/prepare-task.js +43 -26
  34. package/dist/core/runner/runtask.js +18 -6
  35. package/dist/core/runner/session-rule-policy.js +1 -1
  36. package/dist/core/runner/synthetic-tools.js +2 -2
  37. package/dist/core/runner/tool-disclosure.d.ts +0 -1
  38. package/dist/core/runner/tool-disclosure.js +6 -3
  39. package/dist/core/runner/turn-attachments.js +2 -1
  40. package/dist/core/sensitive-path-policy.js +2 -2
  41. package/dist/core/tool-policy.d.ts +1 -3
  42. package/dist/core/tool-policy.js +40 -16
  43. package/dist/index.d.ts +3 -2
  44. package/dist/index.js +2 -1
  45. package/dist/orchestration/run-spec.js +1 -1
  46. package/dist/orchestration/run-workflow-tool.js +7 -3
  47. package/dist/orchestration/workflow-script-store.d.ts +1 -1
  48. package/dist/orchestration/workflow-script-store.js +1 -1
  49. package/dist/prompt-assembly/assemble.js +0 -2
  50. package/dist/prompt-assembly/tool-catalog.d.ts +2 -1
  51. package/dist/prompts/default.d.ts +0 -2
  52. package/dist/prompts/default.js +1 -5
  53. package/dist/stores/cc/mailbox-store.d.ts +4 -0
  54. package/dist/stores/cc/mailbox-store.js +67 -9
  55. package/dist/stores/file/file-snapshot-store.d.ts +9 -1
  56. package/dist/stores/file/file-snapshot-store.js +28 -5
  57. package/dist/stores/file/fs-atomic.d.ts +4 -1
  58. package/dist/stores/file/fs-atomic.js +2 -1
  59. package/dist/stores/file/index.d.ts +10 -3
  60. package/dist/stores/file/index.js +4 -3
  61. package/dist/stores/file/mailbox-store.d.ts +5 -0
  62. package/dist/stores/file/mailbox-store.js +15 -3
  63. package/dist/stores/file/session-policy-store.d.ts +13 -1
  64. package/dist/stores/file/session-policy-store.js +39 -6
  65. package/dist/stores/file/session-store.d.ts +9 -1
  66. package/dist/stores/file/session-store.js +19 -4
  67. package/dist/tools/fs/fs-search-tools.js +10 -2
  68. package/dist/tools/fs/fs-shared.d.ts +1 -2
  69. package/dist/tools/fs/fs-shared.js +1 -2
  70. package/dist/tools/todo.js +14 -7
  71. package/package.json +1 -1
@@ -6,7 +6,7 @@ import { inlineUntrusted } from "../untrusted-text.js";
6
6
  import { formatMemoryAge } from "../memory-recall.js";
7
7
  import { computeEntryRev, parseEntryFile, serializeEntryFile } from "./frontmatter.js";
8
8
  import { DEFAULT_MAX_ENTRY_DEPTH, MEMORY_INDEX_FILENAME, scanEntryFiles } from "./file-backend.js";
9
- import { QUARANTINE_DIR, SCAN_FUSE_THRESHOLD, quarantineAndTombstone, readIndexRevs, writeIndexRevs, bumpScanFuse, canonicalize, claimRootScope, clearScanFuse, deriveControlPlaneDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, ensureDirExists, isContainedIn, registerScope, registeredScopes, resolveMemoryEngineRoot, scopeDirFor, } from "./layout.js";
9
+ import { QUARANTINE_DIR, SCAN_FUSE_THRESHOLD, quarantineAndTombstone, readIndexRevs, writeIndexRevs, bumpScanFuse, canonicalize, claimRootScope, clearScanFuse, adoptCanonicalKeyedControlDir, deriveControlPlaneDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, ensureDirExists, isContainedIn, registerScope, registeredScopes, resolveMemoryEngineRoot, scopeDirFor, } from "./layout.js";
10
10
  import { scanMemoryFileName, scanMemoryWrite, scanRemediation } from "./scan.js";
11
11
  export const MEMORY_INSTRUCTION_TEMPLATE = `# Memory
12
12
 
@@ -50,6 +50,7 @@ export class MemoryEngine {
50
50
  maxDepth;
51
51
  harvestDeadlineMs;
52
52
  harvestFileBudget;
53
+ onIncident;
53
54
  backendPinnedRoot;
54
55
  constructor(opts) {
55
56
  this.backend = opts.backend;
@@ -61,7 +62,7 @@ export class MemoryEngine {
61
62
  this.controlDir =
62
63
  typeof backendControl === "string" && backendControl
63
64
  ? backendControl
64
- : (opts.controlDir ?? deriveControlPlaneDir(resolveMemoryEngineRoot(), opts.memoryDir));
65
+ : (opts.controlDir ?? (adoptCanonicalKeyedControlDir(resolveMemoryEngineRoot(), opts.memoryDir, opts.onIncident), deriveControlPlaneDir(resolveMemoryEngineRoot(), opts.memoryDir)));
65
66
  this.now = opts.now ?? Date.now;
66
67
  this.perScopeBudgetBytes = opts.perScopeBudgetBytes ?? Number.MAX_SAFE_INTEGER;
67
68
  this.perFileBytes = opts.perFileBytes ?? MAX_MEMORY_BYTES;
@@ -69,6 +70,20 @@ export class MemoryEngine {
69
70
  this.maxDepth = opts.maxDepth ?? DEFAULT_MAX_ENTRY_DEPTH;
70
71
  this.harvestDeadlineMs = opts.harvestDeadlineMs ?? DEFAULT_HARVEST_DEADLINE_MS;
71
72
  this.harvestFileBudget = opts.harvestFileBudget ?? DEFAULT_HARVEST_FILE_BUDGET;
73
+ this.onIncident = opts.onIncident;
74
+ }
75
+ discloseAnnounceFailure(stage, cause) {
76
+ const sink = this.onIncident;
77
+ if (sink === undefined)
78
+ return;
79
+ try {
80
+ const detail = cause instanceof Error ? cause.message : String(cause);
81
+ const err = new Error(`memory announcement queue ${stage} failed: ${detail}`);
82
+ err.code = "memory.announce_failed";
83
+ sink(err);
84
+ }
85
+ catch {
86
+ }
72
87
  }
73
88
  async materialize(scopes, writeScope) {
74
89
  ensureDirExists(this.memoryDir);
@@ -156,7 +171,8 @@ export class MemoryEngine {
156
171
  items: [`memory index rejected and rebuilt: ${inlineUntrusted(indexGate.reason)}`],
157
172
  });
158
173
  }
159
- catch {
174
+ catch (err) {
175
+ this.discloseAnnounceFailure("index-gate enqueue", err);
160
176
  }
161
177
  }
162
178
  const indexText = this.rebuildIndex(handle, headers, { write: writeScope !== null });
@@ -191,7 +207,8 @@ export class MemoryEngine {
191
207
  announceBlock = renderAnnouncements(drained.queue, drained.folded);
192
208
  }
193
209
  }
194
- catch {
210
+ catch (err) {
211
+ this.discloseAnnounceFailure("inject drain", err);
195
212
  }
196
213
  const block = [instruction, index, announceBlock].filter((s) => Boolean(s && s.trim())).join("\n\n");
197
214
  return {
@@ -239,7 +256,8 @@ export class MemoryEngine {
239
256
  enqueueMemoryAnnouncement(this.controlDir, { kind: "external", at: this.now(), items: capItems(items) });
240
257
  }
241
258
  }
242
- catch {
259
+ catch (err) {
260
+ this.discloseAnnounceFailure("harvest enqueue", err);
243
261
  }
244
262
  return report;
245
263
  }
@@ -812,7 +830,8 @@ export class MemoryEngine {
812
830
  .concat(sweepSkipped.length > 12 ? [`…and ${sweepSkipped.length - 12} more sweepSkipped path(s)`] : []),
813
831
  });
814
832
  }
815
- catch {
833
+ catch (err) {
834
+ this.discloseAnnounceFailure("mode-sweep enqueue", err);
816
835
  }
817
836
  }
818
837
  }
@@ -1,4 +1,4 @@
1
- import { scopeDirName } from "./layout.js";
1
+ import { scopeDirName, type MemoryPartitionIncidentSink } from "./layout.js";
2
2
  import type { HarvestRejection, MemoryBackend, MemoryEntry, MemoryEntryHeader, NotePatch, PatchReport, ScoredMemoryEntry } from "./types.js";
3
3
  export declare const MEMORY_INDEX_FILENAME = "MEMORY.md";
4
4
  export declare const DEFAULT_MAX_ENTRY_DEPTH = 3;
@@ -11,6 +11,11 @@ export declare function scanEntryFiles(dir: string, opts?: {
11
11
  exclude?: ReadonlySet<string>;
12
12
  onSkip?: (path: string, kind: "symlink" | "depth" | "nonmd" | "dotfile" | "unreadable") => void;
13
13
  }): ScannedEntryFile[];
14
+ export interface FileMemoryEngineBackendOptions {
15
+ controlDir?: string;
16
+ now?: () => number;
17
+ onIncident?: MemoryPartitionIncidentSink;
18
+ }
14
19
  export declare class FileMemoryEngineBackend implements MemoryBackend {
15
20
  readonly directoryRoot: string;
16
21
  readonly controlPlaneRoot: string;
@@ -18,10 +23,7 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
18
23
  private ledger;
19
24
  private inboundFindings;
20
25
  private batchScan;
21
- constructor(dir: string, opts?: {
22
- controlDir?: string;
23
- now?: () => number;
24
- });
26
+ constructor(dir: string, opts?: FileMemoryEngineBackendOptions);
25
27
  checkControlPlane(): void;
26
28
  drainInboundFindings(): HarvestRejection[];
27
29
  readCommittedShadow(id: string): string | undefined;
@@ -5,7 +5,7 @@ import { jaccardDistance, termSet } from "../memory-vector.js";
5
5
  import { MAX_MEMORY_BYTES } from "../memory.js";
6
6
  import { inlineUntrusted } from "../untrusted-text.js";
7
7
  import { computeEntryRev, entryFromFile, isValidEntryId, parseEntryFile, serializeEntryFile } from "./frontmatter.js";
8
- import { ControlPlaneCorruptError, CURSORS_FILE, QUARANTINE_DIR, atomicWriteFileSync, quarantineAndTombstone, claimRootScope, deriveControlPlaneDir, ensureDirExists, enqueueMemoryAnnouncement, registerScope, registeredScopes, resolveMemoryEngineRoot, scopeDirName, writeAllSync, } from "./layout.js";
8
+ import { ControlPlaneCorruptError, CURSORS_FILE, QUARANTINE_DIR, atomicWriteFileSync, quarantineAndTombstone, claimRootScope, adoptCanonicalKeyedControlDir, deriveControlPlaneDir, ensureDirExists, enqueueMemoryAnnouncement, registerScope, registeredScopes, resolveMemoryEngineRoot, scopeDirName, writeAllSync, } from "./layout.js";
9
9
  import { scanMemoryFileName, scanMemoryWrite } from "./scan.js";
10
10
  export const MEMORY_INDEX_FILENAME = "MEMORY.md";
11
11
  export const DEFAULT_MAX_ENTRY_DEPTH = 3;
@@ -78,6 +78,8 @@ export class FileMemoryEngineBackend {
78
78
  constructor(dir, opts = {}) {
79
79
  this.now = opts.now ?? Date.now;
80
80
  this.directoryRoot = dir;
81
+ if (opts.controlDir === undefined)
82
+ adoptCanonicalKeyedControlDir(resolveMemoryEngineRoot(), dir, opts.onIncident);
81
83
  this.controlPlaneRoot = opts.controlDir ?? deriveControlPlaneDir(resolveMemoryEngineRoot(), dir);
82
84
  ensureDirExists(dir);
83
85
  ensureDirExists(this.controlPlaneRoot);
@@ -1,7 +1,7 @@
1
1
  export { MemoryEngine, buildMemoryInstruction, truncateIndex, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, renderAnnouncements, type MemoryEngineOptions, type MemoryInjection, } from "./engine.js";
2
2
  export { scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE } from "./scan.js";
3
3
  export { FileMemoryEngineBackend, scanEntryFiles, MEMORY_INDEX_FILENAME, DEFAULT_MAX_ENTRY_DEPTH, type ScannedEntryFile } from "./file-backend.js";
4
- export { ControlPlaneCorruptError, deriveControlPlaneDir, deriveRepoKey, deriveRepoMemoryDir, deriveProjectMemoryDir, deriveProjectControlDir, recordProjectIdHint, lookupProjectIdHint, PROJECT_ID_HINTS_FILE, resolveMemoryEngineRoot, scopeDirFor, scopeDirName, claimRootScope, rootScopeOf, enqueueMemoryAnnouncement, drainMemoryAnnouncements, peekMemoryAnnouncements, bumpScanFuse, scanFuseCount, clearScanFuse, ANNOUNCEMENTS_FILE, MEMORY_ANNOUNCEMENTS_MAX, SCAN_FUSE_FILE, SCAN_FUSE_THRESHOLD, } from "./layout.js";
4
+ export { ControlPlaneCorruptError, deriveControlPlaneDir, deriveRepoControlPlaneDir, deriveRepoKey, deriveRepoMemoryDir, deriveProjectMemoryDir, deriveProjectControlDir, recordProjectIdHint, lookupProjectIdHint, PROJECT_ID_HINTS_FILE, resolveMemoryEngineRoot, scopeDirFor, scopeDirName, claimRootScope, rootScopeOf, enqueueMemoryAnnouncement, drainMemoryAnnouncements, peekMemoryAnnouncements, bumpScanFuse, scanFuseCount, clearScanFuse, ANNOUNCEMENTS_FILE, MEMORY_ANNOUNCEMENTS_MAX, SCAN_FUSE_FILE, SCAN_FUSE_THRESHOLD, } from "./layout.js";
5
5
  export { parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, type ParsedEntryFile } from "./frontmatter.js";
6
6
  export type { MemoryBackend, MemoryEntry, MemoryEntryFrontmatter, MemoryEntryHeader, ScoredMemoryEntry, NotePatch, PatchReport, MaterializedFile, MemorySessionHandle, HarvestReport, HarvestRejection, HarvestRejectionCode, MemoryAnnouncement, ScanFinding, } from "./types.js";
7
7
  export { memoryBackendContract, assertMemoryBackendSearchEquivalence, type MemoryBackendContractHooks, } from "./memory-backend-contract.js";
@@ -1,7 +1,7 @@
1
1
  export { MemoryEngine, buildMemoryInstruction, truncateIndex, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, renderAnnouncements, } from "./engine.js";
2
2
  export { scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE } from "./scan.js";
3
3
  export { FileMemoryEngineBackend, scanEntryFiles, MEMORY_INDEX_FILENAME, DEFAULT_MAX_ENTRY_DEPTH } from "./file-backend.js";
4
- export { ControlPlaneCorruptError, deriveControlPlaneDir, deriveRepoKey, deriveRepoMemoryDir, deriveProjectMemoryDir, deriveProjectControlDir, recordProjectIdHint, lookupProjectIdHint, PROJECT_ID_HINTS_FILE, resolveMemoryEngineRoot, scopeDirFor, scopeDirName, claimRootScope, rootScopeOf, enqueueMemoryAnnouncement, drainMemoryAnnouncements, peekMemoryAnnouncements, bumpScanFuse, scanFuseCount, clearScanFuse, ANNOUNCEMENTS_FILE, MEMORY_ANNOUNCEMENTS_MAX, SCAN_FUSE_FILE, SCAN_FUSE_THRESHOLD, } from "./layout.js";
4
+ export { ControlPlaneCorruptError, deriveControlPlaneDir, deriveRepoControlPlaneDir, deriveRepoKey, deriveRepoMemoryDir, deriveProjectMemoryDir, deriveProjectControlDir, recordProjectIdHint, lookupProjectIdHint, PROJECT_ID_HINTS_FILE, resolveMemoryEngineRoot, scopeDirFor, scopeDirName, claimRootScope, rootScopeOf, enqueueMemoryAnnouncement, drainMemoryAnnouncements, peekMemoryAnnouncements, bumpScanFuse, scanFuseCount, clearScanFuse, ANNOUNCEMENTS_FILE, MEMORY_ANNOUNCEMENTS_MAX, SCAN_FUSE_FILE, SCAN_FUSE_THRESHOLD, } from "./layout.js";
5
5
  export { parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile } from "./frontmatter.js";
6
6
  export { memoryBackendContract, assertMemoryBackendSearchEquivalence, } from "./memory-backend-contract.js";
7
7
  export { SCOPE_SEGMENT_MAX_ENCODED, PROJECT_MARKER_PATH, encodeScopeSegment, decodeScopeSegment, parseScopeKey, formatUserScope, formatOrgScope, formatProjScope, formatUserProjScope, isPersonalScope, assertScopeContractPlacement, formatProjectMarker, parseProjectMarker, resolveProjectId, PROJECT_ID_REGEX, } from "./scope-contract.js";
@@ -1,11 +1,17 @@
1
1
  import type { MemoryAnnouncement } from "./types.js";
2
2
  export declare const CURSORS_FILE = "cursors.json";
3
+ export declare const CONTROL_PLANE_DIR = ".engine";
3
4
  export declare class ControlPlaneCorruptError extends Error {
4
5
  constructor(message: string, opts?: {
5
6
  cause?: unknown;
6
7
  });
7
8
  }
8
9
  export declare function deriveRepoKey(repoRoot: string): string;
10
+ export type MemoryPartitionIncidentSink = (err: Error & {
11
+ code?: string;
12
+ }) => void;
13
+ export declare function adoptLegacyRepoDirs(configRoot: string, repoRoot: string, onIncident?: MemoryPartitionIncidentSink): void;
14
+ export declare function adoptCanonicalKeyedControlDir(configRoot: string, dir: string, onIncident?: MemoryPartitionIncidentSink): void;
9
15
  export declare function deriveRepoMemoryDir(configRoot: string, repoRoot: string): string;
10
16
  export declare function deriveProjectMemoryDir(configRoot: string, projectId: string): string;
11
17
  export declare function deriveProjectControlDir(configRoot: string, projectId: string): string;
@@ -13,6 +19,7 @@ export declare const PROJECT_ID_HINTS_FILE = "project-id-hints.json";
13
19
  export declare function recordProjectIdHint(configRoot: string, repoRoot: string, projectId: string): void;
14
20
  export declare function lookupProjectIdHint(configRoot: string, repoRoot: string): string | undefined;
15
21
  export declare function deriveControlPlaneDir(configRoot: string, key: string): string;
22
+ export declare function deriveRepoControlPlaneDir(configRoot: string, repoRoot: string): string;
16
23
  export declare function resolveMemoryEngineRoot(explicit?: string): string;
17
24
  export declare function scopeDirName(scope: string): string;
18
25
  export declare function rootScopeOf(controlDir: string): string | undefined;
@@ -1,17 +1,106 @@
1
- import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
1
+ import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { createHash } from "node:crypto";
4
4
  import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
5
5
  const SCOPES_FILE = "scopes.json";
6
6
  export const CURSORS_FILE = "cursors.json";
7
+ export const CONTROL_PLANE_DIR = ".engine";
7
8
  export class ControlPlaneCorruptError extends Error {
8
9
  constructor(message, opts) {
9
10
  super(message, opts);
10
11
  this.name = "ControlPlaneCorruptError";
11
12
  }
12
13
  }
14
+ function slugifyPath(absPath) {
15
+ return absPath.normalize("NFC").replace(/[^A-Za-z0-9]/g, "-");
16
+ }
13
17
  export function deriveRepoKey(repoRoot) {
14
- return resolve(repoRoot).normalize("NFC").replace(/[^A-Za-z0-9]/g, "-");
18
+ return slugifyPath(canonicalize(resolve(repoRoot)));
19
+ }
20
+ function deriveDirKey(dir) {
21
+ return slugifyPath(resolve(dir));
22
+ }
23
+ function subtreeHasFiles(dir) {
24
+ let entries;
25
+ try {
26
+ entries = readdirSync(dir, { withFileTypes: true });
27
+ }
28
+ catch {
29
+ return true;
30
+ }
31
+ for (const e of entries) {
32
+ if (!e.isDirectory())
33
+ return true;
34
+ if (subtreeHasFiles(join(dir, e.name)))
35
+ return true;
36
+ }
37
+ return false;
38
+ }
39
+ function migrateKeyedPartition(m) {
40
+ const disclose = (code, message) => {
41
+ try {
42
+ const e = new Error(message);
43
+ e.code = code;
44
+ m.onIncident?.(e);
45
+ }
46
+ catch {
47
+ }
48
+ };
49
+ 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.`;
50
+ try {
51
+ if (m.fromDir === m.toDir)
52
+ return;
53
+ if (!existsSync(m.fromDir))
54
+ return;
55
+ if (existsSync(m.toDir)) {
56
+ if (subtreeHasFiles(m.toDir)) {
57
+ disclose("memory.partition_split", splitText);
58
+ return;
59
+ }
60
+ try {
61
+ rmSync(m.toDir, { recursive: true });
62
+ }
63
+ catch (err) {
64
+ const code = err.code;
65
+ disclose("memory.partition_adopt_failed", `could not clear the empty directory scaffolding at ${m.toDir} before adopting ${m.fromDir} (${code ?? String(err)}): ` +
66
+ `this session proceeds on ${m.toDir}; ${m.fromDir} still holds the prior memory — move it by hand.`);
67
+ return;
68
+ }
69
+ }
70
+ try {
71
+ ensureDirExists(dirname(m.toDir));
72
+ renameSync(m.fromDir, m.toDir);
73
+ }
74
+ catch (err) {
75
+ const code = err.code;
76
+ if ((code === "ENOENT" || code === "EEXIST" || code === "ENOTEMPTY") && existsSync(m.toDir)) {
77
+ if (!existsSync(m.fromDir))
78
+ return;
79
+ disclose("memory.partition_split", splitText);
80
+ return;
81
+ }
82
+ disclose("memory.partition_adopt_failed", `could not adopt the memory partition ${m.fromDir} → ${m.toDir} (${code ?? String(err)}): this session proceeds on ${m.toDir}; ` +
83
+ `${m.fromDir} still holds the prior memory — move it by hand.`);
84
+ }
85
+ }
86
+ catch {
87
+ }
88
+ }
89
+ export function adoptLegacyRepoDirs(configRoot, repoRoot, onIncident) {
90
+ migrateKeyedPartition({
91
+ fromDir: join(configRoot, deriveDirKey(repoRoot)),
92
+ toDir: join(configRoot, deriveRepoKey(repoRoot)),
93
+ subject: repoRoot,
94
+ onIncident,
95
+ });
96
+ }
97
+ export function adoptCanonicalKeyedControlDir(configRoot, dir, onIncident) {
98
+ migrateKeyedPartition({
99
+ fromDir: join(configRoot, deriveRepoKey(dir), CONTROL_PLANE_DIR),
100
+ toDir: join(configRoot, deriveDirKey(dir), CONTROL_PLANE_DIR),
101
+ subject: dir,
102
+ onIncident,
103
+ });
15
104
  }
16
105
  export function deriveRepoMemoryDir(configRoot, repoRoot) {
17
106
  return join(configRoot, deriveRepoKey(repoRoot), "memory");
@@ -20,7 +109,7 @@ export function deriveProjectMemoryDir(configRoot, projectId) {
20
109
  return join(configRoot, `proj-${projectId.toLowerCase()}`, "memory");
21
110
  }
22
111
  export function deriveProjectControlDir(configRoot, projectId) {
23
- return join(configRoot, `proj-${projectId.toLowerCase()}`, ".engine");
112
+ return join(configRoot, `proj-${projectId.toLowerCase()}`, CONTROL_PLANE_DIR);
24
113
  }
25
114
  export const PROJECT_ID_HINTS_FILE = "project-id-hints.json";
26
115
  export function recordProjectIdHint(configRoot, repoRoot, projectId) {
@@ -57,7 +146,10 @@ export function lookupProjectIdHint(configRoot, repoRoot) {
57
146
  }
58
147
  }
59
148
  export function deriveControlPlaneDir(configRoot, key) {
60
- return join(configRoot, deriveRepoKey(key), ".engine");
149
+ return join(configRoot, deriveDirKey(key), CONTROL_PLANE_DIR);
150
+ }
151
+ export function deriveRepoControlPlaneDir(configRoot, repoRoot) {
152
+ return join(configRoot, deriveRepoKey(repoRoot), CONTROL_PLANE_DIR);
61
153
  }
62
154
  export function resolveMemoryEngineRoot(explicit) {
63
155
  const raw = explicit ?? process.env.AGENT_DATA_DIR ?? join(homedir(), ".ai-agent");
@@ -178,7 +270,8 @@ export function canonicalize(p) {
178
270
  const parent = resolve(p, "..");
179
271
  if (parent === p)
180
272
  return p;
181
- return join(canonicalize(parent), p.slice(parent.length + 1) || "");
273
+ const rest = p.slice(parent.endsWith(sep) ? parent.length : parent.length + 1);
274
+ return join(canonicalize(parent), rest);
182
275
  }
183
276
  }
184
277
  export function isContainedIn(root, child) {
@@ -91,7 +91,6 @@ export interface NormalizedMemorySpec {
91
91
  scopeContract?: "v2";
92
92
  }
93
93
  export type MemorySpecInput = {
94
- scope?: string;
95
94
  scopes?: ReadonlyArray<string>;
96
95
  writeScope?: string | null;
97
96
  enabled?: boolean;
@@ -354,7 +354,13 @@ function mtimeMsOf(ts) {
354
354
  export function normalizeMemorySpec(input) {
355
355
  if (!input)
356
356
  return undefined;
357
- const raw = input.scopes && input.scopes.length > 0 ? input.scopes : input.scope !== undefined ? [input.scope] : [];
357
+ if (Object.prototype.hasOwnProperty.call(input, "scope")) {
358
+ const e = new Error("TaskSpec.memory: the singular `scope` field was removed — write `scopes: [\"<scope>\"]` instead " +
359
+ "(an ordered read layering; `writeScope` defaults to its last entry). Refusing to start with memory silently off.");
360
+ e.code = "config.memory_scope_spelling";
361
+ throw e;
362
+ }
363
+ const raw = input.scopes ?? [];
358
364
  const scopes = [];
359
365
  const seen = new Set();
360
366
  for (const s of raw) {
@@ -1,3 +1,4 @@
1
+ import { MCP_NAMESPACE } from "./protocol-table.js";
1
2
  export const BASH_GENERIC_PARAMS = new Set(["command", "timeout", "description", "run_in_background"]);
2
3
  const DEFAULT_CAPS = {
3
4
  maxRules: 256,
@@ -95,7 +96,7 @@ const FILE_TOOL_PARAMS = {
95
96
  Write: new Set(["file_path", "path", "content"]),
96
97
  NotebookEdit: new Set(["notebook_path", "path", "cell_id", "cell_type", "edit_mode", "new_source"]),
97
98
  Grep: new Set([
98
- "pattern", "path", "glob", "type", "output_mode", "head_limit", "max_results", "offset",
99
+ "pattern", "path", "glob", "type", "output_mode", "head_limit", "offset",
99
100
  "multiline", "context", "ignore_case", "-i", "-n", "-o", "-A", "-B", "-C",
100
101
  ]),
101
102
  Glob: new Set(["pattern", "path", "max_results"]),
@@ -142,7 +143,7 @@ function compile(rules, caps, primaryFieldGeneric) {
142
143
  }
143
144
  }
144
145
  const toolName = parsed.toolName;
145
- if (toolName.startsWith("mcp__") && (parsed.ruleContent !== undefined || indexOfUnescaped(text, "(") !== -1)) {
146
+ if (toolName.startsWith(MCP_NAMESPACE.prefix) && (parsed.ruleContent !== undefined || indexOfUnescaped(text, "(") !== -1)) {
146
147
  bad(text, "unsupported.mcp_paren", "MCP rules do not support patterns in parentheses (CC parity); use the toolAxes system");
147
148
  continue;
148
149
  }
@@ -274,18 +275,18 @@ export function createPermissionRulePolicy(rules, opts) {
274
275
  const entry = byTool.get(req.toolName);
275
276
  if (entry) {
276
277
  if (entry.bare.deny) {
277
- return { action: "deny", reason: ruleMessage("denied", entry.bare.deny.ruleText, entry.bare.deny.source) };
278
+ return { action: "deny", message: ruleMessage("denied", entry.bare.deny.ruleText, entry.bare.deny.source) };
278
279
  }
279
280
  const paramDeny = matchParamRules(entry.param.deny, req.args, caps.maxScalarValueChars);
280
281
  if (paramDeny) {
281
- return { action: "deny", reason: ruleMessage("denied", paramDeny.ruleText, paramDeny.source) };
282
+ return { action: "deny", message: ruleMessage("denied", paramDeny.ruleText, paramDeny.source) };
282
283
  }
283
284
  if (entry.bare.ask) {
284
- return { action: "ask", reason: ruleMessage("flagged", entry.bare.ask.ruleText, entry.bare.ask.source) };
285
+ return { action: "ask", message: ruleMessage("flagged", entry.bare.ask.ruleText, entry.bare.ask.source) };
285
286
  }
286
287
  const paramAsk = matchParamRules(entry.param.ask, req.args, caps.maxScalarValueChars);
287
288
  if (paramAsk) {
288
- return { action: "ask", reason: ruleMessage("flagged", paramAsk.ruleText, paramAsk.source) };
289
+ return { action: "ask", message: ruleMessage("flagged", paramAsk.ruleText, paramAsk.source) };
289
290
  }
290
291
  if (entry.bare.allow) {
291
292
  return { action: "allow" };
@@ -293,7 +294,7 @@ export function createPermissionRulePolicy(rules, opts) {
293
294
  }
294
295
  if (defaultAction === "allow")
295
296
  return { action: "allow" };
296
- return { action: defaultAction, reason: `no permission rule matched "${req.toolName}" (default-${defaultAction} rule set)` };
297
+ return { action: defaultAction, message: `no permission rule matched "${req.toolName}" (default-${defaultAction} rule set)` };
297
298
  },
298
299
  };
299
300
  }
@@ -0,0 +1,17 @@
1
+ export type ProtocolId = "mcp";
2
+ export interface ProtocolPeerTool {
3
+ peer: string;
4
+ tool: string;
5
+ }
6
+ export interface ProtocolNamespace<P extends string = string> {
7
+ readonly id: ProtocolId;
8
+ readonly prefix: P;
9
+ makeName(peer: string, tool: string): string;
10
+ parse(name: string): ProtocolPeerTool | undefined;
11
+ displayGroupKey(name: string): string;
12
+ }
13
+ declare const MCP_PREFIX_NAME: "mcp__";
14
+ export declare const MCP_NAMESPACE: ProtocolNamespace<typeof MCP_PREFIX_NAME>;
15
+ export declare const PROTOCOL_TABLE: readonly ProtocolNamespace[];
16
+ export declare function protocolOf(name: string): ProtocolNamespace | undefined;
17
+ export {};
@@ -0,0 +1,37 @@
1
+ const NAME_SEP = "__";
2
+ const MCP_PREFIX_NAME = `mcp${NAME_SEP}`;
3
+ const parseMcpName = (name) => {
4
+ if (!name.startsWith(MCP_PREFIX_NAME))
5
+ return undefined;
6
+ const rest = name.slice(MCP_PREFIX_NAME.length);
7
+ const sep = rest.indexOf(NAME_SEP);
8
+ if (sep <= 0)
9
+ return undefined;
10
+ const tool = rest.slice(sep + NAME_SEP.length);
11
+ if (tool.length === 0)
12
+ return undefined;
13
+ return { peer: rest.slice(0, sep), tool };
14
+ };
15
+ const makeMcpName = (peer, tool) => {
16
+ if (peer.length === 0 || tool.length === 0) {
17
+ throw new Error(`protocol table (mcp): peer and tool must both be non-empty (got peer=${JSON.stringify(peer)}, tool=${JSON.stringify(tool)})`);
18
+ }
19
+ if (peer.includes(NAME_SEP)) {
20
+ throw new Error(`protocol table (mcp): peer must not contain the ${NAME_SEP} separator (got ${JSON.stringify(peer)}) — the composed name would parse back as a DIFFERENT peer`);
21
+ }
22
+ return `${MCP_PREFIX_NAME}${peer}${NAME_SEP}${tool}`;
23
+ };
24
+ export const MCP_NAMESPACE = {
25
+ id: "mcp",
26
+ prefix: MCP_PREFIX_NAME,
27
+ makeName: makeMcpName,
28
+ parse: parseMcpName,
29
+ displayGroupKey: (name) => {
30
+ const p = parseMcpName(name);
31
+ return p === undefined ? name : `${MCP_PREFIX_NAME}${p.peer}${NAME_SEP}*`;
32
+ },
33
+ };
34
+ export const PROTOCOL_TABLE = [MCP_NAMESPACE];
35
+ export function protocolOf(name) {
36
+ return PROTOCOL_TABLE.find((ns) => name.startsWith(ns.prefix));
37
+ }
@@ -28,7 +28,7 @@ export function createActiveSkillScopePolicy(opts) {
28
28
  if (unresolved && unresolved.kind === "unresolved") {
29
29
  return {
30
30
  action: "deny",
31
- reason: `tool "${req.toolName}" denied: active skill artifact "${unresolved.lineageId}" manifest could not be resolved (${unresolved.reason}) — fail-closed deny-all`,
31
+ message: `tool "${req.toolName}" denied: active skill artifact "${unresolved.lineageId}" manifest could not be resolved (${unresolved.reason}) — fail-closed deny-all`,
32
32
  decisionReason: "safety",
33
33
  };
34
34
  }
@@ -49,7 +49,7 @@ export function createActiveSkillScopePolicy(opts) {
49
49
  const ids = manifests.map((m) => m.lineageId).join(", ");
50
50
  return {
51
51
  action: "deny",
52
- reason: `tool "${req.toolName}" is not in the active skill manifest allowlist (artifact(s): ${ids})`,
52
+ message: `tool "${req.toolName}" is not in the active skill manifest allowlist (artifact(s): ${ids})`,
53
53
  decisionReason: "safety",
54
54
  };
55
55
  }
@@ -68,7 +68,7 @@ export function createActiveSkillScopePolicy(opts) {
68
68
  if (!admitted) {
69
69
  return {
70
70
  action: "deny",
71
- reason: `tool "${req.toolName}" is narrowed by skill manifest "${frame.manifest.lineageId}": ${rejections.join("; ")}`,
71
+ message: `tool "${req.toolName}" is narrowed by skill manifest "${frame.manifest.lineageId}": ${rejections.join("; ")}`,
72
72
  decisionReason: "safety",
73
73
  };
74
74
  }
@@ -80,7 +80,7 @@ export function createActiveSkillScopePolicy(opts) {
80
80
  const ids = manifests.filter((m) => m.allowPaths?.length).map((m) => m.lineageId).join(", ");
81
81
  return {
82
82
  action: "deny",
83
- reason: `write-capable tool "${req.toolName}" denied: active skill manifest constrains writes to allowPaths but this tool's target cannot be path-confined (only ${[...PATH_WRITE_TOOLS].join("/")} are) — fail-closed (artifact(s): ${ids})`,
83
+ message: `write-capable tool "${req.toolName}" denied: active skill manifest constrains writes to allowPaths but this tool's target cannot be path-confined (only ${[...PATH_WRITE_TOOLS].join("/")} are) — fail-closed (artifact(s): ${ids})`,
84
84
  decisionReason: "safety",
85
85
  };
86
86
  }
@@ -92,7 +92,7 @@ export function createActiveSkillScopePolicy(opts) {
92
92
  if (typeof path !== "string" || path.length === 0) {
93
93
  return {
94
94
  action: "deny",
95
- reason: `write tool "${req.toolName}" denied: active skill manifest constrains writes to allowPaths but the call has no resolvable path`,
95
+ message: `write tool "${req.toolName}" denied: active skill manifest constrains writes to allowPaths but the call has no resolvable path`,
96
96
  decisionReason: "safety",
97
97
  };
98
98
  }
@@ -100,7 +100,7 @@ export function createActiveSkillScopePolicy(opts) {
100
100
  if (!canon.ok) {
101
101
  return {
102
102
  action: "deny",
103
- reason: `write to "${path}" denied: its real target could not be resolved against the active skill manifest allowPaths`,
103
+ message: `write to "${path}" denied: its real target could not be resolved against the active skill manifest allowPaths`,
104
104
  decisionReason: "safety",
105
105
  };
106
106
  }
@@ -115,7 +115,7 @@ export function createActiveSkillScopePolicy(opts) {
115
115
  if (!inside) {
116
116
  return {
117
117
  action: "deny",
118
- reason: `write to "${path}" is outside the paths allowed by skill manifest "${m.lineageId}" (allowPaths: ${(m.allowPaths ?? []).join(", ")})`,
118
+ message: `write to "${path}" is outside the paths allowed by skill manifest "${m.lineageId}" (allowPaths: ${(m.allowPaths ?? []).join(", ")})`,
119
119
  decisionReason: "safety",
120
120
  };
121
121
  }
@@ -1,4 +1,4 @@
1
- import { deriveControlPlaneDir, deriveProjectControlDir, deriveProjectMemoryDir, deriveRepoMemoryDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, lookupProjectIdHint, recordProjectIdHint, resolveMemoryEngineRoot } from "../memory-engine/layout.js";
1
+ import { adoptLegacyRepoDirs, deriveRepoControlPlaneDir, deriveProjectControlDir, deriveProjectMemoryDir, deriveRepoMemoryDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, lookupProjectIdHint, recordProjectIdHint, resolveMemoryEngineRoot } from "../memory-engine/layout.js";
2
2
  import { classifyScopePlanes, derivePersonalControlDir, derivePersonalMemoryDir, mergeHarvestReports, mergeInjections, needsDualRoots, parsedProjectPlane } from "../memory-engine/dual-root.js";
3
3
  import { normalizeMemorySpec } from "../memory.js";
4
4
  import { MemoryEngine } from "../memory-engine/engine.js";
@@ -15,6 +15,9 @@ export async function prepareMemory(input) {
15
15
  const pinned = backend.directoryRoot;
16
16
  const engineRoot = resolveMemoryEngineRoot(deps.memoryEngineDir);
17
17
  const repoRoot = deps.rootPath ?? taskRootPath;
18
+ if (repoRoot) {
19
+ adoptLegacyRepoDirs(engineRoot, repoRoot, (err) => deps.onError?.(err, { phase: "memory", sessionId }));
20
+ }
18
21
  let identityKey;
19
22
  if (memorySpec.scopeContract === "v2" && repoRoot) {
20
23
  const consumesProjectIdentity = [...memorySpec.scopes, ...(memorySpec.writeScope !== null ? [memorySpec.writeScope] : [])]
@@ -35,7 +38,7 @@ export async function prepareMemory(input) {
35
38
  recordProjectIdHint(engineRoot, repoRoot, marker.projectId);
36
39
  if (firstSwitch) {
37
40
  try {
38
- const oldCtl = deriveControlPlaneDir(engineRoot, repoRoot);
41
+ const oldCtl = deriveRepoControlPlaneDir(engineRoot, repoRoot);
39
42
  const newCtl = deriveProjectControlDir(engineRoot, marker.projectId);
40
43
  const drained = drainMemoryAnnouncements(oldCtl);
41
44
  for (const ann of drained.queue)
@@ -97,7 +100,7 @@ export async function prepareMemory(input) {
97
100
  const projectEngine = new MemoryEngine({
98
101
  backend,
99
102
  memoryDir,
100
- controlDir: identityKey !== undefined ? deriveProjectControlDir(engineRoot, identityKey) : deriveControlPlaneDir(engineRoot, repoRoot),
103
+ controlDir: identityKey !== undefined ? deriveProjectControlDir(engineRoot, identityKey) : deriveRepoControlPlaneDir(engineRoot, repoRoot),
101
104
  });
102
105
  const personalEngine = createPersonalEngine();
103
106
  const p = planes;
@@ -137,7 +140,7 @@ export async function prepareMemory(input) {
137
140
  const engine = new MemoryEngine({
138
141
  backend,
139
142
  memoryDir,
140
- controlDir: identityKey !== undefined ? deriveProjectControlDir(engineRoot, identityKey) : deriveControlPlaneDir(engineRoot, repoRoot),
143
+ controlDir: identityKey !== undefined ? deriveProjectControlDir(engineRoot, identityKey) : deriveRepoControlPlaneDir(engineRoot, repoRoot),
141
144
  });
142
145
  const handle = await engine.materialize(memorySpec.scopes, memorySpec.writeScope);
143
146
  writeEngine = engine;