@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.
- package/CHANGELOG.md +67 -0
- package/dist/agents/roster-store.d.ts +9 -2
- package/dist/agents/roster-store.js +32 -6
- package/dist/agents/subagent.js +8 -3
- package/dist/bin/sema-tb.d.ts +1 -2
- package/dist/bin/sema-tb.js +14 -27
- package/dist/brain/anthropic.js +2 -3
- package/dist/brain/degrading.d.ts +2 -8
- package/dist/brain/degrading.js +3 -3
- package/dist/brain/openai.js +3 -5
- package/dist/brain/terminal-cause.d.ts +1 -1
- package/dist/brain/terminal-cause.js +2 -8
- package/dist/core/hooks.js +12 -7
- package/dist/core/lsp.js +2 -3
- package/dist/core/mcp.d.ts +1 -1
- package/dist/core/mcp.js +2 -1
- package/dist/core/memory-engine/dual-root.js +2 -1
- package/dist/core/memory-engine/engine.d.ts +4 -0
- package/dist/core/memory-engine/engine.js +25 -6
- package/dist/core/memory-engine/file-backend.d.ts +7 -5
- package/dist/core/memory-engine/file-backend.js +3 -1
- package/dist/core/memory-engine/index.d.ts +1 -1
- package/dist/core/memory-engine/index.js +1 -1
- package/dist/core/memory-engine/layout.d.ts +7 -0
- package/dist/core/memory-engine/layout.js +98 -5
- package/dist/core/memory.d.ts +0 -1
- package/dist/core/memory.js +7 -1
- package/dist/core/permission-rules.js +8 -7
- package/dist/core/protocol-table.d.ts +17 -0
- package/dist/core/protocol-table.js +37 -0
- package/dist/core/runner/active-skill-scope.js +7 -7
- package/dist/core/runner/prepare-memory.js +7 -4
- package/dist/core/runner/prepare-task.js +43 -26
- package/dist/core/runner/runtask.js +18 -6
- package/dist/core/runner/session-rule-policy.js +1 -1
- package/dist/core/runner/synthetic-tools.js +2 -2
- package/dist/core/runner/tool-disclosure.d.ts +0 -1
- package/dist/core/runner/tool-disclosure.js +6 -3
- package/dist/core/runner/turn-attachments.js +2 -1
- package/dist/core/sensitive-path-policy.js +2 -2
- package/dist/core/tool-policy.d.ts +1 -3
- package/dist/core/tool-policy.js +40 -16
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/orchestration/run-spec.js +1 -1
- package/dist/orchestration/run-workflow-tool.js +7 -3
- package/dist/orchestration/workflow-script-store.d.ts +1 -1
- package/dist/orchestration/workflow-script-store.js +1 -1
- package/dist/prompt-assembly/assemble.js +0 -2
- package/dist/prompt-assembly/tool-catalog.d.ts +2 -1
- package/dist/prompts/default.d.ts +0 -2
- package/dist/prompts/default.js +1 -5
- package/dist/stores/cc/mailbox-store.d.ts +4 -0
- package/dist/stores/cc/mailbox-store.js +67 -9
- package/dist/stores/file/file-snapshot-store.d.ts +9 -1
- package/dist/stores/file/file-snapshot-store.js +28 -5
- package/dist/stores/file/fs-atomic.d.ts +4 -1
- package/dist/stores/file/fs-atomic.js +2 -1
- package/dist/stores/file/index.d.ts +10 -3
- package/dist/stores/file/index.js +4 -3
- package/dist/stores/file/mailbox-store.d.ts +5 -0
- package/dist/stores/file/mailbox-store.js +15 -3
- package/dist/stores/file/session-policy-store.d.ts +13 -1
- package/dist/stores/file/session-policy-store.js +39 -6
- package/dist/stores/file/session-store.d.ts +9 -1
- package/dist/stores/file/session-store.js +19 -4
- package/dist/tools/fs/fs-search-tools.js +10 -2
- package/dist/tools/fs/fs-shared.d.ts +1 -2
- package/dist/tools/fs/fs-shared.js +1 -2
- package/dist/tools/todo.js +14 -7
- 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)
|
|
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()}`,
|
|
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,
|
|
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
|
-
|
|
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) {
|
package/dist/core/memory.d.ts
CHANGED
package/dist/core/memory.js
CHANGED
|
@@ -354,7 +354,13 @@ function mtimeMsOf(ts) {
|
|
|
354
354
|
export function normalizeMemorySpec(input) {
|
|
355
355
|
if (!input)
|
|
356
356
|
return undefined;
|
|
357
|
-
|
|
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", "
|
|
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(
|
|
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",
|
|
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",
|
|
282
|
+
return { action: "deny", message: ruleMessage("denied", paramDeny.ruleText, paramDeny.source) };
|
|
282
283
|
}
|
|
283
284
|
if (entry.bare.ask) {
|
|
284
|
-
return { action: "ask",
|
|
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",
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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 =
|
|
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) :
|
|
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) :
|
|
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;
|