@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
@@ -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 = typeof resolved === "string" ? resolved : resolved.script;
348
- if (typeof resolved !== "string" && "defaultArgs" in resolved)
351
+ script = resolved.script;
352
+ if ("defaultArgs" in resolved)
349
353
  registeredDefaultArgs = resolved.defaultArgs;
350
- if (typeof resolved !== "string" && typeof resolved.stringArgKey === "string" && resolved.stringArgKey.length > 0) {
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<string | NamedWorkflowResolution | undefined> | string | NamedWorkflowResolution | undefined;
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
- export type ToolOrigin = "core" | "caller" | "mcp" | "synthetic";
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;
@@ -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, memoryEnabled: true }), memoryBlock);
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 loadBox = (path) => {
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
- const parsed = JSON.parse(raw);
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
- constructor(root: string, bounds?: Partial<FileSnapshotBounds>);
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
- constructor(root, bounds) {
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
- return Array.isArray(parsed) ? new Map(parsed) : null;
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): T[];
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 repo = new FileSessionRepo(this.root);
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
- constructor(root: string);
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
- constructor(root) {
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(readFileSync(join(this.dir, file), "utf8"));
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;