@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
|
@@ -338,16 +338,20 @@ export async function createRunWorkflowTool(d) {
|
|
|
338
338
|
catch (err) {
|
|
339
339
|
return structuredError(`failed to resolve workflow name: ${redactSecrets(err instanceof Error ? err.message : String(err)).slice(0, 300)}`);
|
|
340
340
|
}
|
|
341
|
+
const resolvedShape = resolved;
|
|
342
|
+
if (resolvedShape !== undefined && (resolvedShape === null || typeof resolvedShape !== "object")) {
|
|
343
|
+
return structuredError("the wired workflow script store's resolveName returned the retired bare-string form — the resolution shape is now { script, defaultArgs? } (one shape); upgrade the store implementation");
|
|
344
|
+
}
|
|
341
345
|
}
|
|
342
346
|
if (resolved === undefined && builtinsEnabled) {
|
|
343
347
|
resolved = resolveBuiltinWorkflow(rawName);
|
|
344
348
|
}
|
|
345
349
|
if (resolved === undefined)
|
|
346
350
|
return structuredError(`unknown workflow name: ${JSON.stringify(rawName)}`);
|
|
347
|
-
script =
|
|
348
|
-
if (
|
|
351
|
+
script = resolved.script;
|
|
352
|
+
if ("defaultArgs" in resolved)
|
|
349
353
|
registeredDefaultArgs = resolved.defaultArgs;
|
|
350
|
-
if (typeof resolved
|
|
354
|
+
if (typeof resolved.stringArgKey === "string" && resolved.stringArgKey.length > 0) {
|
|
351
355
|
registeredStringArgKey = resolved.stringArgKey;
|
|
352
356
|
}
|
|
353
357
|
}
|
|
@@ -12,7 +12,7 @@ export interface WorkflowScriptStore {
|
|
|
12
12
|
readonly scopePartitioned: true;
|
|
13
13
|
persist(runId: string, script: string, scope: string): Promise<string> | string;
|
|
14
14
|
load(scriptPath: string, scope: string): Promise<string> | string;
|
|
15
|
-
resolveName?(name: string): Promise<
|
|
15
|
+
resolveName?(name: string): Promise<NamedWorkflowResolution | undefined> | NamedWorkflowResolution | undefined;
|
|
16
16
|
list?(): NamedWorkflowListing[] | Promise<NamedWorkflowListing[]>;
|
|
17
17
|
}
|
|
18
18
|
export declare function mergeWorkflowArgs(callArgs: unknown, defaultArgs: unknown): unknown;
|
|
@@ -95,7 +95,7 @@ export function createFileWorkflowScriptStore(dir) {
|
|
|
95
95
|
const script = readFileSync(path, "utf-8");
|
|
96
96
|
const argsPath = join(root, `${stem}.args.json`);
|
|
97
97
|
if (!existsSync(argsPath))
|
|
98
|
-
return script;
|
|
98
|
+
return { script };
|
|
99
99
|
let defaultArgs;
|
|
100
100
|
try {
|
|
101
101
|
defaultArgs = JSON.parse(readFileSync(argsPath, "utf-8"));
|
|
@@ -101,8 +101,6 @@ export function assemblePrompt(inputs) {
|
|
|
101
101
|
userSystemPrompt: inputs.userSystemPrompt,
|
|
102
102
|
userAppendSystemPrompt: inputs.userAppendSystemPrompt,
|
|
103
103
|
tools: inputs.tools,
|
|
104
|
-
memoryEnabled: false,
|
|
105
|
-
consolidationEnabled: false,
|
|
106
104
|
...facts,
|
|
107
105
|
};
|
|
108
106
|
let pack = SEMA_DEFAULT_PACK;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { TSchema } from "typebox";
|
|
2
2
|
import type { AgentTool } from "../internal/harness-types.js";
|
|
3
|
-
|
|
3
|
+
import type { ProtocolId } from "../core/protocol-table.js";
|
|
4
|
+
export type ToolOrigin = "core" | "caller" | "synthetic" | ProtocolId;
|
|
4
5
|
export interface ToolContractDescriptor {
|
|
5
6
|
contractId: string;
|
|
6
7
|
implementationRevision: string;
|
|
@@ -64,8 +64,6 @@ export interface StablePromptContext {
|
|
|
64
64
|
userSystemPrompt?: string;
|
|
65
65
|
userAppendSystemPrompt?: string;
|
|
66
66
|
tools: AgentTool[];
|
|
67
|
-
memoryEnabled: boolean;
|
|
68
|
-
consolidationEnabled?: boolean;
|
|
69
67
|
policyEnabled?: boolean;
|
|
70
68
|
hooksEnabled?: boolean;
|
|
71
69
|
isolationEnabled?: boolean;
|
package/dist/prompts/default.js
CHANGED
|
@@ -365,10 +365,6 @@ export function constitutionBlocks(ctx) {
|
|
|
365
365
|
blocks.push({ id: "mode.worktree", text: WORKTREE_NOTICE });
|
|
366
366
|
if (ctx.goalEnabled)
|
|
367
367
|
blocks.push({ id: "mode.goal", text: GOAL_COMPLETION_GUIDANCE });
|
|
368
|
-
if (ctx.memoryEnabled)
|
|
369
|
-
blocks.push({ id: "memory.safety", text: MEMORY_SAFETY });
|
|
370
|
-
if (ctx.memoryEnabled && !ctx.consolidationEnabled)
|
|
371
|
-
blocks.push({ id: "memory.hygiene", text: MEMORY_HYGIENE });
|
|
372
368
|
return blocks;
|
|
373
369
|
}
|
|
374
370
|
export function composeConstitution(roleBase, ctx) {
|
|
@@ -393,7 +389,7 @@ export function analyzePromptCacheFriendliness(provider, opts = {}) {
|
|
|
393
389
|
const build = (memoryBlock) => {
|
|
394
390
|
if (!provider.stableSystem)
|
|
395
391
|
throw new Error("PromptProvider does not implement stableSystem");
|
|
396
|
-
return composeSystemPrompt(provider.stableSystem({ userSystemPrompt: opts.userSystemPrompt, tools
|
|
392
|
+
return composeSystemPrompt(provider.stableSystem({ userSystemPrompt: opts.userSystemPrompt, tools }), memoryBlock);
|
|
397
393
|
};
|
|
398
394
|
const a = build(PROBE_MEMORY_A);
|
|
399
395
|
const b = build(PROBE_MEMORY_B);
|
|
@@ -4,5 +4,9 @@ export interface CcFileMailboxStoreOptions {
|
|
|
4
4
|
teamDir: string;
|
|
5
5
|
agentNameOf?: (handle: string) => string | undefined;
|
|
6
6
|
now?: () => number;
|
|
7
|
+
onCorruptRead?: (info: {
|
|
8
|
+
path: string;
|
|
9
|
+
reason: string;
|
|
10
|
+
}) => void;
|
|
7
11
|
}
|
|
8
12
|
export declare function createCcFileMailboxStore(opts: CcFileMailboxStoreOptions): MailboxStore;
|
|
@@ -27,21 +27,73 @@ export function createCcFileMailboxStore(opts) {
|
|
|
27
27
|
const name = opts.agentNameOf?.(handle) ?? handle;
|
|
28
28
|
return join(inboxDir, `${sanitizeCcAgentName(name)}.json`);
|
|
29
29
|
};
|
|
30
|
-
const
|
|
30
|
+
const discloseCorrupt = (path, reason) => {
|
|
31
|
+
try {
|
|
32
|
+
opts.onCorruptRead?.({ path, reason });
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const deferredNotes = () => {
|
|
38
|
+
const pending = [];
|
|
39
|
+
return {
|
|
40
|
+
note: (path, reason) => {
|
|
41
|
+
pending.push({ path, reason });
|
|
42
|
+
},
|
|
43
|
+
flush: () => {
|
|
44
|
+
const batch = pending.splice(0, pending.length);
|
|
45
|
+
for (const n of batch)
|
|
46
|
+
discloseCorrupt(n.path, n.reason);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
const entryDefect = (e) => {
|
|
51
|
+
if (e === null)
|
|
52
|
+
return "null entry";
|
|
53
|
+
if (Array.isArray(e))
|
|
54
|
+
return "array entry";
|
|
55
|
+
if (typeof e !== "object")
|
|
56
|
+
return `${typeof e} entry`;
|
|
57
|
+
return undefined;
|
|
58
|
+
};
|
|
59
|
+
const loadBox = (path, note) => {
|
|
31
60
|
let raw;
|
|
32
61
|
try {
|
|
33
62
|
raw = readFileSync(path, "utf8");
|
|
34
63
|
}
|
|
35
|
-
catch {
|
|
64
|
+
catch (err) {
|
|
65
|
+
if (err.code !== "ENOENT")
|
|
66
|
+
note(path, `read failed (non-ENOENT): ${err.message}`);
|
|
36
67
|
return [];
|
|
37
68
|
}
|
|
69
|
+
let parsed;
|
|
38
70
|
try {
|
|
39
|
-
|
|
40
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
71
|
+
parsed = JSON.parse(raw);
|
|
41
72
|
}
|
|
42
73
|
catch {
|
|
74
|
+
note(path, "unparseable inbox JSON (torn write?)");
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
if (!Array.isArray(parsed)) {
|
|
78
|
+
note(path, "inbox document is not an array");
|
|
43
79
|
return [];
|
|
44
80
|
}
|
|
81
|
+
const kept = [];
|
|
82
|
+
let dropped = 0;
|
|
83
|
+
let firstReason;
|
|
84
|
+
for (const e of parsed) {
|
|
85
|
+
const defect = entryDefect(e);
|
|
86
|
+
if (defect === undefined) {
|
|
87
|
+
kept.push(e);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
dropped += 1;
|
|
91
|
+
if (firstReason === undefined)
|
|
92
|
+
firstReason = defect;
|
|
93
|
+
}
|
|
94
|
+
if (dropped > 0)
|
|
95
|
+
note(path, `${dropped} malformed inbox entries skipped (first: ${firstReason})`);
|
|
96
|
+
return kept;
|
|
45
97
|
};
|
|
46
98
|
const saveBox = (path, box) => {
|
|
47
99
|
atomicWriteFile(inboxDir, path, JSON.stringify(box, null, 2));
|
|
@@ -51,8 +103,9 @@ export function createCcFileMailboxStore(opts) {
|
|
|
51
103
|
requireDefaultScope(scope);
|
|
52
104
|
mkdirSync(inboxDir, { recursive: true });
|
|
53
105
|
const path = inboxPath(handle);
|
|
106
|
+
const notes = deferredNotes();
|
|
54
107
|
return withCcLock(path, () => {
|
|
55
|
-
const box = loadBox(path);
|
|
108
|
+
const box = loadBox(path, notes.note);
|
|
56
109
|
box.push({
|
|
57
110
|
type: "message",
|
|
58
111
|
...(msg.from !== undefined ? { from: msg.from } : {}),
|
|
@@ -64,6 +117,8 @@ export function createCcFileMailboxStore(opts) {
|
|
|
64
117
|
});
|
|
65
118
|
saveBox(path, box);
|
|
66
119
|
return box.length;
|
|
120
|
+
}).finally(() => {
|
|
121
|
+
notes.flush();
|
|
67
122
|
});
|
|
68
123
|
},
|
|
69
124
|
claimLease: (scope, handle, owner, ttlMs, now) => {
|
|
@@ -73,7 +128,7 @@ export function createCcFileMailboxStore(opts) {
|
|
|
73
128
|
const held = leases.get(key);
|
|
74
129
|
if (held !== undefined && held.expiresAt > t && held.owner !== owner)
|
|
75
130
|
return Promise.resolve(null);
|
|
76
|
-
const box = loadBox(key);
|
|
131
|
+
const box = loadBox(key, discloseCorrupt);
|
|
77
132
|
const maxSeq = box.length;
|
|
78
133
|
const pending = box
|
|
79
134
|
.map((e, i) => ({ e, seq: i + 1 }))
|
|
@@ -97,8 +152,9 @@ export function createCcFileMailboxStore(opts) {
|
|
|
97
152
|
return Promise.resolve();
|
|
98
153
|
if (!existsSync(path))
|
|
99
154
|
return Promise.resolve();
|
|
155
|
+
const notes = deferredNotes();
|
|
100
156
|
return withCcLock(path, () => {
|
|
101
|
-
const box = loadBox(path);
|
|
157
|
+
const box = loadBox(path, notes.note);
|
|
102
158
|
let changed = false;
|
|
103
159
|
for (let i = 0; i < Math.min(upToSeq, box.length); i++) {
|
|
104
160
|
if (box[i].read !== true) {
|
|
@@ -110,6 +166,8 @@ export function createCcFileMailboxStore(opts) {
|
|
|
110
166
|
saveBox(path, box);
|
|
111
167
|
if (held.maxSeq <= upToSeq)
|
|
112
168
|
leases.delete(path);
|
|
169
|
+
}).finally(() => {
|
|
170
|
+
notes.flush();
|
|
113
171
|
});
|
|
114
172
|
},
|
|
115
173
|
releaseLease: (scope, handle, owner) => {
|
|
@@ -122,7 +180,7 @@ export function createCcFileMailboxStore(opts) {
|
|
|
122
180
|
},
|
|
123
181
|
peekCount: (scope, handle) => {
|
|
124
182
|
requireDefaultScope(scope);
|
|
125
|
-
return Promise.resolve(loadBox(inboxPath(handle)).filter((e) => e.read !== true).length);
|
|
183
|
+
return Promise.resolve(loadBox(inboxPath(handle), discloseCorrupt).filter((e) => e.read !== true).length);
|
|
126
184
|
},
|
|
127
185
|
drop: (scope, handle) => {
|
|
128
186
|
requireDefaultScope(scope);
|
|
@@ -146,7 +204,7 @@ export function createCcFileMailboxStore(opts) {
|
|
|
146
204
|
if (!f.endsWith(".json"))
|
|
147
205
|
continue;
|
|
148
206
|
const path = join(inboxDir, f);
|
|
149
|
-
const box = loadBox(path);
|
|
207
|
+
const box = loadBox(path, discloseCorrupt);
|
|
150
208
|
const newest = box.reduce((acc, e) => {
|
|
151
209
|
const t = typeof e.timestamp === "string" ? Date.parse(e.timestamp) : Number.NaN;
|
|
152
210
|
return Number.isFinite(t) ? Math.max(acc, t) : Number.POSITIVE_INFINITY;
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { ExecutionEnv } from "../../internal/harness-types.js";
|
|
2
2
|
import { type FileSnapshotBounds, type FileSnapshotResult, type FileSnapshotStore } from "../../core/file-snapshot-store.js";
|
|
3
|
+
export interface FileFileSnapshotStoreOptions {
|
|
4
|
+
onCorruptRead?: (info: {
|
|
5
|
+
path: string;
|
|
6
|
+
reason: string;
|
|
7
|
+
}) => void;
|
|
8
|
+
}
|
|
3
9
|
export declare class FileFileSnapshotStore implements FileSnapshotStore {
|
|
4
10
|
private readonly base;
|
|
5
11
|
private readonly blobsDir;
|
|
@@ -7,7 +13,9 @@ export declare class FileFileSnapshotStore implements FileSnapshotStore {
|
|
|
7
13
|
private get inFlight();
|
|
8
14
|
private readonly inFlightKey;
|
|
9
15
|
private readonly bounds;
|
|
10
|
-
|
|
16
|
+
private readonly onCorruptRead;
|
|
17
|
+
constructor(root: string, bounds?: Partial<FileSnapshotBounds>, opts?: FileFileSnapshotStoreOptions);
|
|
18
|
+
private disclose;
|
|
11
19
|
private scopeDir;
|
|
12
20
|
private manifestPath;
|
|
13
21
|
private tryManifestPath;
|
|
@@ -18,7 +18,8 @@ export class FileFileSnapshotStore {
|
|
|
18
18
|
}
|
|
19
19
|
inFlightKey;
|
|
20
20
|
bounds;
|
|
21
|
-
|
|
21
|
+
onCorruptRead;
|
|
22
|
+
constructor(root, bounds, opts) {
|
|
22
23
|
this.base = join(root, "file-snapshots");
|
|
23
24
|
this.blobsDir = join(this.base, "blobs");
|
|
24
25
|
this.manifestsDir = join(this.base, "manifests");
|
|
@@ -30,6 +31,14 @@ export class FileFileSnapshotStore {
|
|
|
30
31
|
maxBytes: bounds?.maxBytes ?? DEFAULT_SNAPSHOT_BOUNDS.maxBytes,
|
|
31
32
|
ignoreDirs: new Set(bounds?.ignoreDirs ?? DEFAULT_SNAPSHOT_BOUNDS.ignoreDirs),
|
|
32
33
|
};
|
|
34
|
+
this.onCorruptRead = opts?.onCorruptRead;
|
|
35
|
+
}
|
|
36
|
+
disclose(path, reason) {
|
|
37
|
+
try {
|
|
38
|
+
this.onCorruptRead?.({ path, reason });
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
}
|
|
33
42
|
}
|
|
34
43
|
scopeDir(scope) {
|
|
35
44
|
return join(this.manifestsDir, sanitizeScope(scope));
|
|
@@ -60,9 +69,13 @@ export class FileFileSnapshotStore {
|
|
|
60
69
|
}
|
|
61
70
|
try {
|
|
62
71
|
const parsed = JSON.parse(raw);
|
|
63
|
-
|
|
72
|
+
if (Array.isArray(parsed))
|
|
73
|
+
return new Map(parsed);
|
|
74
|
+
this.disclose(path, "manifest is not a pair array — read as no snapshot");
|
|
75
|
+
return null;
|
|
64
76
|
}
|
|
65
77
|
catch {
|
|
78
|
+
this.disclose(path, "unparseable manifest JSON (torn/zero-byte write?) — read as no snapshot");
|
|
66
79
|
return null;
|
|
67
80
|
}
|
|
68
81
|
}
|
|
@@ -132,6 +145,7 @@ export class FileFileSnapshotStore {
|
|
|
132
145
|
catch (err) {
|
|
133
146
|
if (err.code === "ENOENT")
|
|
134
147
|
return [];
|
|
148
|
+
this.disclose(dir, `manifest scope directory unreadable (${err.code ?? "unknown"}) — listed as no keys`);
|
|
135
149
|
return [];
|
|
136
150
|
}
|
|
137
151
|
return entries
|
|
@@ -242,8 +256,10 @@ export class FileFileSnapshotStore {
|
|
|
242
256
|
catch (err) {
|
|
243
257
|
if (err.code === "ENOENT")
|
|
244
258
|
entries = [];
|
|
245
|
-
else
|
|
259
|
+
else {
|
|
246
260
|
entries = [];
|
|
261
|
+
this.disclose(dir, `manifest scope directory unreadable (${err.code ?? "unknown"}) — the manifest-deletion pass was skipped`);
|
|
262
|
+
}
|
|
247
263
|
}
|
|
248
264
|
for (const file of entries) {
|
|
249
265
|
if (!file.endsWith(".json"))
|
|
@@ -266,8 +282,10 @@ export class FileFileSnapshotStore {
|
|
|
266
282
|
catch (err) {
|
|
267
283
|
if (err.code === "ENOENT")
|
|
268
284
|
scopeDirs = [];
|
|
269
|
-
else
|
|
285
|
+
else {
|
|
286
|
+
this.disclose(this.manifestsDir, `manifest root unreadable (${err.code ?? "unknown"}) — blob GC aborted, every blob kept`);
|
|
270
287
|
return removed;
|
|
288
|
+
}
|
|
271
289
|
}
|
|
272
290
|
for (const scopeName of scopeDirs) {
|
|
273
291
|
const sdir = join(this.manifestsDir, scopeName);
|
|
@@ -279,6 +297,7 @@ export class FileFileSnapshotStore {
|
|
|
279
297
|
if (err.code === "ENOENT")
|
|
280
298
|
continue;
|
|
281
299
|
complete = false;
|
|
300
|
+
this.disclose(sdir, `manifest scope directory unreadable (${err.code ?? "unknown"}) — blob GC aborted, every blob kept`);
|
|
282
301
|
break;
|
|
283
302
|
}
|
|
284
303
|
for (const file of files) {
|
|
@@ -287,6 +306,7 @@ export class FileFileSnapshotStore {
|
|
|
287
306
|
const m = this.readManifest(join(sdir, file));
|
|
288
307
|
if (!m) {
|
|
289
308
|
complete = false;
|
|
309
|
+
this.disclose(join(sdir, file), "manifest unreadable during the live-set scan — blob GC aborted, every blob kept");
|
|
290
310
|
break;
|
|
291
311
|
}
|
|
292
312
|
for (const h of m.values())
|
|
@@ -301,7 +321,10 @@ export class FileFileSnapshotStore {
|
|
|
301
321
|
try {
|
|
302
322
|
blobs = readdirSync(this.blobsDir);
|
|
303
323
|
}
|
|
304
|
-
catch {
|
|
324
|
+
catch (err) {
|
|
325
|
+
if (err.code !== "ENOENT") {
|
|
326
|
+
this.disclose(this.blobsDir, `blob directory unreadable (${err.code ?? "unknown"}) — blob GC skipped, every blob kept`);
|
|
327
|
+
}
|
|
305
328
|
return removed;
|
|
306
329
|
}
|
|
307
330
|
for (const blob of blobs) {
|
|
@@ -4,7 +4,10 @@ export declare function resolveDataRoot(explicit?: string): string;
|
|
|
4
4
|
export declare function writeThenLink(target: string, content: string | Uint8Array): void;
|
|
5
5
|
export declare function ensureDir(dir: string): void;
|
|
6
6
|
export declare function atomicWriteFile(tmpDir: string, target: string, bytes: string): void;
|
|
7
|
-
export declare function readJsonlRecords<T>(path: string
|
|
7
|
+
export declare function readJsonlRecords<T>(path: string, onCorrupt?: (info: {
|
|
8
|
+
path: string;
|
|
9
|
+
reason: string;
|
|
10
|
+
}) => void): T[];
|
|
8
11
|
export declare function canonicalStoreKey(p: string): string;
|
|
9
12
|
export declare class AppendLog {
|
|
10
13
|
private fd;
|
|
@@ -101,7 +101,7 @@ function basenameOf(p) {
|
|
|
101
101
|
function dirnameOf(p) {
|
|
102
102
|
return dirname(p);
|
|
103
103
|
}
|
|
104
|
-
export function readJsonlRecords(path) {
|
|
104
|
+
export function readJsonlRecords(path, onCorrupt) {
|
|
105
105
|
let raw;
|
|
106
106
|
try {
|
|
107
107
|
raw = readFileSync(path, "utf8");
|
|
@@ -126,6 +126,7 @@ export function readJsonlRecords(path) {
|
|
|
126
126
|
out.push(JSON.parse(line));
|
|
127
127
|
}
|
|
128
128
|
catch {
|
|
129
|
+
onCorrupt?.({ path, reason: `unparseable jsonl record at line ${i + 1} (skipped; the rest of the replay is intact)` });
|
|
129
130
|
}
|
|
130
131
|
}
|
|
131
132
|
return out;
|
|
@@ -9,10 +9,10 @@ import type { FileSnapshotStore } from "../../core/file-snapshot-store.js";
|
|
|
9
9
|
import type { WorkflowJournalStore } from "../../core/workflow-journal-store.js";
|
|
10
10
|
export { FileCheckpointStore, type FileCheckpointStoreOptions } from "./checkpoint-store.js";
|
|
11
11
|
export { FileMemoryStore } from "./memory-store.js";
|
|
12
|
-
export { FileSessionRepo } from "./session-store.js";
|
|
12
|
+
export { FileSessionRepo, type FileSessionRepoOptions } from "./session-store.js";
|
|
13
13
|
export { FileToolResultStore } from "./tool-result-store.js";
|
|
14
|
-
export { FileSessionPolicyStore } from "./session-policy-store.js";
|
|
15
|
-
export { FileFileSnapshotStore } from "./file-snapshot-store.js";
|
|
14
|
+
export { FileSessionPolicyStore, type FileSessionPolicyStoreOptions, type SessionPolicyCorruptReadInfo } from "./session-policy-store.js";
|
|
15
|
+
export { FileFileSnapshotStore, type FileFileSnapshotStoreOptions } from "./file-snapshot-store.js";
|
|
16
16
|
export { FileWorkflowJournalStore, MAX_JOURNAL_RESULT_BYTES, oversizeJournalResult } from "./workflow-journal-store.js";
|
|
17
17
|
export { resolveDataRoot, sanitizeScope, sanitizePathComponent, createFileConsolidationLock } from "./fs-atomic.js";
|
|
18
18
|
export { atomicWriteFile, writeThenLink, ensureDir, readJsonlRecords, AppendLog } from "./fs-atomic.js";
|
|
@@ -23,6 +23,13 @@ export interface FileStorageBackendOptions {
|
|
|
23
23
|
checkpoint?: FileCheckpointStoreOptions;
|
|
24
24
|
snapshotBounds?: Partial<import("../../core/file-snapshot-store.js").FileSnapshotBounds>;
|
|
25
25
|
embedder?: import("../../core/memory.js").Embedder;
|
|
26
|
+
onCorruptRead?: (info: FileStorageCorruptReadInfo) => void;
|
|
27
|
+
}
|
|
28
|
+
export interface FileStorageCorruptReadInfo {
|
|
29
|
+
path: string;
|
|
30
|
+
reason: string;
|
|
31
|
+
sessionId?: string;
|
|
32
|
+
principal?: string;
|
|
26
33
|
}
|
|
27
34
|
export declare class FileStorageBackend {
|
|
28
35
|
readonly root: string;
|
|
@@ -39,7 +39,8 @@ export class FileStorageBackend {
|
|
|
39
39
|
this.lock = new BootLock(join(this.root, "LOCK"));
|
|
40
40
|
this.lock.acquire();
|
|
41
41
|
try {
|
|
42
|
-
const
|
|
42
|
+
const corruptRead = opts.onCorruptRead !== undefined ? { onCorruptRead: opts.onCorruptRead } : undefined;
|
|
43
|
+
const repo = new FileSessionRepo(this.root, corruptRead);
|
|
43
44
|
this.fileSessions = repo;
|
|
44
45
|
this.ttl = new TtlSessionStore({ repo, evict: opts.evict ?? "forget" });
|
|
45
46
|
this.sessionStore = this.ttl;
|
|
@@ -48,8 +49,8 @@ export class FileStorageBackend {
|
|
|
48
49
|
this.fileMemory = new FileMemoryStore(this.root, opts.embedder ? { embedder: opts.embedder } : {});
|
|
49
50
|
this.memoryStore = guardedMemoryStore(this.fileMemory, opts.utilityGate);
|
|
50
51
|
this.toolResultStore = new FileToolResultStore(this.root);
|
|
51
|
-
this.sessionPolicyStore = new FileSessionPolicyStore(this.root);
|
|
52
|
-
this.fileSnapshotStore = new FileFileSnapshotStore(this.root, opts.snapshotBounds);
|
|
52
|
+
this.sessionPolicyStore = new FileSessionPolicyStore(this.root, corruptRead);
|
|
53
|
+
this.fileSnapshotStore = new FileFileSnapshotStore(this.root, opts.snapshotBounds, corruptRead);
|
|
53
54
|
this.fileWorkflowJournal = new FileWorkflowJournalStore(this.root);
|
|
54
55
|
this.workflowJournalStore = this.fileWorkflowJournal;
|
|
55
56
|
this.consolidationLock = createFileConsolidationLock(join(this.root, "consolidation-locks"));
|
|
@@ -2,6 +2,10 @@ import { type MailboxAppendMessage, type MailboxLease, type MailboxStore } from
|
|
|
2
2
|
export interface FileMailboxStoreOptions {
|
|
3
3
|
fsync?: boolean;
|
|
4
4
|
compactEvery?: number;
|
|
5
|
+
onCorruptRead?: (info: {
|
|
6
|
+
path: string;
|
|
7
|
+
reason: string;
|
|
8
|
+
}) => void;
|
|
5
9
|
}
|
|
6
10
|
export declare class FileMailboxStore implements MailboxStore {
|
|
7
11
|
private readonly dir;
|
|
@@ -9,6 +13,7 @@ export declare class FileMailboxStore implements MailboxStore {
|
|
|
9
13
|
private readonly fsyncEnabled;
|
|
10
14
|
private readonly compactEvery;
|
|
11
15
|
private readonly touched;
|
|
16
|
+
private readonly discloseCorrupt;
|
|
12
17
|
constructor(root: string, opts?: FileMailboxStoreOptions);
|
|
13
18
|
private boxPath;
|
|
14
19
|
private lockKey;
|
|
@@ -39,9 +39,21 @@ export class FileMailboxStore {
|
|
|
39
39
|
fsyncEnabled;
|
|
40
40
|
compactEvery;
|
|
41
41
|
touched = new Map();
|
|
42
|
+
discloseCorrupt;
|
|
42
43
|
constructor(root, opts = {}) {
|
|
43
44
|
this.fsyncEnabled = opts.fsync !== false;
|
|
44
45
|
this.compactEvery = opts.compactEvery ?? 500;
|
|
46
|
+
const sink = opts.onCorruptRead;
|
|
47
|
+
this.discloseCorrupt =
|
|
48
|
+
sink === undefined
|
|
49
|
+
? undefined
|
|
50
|
+
: (info) => {
|
|
51
|
+
try {
|
|
52
|
+
sink(info);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
}
|
|
56
|
+
};
|
|
45
57
|
const lexicalDir = resolve(join(root, "mailboxes"));
|
|
46
58
|
const lexicalTmp = resolve(join(root, "tmp"));
|
|
47
59
|
ensureDir(lexicalDir);
|
|
@@ -67,7 +79,7 @@ export class FileMailboxStore {
|
|
|
67
79
|
return b;
|
|
68
80
|
}
|
|
69
81
|
ensureDir(join(this.dir, sanitizeScope(scope)));
|
|
70
|
-
const events = readJsonlRecords(path);
|
|
82
|
+
const events = readJsonlRecords(path, this.discloseCorrupt);
|
|
71
83
|
const state = { messages: [], nextSeq: 1, log: new AppendLog(path), events: events.length, refs: 1, path };
|
|
72
84
|
for (const ev of events)
|
|
73
85
|
applyEvent(state, ev);
|
|
@@ -145,7 +157,7 @@ export class FileMailboxStore {
|
|
|
145
157
|
return 0;
|
|
146
158
|
return withPathLock(this.lockKey(scope, handle), () => {
|
|
147
159
|
const b = { messages: [], nextSeq: 1 };
|
|
148
|
-
for (const ev of readJsonlRecords(path))
|
|
160
|
+
for (const ev of readJsonlRecords(path, this.discloseCorrupt))
|
|
149
161
|
applyEvent(b, ev);
|
|
150
162
|
return b.messages.length;
|
|
151
163
|
});
|
|
@@ -200,7 +212,7 @@ export class FileMailboxStore {
|
|
|
200
212
|
if (sharedBoxes.has(key))
|
|
201
213
|
return false;
|
|
202
214
|
const b = { messages: [], nextSeq: 1 };
|
|
203
|
-
for (const ev of readJsonlRecords(path))
|
|
215
|
+
for (const ev of readJsonlRecords(path, this.discloseCorrupt))
|
|
204
216
|
applyEvent(b, ev);
|
|
205
217
|
const newest = newestSentAt(b.messages);
|
|
206
218
|
if (newest === undefined || newest >= now - maxAgeMs)
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import { type PutRulesOptions, type SessionPermissionRules, type SessionPolicyStore, type SessionRulesRecord, type StoredSessionRules } from "../../core/session-policy-store.js";
|
|
2
|
+
export interface SessionPolicyCorruptReadInfo {
|
|
3
|
+
sessionId: string;
|
|
4
|
+
principal?: string;
|
|
5
|
+
path: string;
|
|
6
|
+
reason: string;
|
|
7
|
+
}
|
|
8
|
+
export interface FileSessionPolicyStoreOptions {
|
|
9
|
+
onCorruptRead?: (info: SessionPolicyCorruptReadInfo) => void;
|
|
10
|
+
}
|
|
2
11
|
export declare class FileSessionPolicyStore implements SessionPolicyStore {
|
|
3
12
|
private readonly dir;
|
|
4
|
-
|
|
13
|
+
private readonly onCorruptRead;
|
|
14
|
+
constructor(root: string, opts?: FileSessionPolicyStoreOptions);
|
|
15
|
+
private disclose;
|
|
16
|
+
private discloseCorrupt;
|
|
5
17
|
private pathFor;
|
|
6
18
|
private read;
|
|
7
19
|
getRules(sessionId: string, principal?: string): Promise<StoredSessionRules | null>;
|
|
@@ -4,9 +4,21 @@ import { loosenReasons, normalizeRules, stripRev, SessionPolicyError, } from "..
|
|
|
4
4
|
import { atomicWriteFile, ensureDir, sanitizeScope } from "./fs-atomic.js";
|
|
5
5
|
export class FileSessionPolicyStore {
|
|
6
6
|
dir;
|
|
7
|
-
|
|
7
|
+
onCorruptRead;
|
|
8
|
+
constructor(root, opts) {
|
|
8
9
|
this.dir = join(root, "session-policy");
|
|
9
10
|
ensureDir(this.dir);
|
|
11
|
+
this.onCorruptRead = opts?.onCorruptRead;
|
|
12
|
+
}
|
|
13
|
+
disclose(info) {
|
|
14
|
+
try {
|
|
15
|
+
this.onCorruptRead?.(info);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
discloseCorrupt(sessionId, principal, reason) {
|
|
21
|
+
this.disclose({ sessionId, ...(principal !== undefined ? { principal } : {}), path: this.pathFor(sessionId, principal), reason });
|
|
10
22
|
}
|
|
11
23
|
pathFor(sessionId, principal) {
|
|
12
24
|
const composite = JSON.stringify([sessionId, principal ?? null]);
|
|
@@ -24,20 +36,27 @@ export class FileSessionPolicyStore {
|
|
|
24
36
|
}
|
|
25
37
|
try {
|
|
26
38
|
const parsed = JSON.parse(raw);
|
|
27
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
39
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
40
|
+
this.discloseCorrupt(sessionId, principal, "not a JSON object");
|
|
28
41
|
return null;
|
|
42
|
+
}
|
|
29
43
|
const r = parsed;
|
|
30
|
-
if (r.rev !== undefined && (typeof r.rev !== "number" || !Number.isFinite(r.rev)))
|
|
44
|
+
if (r.rev !== undefined && (typeof r.rev !== "number" || !Number.isFinite(r.rev))) {
|
|
45
|
+
this.discloseCorrupt(sessionId, principal, "non-numeric rev");
|
|
31
46
|
return null;
|
|
47
|
+
}
|
|
32
48
|
const strArr = (x) => x === undefined || (Array.isArray(x) && x.every((e) => typeof e === "string"));
|
|
33
|
-
if (!strArr(r.toolAllow) || !strArr(r.toolDeny) || !strArr(r.commandAllow) || !strArr(r.commandDeny) || !strArr(r.allowDirs))
|
|
49
|
+
if (!strArr(r.toolAllow) || !strArr(r.toolDeny) || !strArr(r.commandAllow) || !strArr(r.commandDeny) || !strArr(r.allowDirs)) {
|
|
50
|
+
this.discloseCorrupt(sessionId, principal, "malformed rule field");
|
|
34
51
|
return null;
|
|
52
|
+
}
|
|
35
53
|
const { __sid: _sid, __principal: _principal, ...clean } = r;
|
|
36
54
|
void _sid;
|
|
37
55
|
void _principal;
|
|
38
56
|
return clean;
|
|
39
57
|
}
|
|
40
58
|
catch {
|
|
59
|
+
this.discloseCorrupt(sessionId, principal, "unparseable JSON (torn/zero-byte write?)");
|
|
41
60
|
return null;
|
|
42
61
|
}
|
|
43
62
|
}
|
|
@@ -74,15 +93,29 @@ export class FileSessionPolicyStore {
|
|
|
74
93
|
for (const file of files) {
|
|
75
94
|
if (!file.endsWith(".json"))
|
|
76
95
|
continue;
|
|
96
|
+
const full = join(this.dir, file);
|
|
97
|
+
let raw;
|
|
98
|
+
try {
|
|
99
|
+
raw = readFileSync(full, "utf8");
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
if (err.code !== "ENOENT") {
|
|
103
|
+
this.disclose({ sessionId, path: full, reason: `read failed (non-ENOENT): ${err.message}` });
|
|
104
|
+
}
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
77
107
|
let parsed;
|
|
78
108
|
try {
|
|
79
|
-
parsed = JSON.parse(
|
|
109
|
+
parsed = JSON.parse(raw);
|
|
80
110
|
}
|
|
81
111
|
catch {
|
|
112
|
+
this.disclose({ sessionId, path: full, reason: "unparseable JSON (torn/zero-byte write?)" });
|
|
82
113
|
continue;
|
|
83
114
|
}
|
|
84
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
115
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
116
|
+
this.disclose({ sessionId, path: full, reason: "not a JSON object" });
|
|
85
117
|
continue;
|
|
118
|
+
}
|
|
86
119
|
const r = parsed;
|
|
87
120
|
if (r.__sid !== sessionId)
|
|
88
121
|
continue;
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import type { Session, SessionForkOptions, SessionMetadata, SessionRepo, SessionTreeEntry } from "../../internal/harness.js";
|
|
2
|
+
export interface FileSessionRepoOptions {
|
|
3
|
+
onCorruptRead?: (info: {
|
|
4
|
+
path: string;
|
|
5
|
+
reason: string;
|
|
6
|
+
}) => void;
|
|
7
|
+
}
|
|
2
8
|
export declare class FileSessionRepo implements SessionRepo {
|
|
3
9
|
private readonly dir;
|
|
4
10
|
private readonly tmpDir;
|
|
11
|
+
private readonly onCorruptRead;
|
|
5
12
|
private readonly joined;
|
|
6
|
-
constructor(root: string);
|
|
13
|
+
constructor(root: string, opts?: FileSessionRepoOptions);
|
|
14
|
+
private disclose;
|
|
7
15
|
private pathFor;
|
|
8
16
|
private read;
|
|
9
17
|
private storage;
|