@sema-agent/core 5.1.0 → 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 (47) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/agents/roster-store.js +6 -1
  3. package/dist/bin/sema-tb.js +3 -4
  4. package/dist/brain/openai.js +3 -5
  5. package/dist/brain/terminal-cause.d.ts +1 -1
  6. package/dist/core/hooks.js +8 -3
  7. package/dist/core/mcp.d.ts +1 -1
  8. package/dist/core/memory-engine/dual-root.js +2 -1
  9. package/dist/core/memory-engine/engine.d.ts +4 -0
  10. package/dist/core/memory-engine/engine.js +25 -6
  11. package/dist/core/memory-engine/file-backend.d.ts +7 -5
  12. package/dist/core/memory-engine/file-backend.js +2 -2
  13. package/dist/core/memory-engine/index.d.ts +1 -1
  14. package/dist/core/memory-engine/index.js +1 -1
  15. package/dist/core/memory-engine/layout.d.ts +6 -2
  16. package/dist/core/memory-engine/layout.js +73 -31
  17. package/dist/core/memory.js +6 -0
  18. package/dist/core/protocol-table.d.ts +10 -7
  19. package/dist/core/protocol-table.js +28 -14
  20. package/dist/core/runner/prepare-memory.js +4 -4
  21. package/dist/core/runner/prepare-task.js +30 -14
  22. package/dist/core/runner/runtask.js +11 -5
  23. package/dist/core/runner/tool-disclosure.js +6 -1
  24. package/dist/core/tool-policy.d.ts +1 -0
  25. package/dist/core/tool-policy.js +12 -2
  26. package/dist/index.d.ts +2 -2
  27. package/dist/index.js +1 -1
  28. package/dist/orchestration/run-workflow-tool.js +5 -1
  29. package/dist/prompt-assembly/assemble.js +0 -1
  30. package/dist/stores/cc/mailbox-store.js +58 -14
  31. package/dist/stores/file/file-snapshot-store.d.ts +9 -1
  32. package/dist/stores/file/file-snapshot-store.js +28 -5
  33. package/dist/stores/file/fs-atomic.d.ts +4 -1
  34. package/dist/stores/file/fs-atomic.js +2 -1
  35. package/dist/stores/file/index.d.ts +10 -3
  36. package/dist/stores/file/index.js +4 -3
  37. package/dist/stores/file/mailbox-store.d.ts +5 -0
  38. package/dist/stores/file/mailbox-store.js +15 -3
  39. package/dist/stores/file/session-policy-store.d.ts +11 -8
  40. package/dist/stores/file/session-policy-store.js +21 -4
  41. package/dist/stores/file/session-store.d.ts +9 -1
  42. package/dist/stores/file/session-store.js +19 -4
  43. package/dist/tools/fs/fs-search-tools.js +9 -0
  44. package/dist/tools/fs/fs-shared.d.ts +1 -1
  45. package/dist/tools/fs/fs-shared.js +1 -1
  46. package/dist/tools/todo.js +13 -6
  47. package/package.json +1 -1
@@ -1,20 +1,34 @@
1
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
+ };
2
24
  export const MCP_NAMESPACE = {
3
25
  id: "mcp",
4
- prefix: `mcp${NAME_SEP}`,
5
- makeName: (peer, tool) => `mcp${NAME_SEP}${peer}${NAME_SEP}${tool}`,
6
- parse(name) {
7
- if (!name.startsWith(this.prefix))
8
- return undefined;
9
- const rest = name.slice(this.prefix.length);
10
- const sep = rest.indexOf(NAME_SEP);
11
- if (sep <= 0)
12
- return undefined;
13
- return { peer: rest.slice(0, sep), tool: rest.slice(sep + NAME_SEP.length) };
14
- },
15
- displayGroupKey(name) {
16
- const p = this.parse(name);
17
- return p === undefined ? name : `${this.prefix}${p.peer}${NAME_SEP}*`;
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}*`;
18
32
  },
19
33
  };
20
34
  export const PROTOCOL_TABLE = [MCP_NAMESPACE];
@@ -1,4 +1,4 @@
1
- import { adoptLegacyRepoDirs, deriveControlPlaneDir, deriveProjectControlDir, deriveProjectMemoryDir, deriveRepoMemoryDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, lookupProjectIdHint, recordProjectIdHint, resolveMemoryEngineRoot } from "../memory-engine/layout.js";
1
+ import { adoptLegacyRepoDirs, deriveRepoControlPlaneDir, deriveProjectControlDir, deriveProjectMemoryDir, deriveRepoMemoryDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, lookupProjectIdHint, recordProjectIdHint, resolveMemoryEngineRoot } from "../memory-engine/layout.js";
2
2
  import { classifyScopePlanes, derivePersonalControlDir, derivePersonalMemoryDir, mergeHarvestReports, mergeInjections, needsDualRoots, parsedProjectPlane } from "../memory-engine/dual-root.js";
3
3
  import { normalizeMemorySpec } from "../memory.js";
4
4
  import { MemoryEngine } from "../memory-engine/engine.js";
@@ -38,7 +38,7 @@ export async function prepareMemory(input) {
38
38
  recordProjectIdHint(engineRoot, repoRoot, marker.projectId);
39
39
  if (firstSwitch) {
40
40
  try {
41
- const oldCtl = deriveControlPlaneDir(engineRoot, repoRoot);
41
+ const oldCtl = deriveRepoControlPlaneDir(engineRoot, repoRoot);
42
42
  const newCtl = deriveProjectControlDir(engineRoot, marker.projectId);
43
43
  const drained = drainMemoryAnnouncements(oldCtl);
44
44
  for (const ann of drained.queue)
@@ -100,7 +100,7 @@ export async function prepareMemory(input) {
100
100
  const projectEngine = new MemoryEngine({
101
101
  backend,
102
102
  memoryDir,
103
- controlDir: identityKey !== undefined ? deriveProjectControlDir(engineRoot, identityKey) : deriveControlPlaneDir(engineRoot, repoRoot),
103
+ controlDir: identityKey !== undefined ? deriveProjectControlDir(engineRoot, identityKey) : deriveRepoControlPlaneDir(engineRoot, repoRoot),
104
104
  });
105
105
  const personalEngine = createPersonalEngine();
106
106
  const p = planes;
@@ -140,7 +140,7 @@ export async function prepareMemory(input) {
140
140
  const engine = new MemoryEngine({
141
141
  backend,
142
142
  memoryDir,
143
- controlDir: identityKey !== undefined ? deriveProjectControlDir(engineRoot, identityKey) : deriveControlPlaneDir(engineRoot, repoRoot),
143
+ controlDir: identityKey !== undefined ? deriveProjectControlDir(engineRoot, identityKey) : deriveRepoControlPlaneDir(engineRoot, repoRoot),
144
144
  });
145
145
  const handle = await engine.materialize(memorySpec.scopes, memorySpec.writeScope);
146
146
  writeEngine = engine;
@@ -16,7 +16,7 @@ import { createSubagentWorktreeHelper, forkGovernanceDenial } from "../../agents
16
16
  import { createAgentTranscriptTool, AGENT_TRANSCRIPT_TOOL_NAME } from "../../agents/agent-transcript-tool.js";
17
17
  import { createSendMessageTool, SEND_MESSAGE_TOOL_NAME } from "../../agents/send-message-tool.js";
18
18
  import { SubagentRetainLedger } from "../../agents/retain-ledger.js";
19
- import { combinePolicies, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, resolveAsk, toolPolicyNameSets } from "../tool-policy.js";
19
+ import { combinePolicies, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, refuseOutOfContractDecision, resolveAsk, toolPolicyNameSets } from "../tool-policy.js";
20
20
  import { ActiveSkillScope, createActiveSkillScopePolicy } from "./active-skill-scope.js";
21
21
  import { computeCallCap, createCallCapRef, softExecDeadlineMs, toolCutDeadlineMs, WALLTIME_STALL_CONNECT_MS, WALLTIME_STALL_FIRST_TOKEN_MS, WALLTIME_STALL_IDLE_MS } from "./call-cap.js";
22
22
  import { createCutKillRegistry } from "./cut-kill.js";
@@ -2364,7 +2364,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2364
2364
  toolName: creq.toolName,
2365
2365
  toolCallId: creq.toolCallId,
2366
2366
  args: editArgs,
2367
- message: re.message ?? re.message ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
2367
+ message: re.message ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
2368
2368
  ...askSourceIdentity(),
2369
2369
  ...(re.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
2370
2370
  }, onAskOf, csignal ?? abortController.signal);
@@ -2410,7 +2410,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2410
2410
  toolName: creq.toolName,
2411
2411
  toolCallId: creq.toolCallId,
2412
2412
  args: presentedArgs,
2413
- message: first.message ?? first.message ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
2413
+ message: first.message ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
2414
2414
  ...askSourceIdentity(),
2415
2415
  ...(first.action === "ask" && first.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
2416
2416
  }, pc.onAsk, csignal ?? abortController.signal);
@@ -2466,7 +2466,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2466
2466
  toolName: creq.toolName,
2467
2467
  toolCallId: creq.toolCallId,
2468
2468
  args: presentedArgs,
2469
- message: decision.message ?? decision.message ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
2469
+ message: decision.message ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
2470
2470
  ...askSourceIdentity(),
2471
2471
  ...(decision.action === "ask" && decision.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
2472
2472
  }, pc.onAsk, csignal ?? abortController.signal);
@@ -2706,7 +2706,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2706
2706
  const preview = approvalPreviewOf(req.toolName, req.args);
2707
2707
  return preview !== undefined ? { preview } : {};
2708
2708
  })(),
2709
- message: decision.message ?? decision.message ?? `approval required for "${req.toolName}"`,
2709
+ message: decision.message ?? `approval required for "${req.toolName}"`,
2710
2710
  ...askSourceIdentity(),
2711
2711
  ...(decision.action === "ask" && decision.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
2712
2712
  }, onAsk, abortController.signal);
@@ -2750,17 +2750,33 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2750
2750
  const checkpointStore = spec.checkpointStore ?? deps.checkpointStore;
2751
2751
  const durableApproval = spec.durableApproval ??
2752
2752
  (runtimeCaps?.forceDurableGate ? { scope: spec.principal || DEFAULT_IRREVERSIBLE_SCOPE } : undefined);
2753
- const serializeCheckpointState = (workspaceHandle) => ({
2753
+ const inFlightSpendMicroUsd = () => {
2754
+ const own = liveSpendRef.get?.().costMicroUsd ?? 0;
2755
+ const seededNested = resume?.seed.nestedStats.costMicroUsd;
2756
+ const nestedDelta = nestedStats.costMicroUsd - (typeof seededNested === "number" && Number.isFinite(seededNested) ? seededNested : 0);
2757
+ return (Number.isFinite(own) ? own : 0) + Math.max(0, Number.isFinite(nestedDelta) ? nestedDelta : 0);
2758
+ };
2759
+ const repairBundleForCheckpoint = (parkedSpendMicroUsd) => {
2760
+ const carried = internals?.repairBundle !== undefined
2761
+ ? structuredClone(internals.repairBundle)
2762
+ : resume?.seed.repairBundle !== undefined
2763
+ ? structuredClone(resume.seed.repairBundle)
2764
+ : undefined;
2765
+ if (carried === undefined)
2766
+ return undefined;
2767
+ if (parkedSpendMicroUsd === undefined || !Number.isFinite(parkedSpendMicroUsd) || parkedSpendMicroUsd <= 0)
2768
+ return carried;
2769
+ const prior = typeof carried.spentMicroUsd === "number" && Number.isFinite(carried.spentMicroUsd) ? Math.max(0, carried.spentMicroUsd) : 0;
2770
+ carried.spentMicroUsd = prior + parkedSpendMicroUsd;
2771
+ return carried;
2772
+ };
2773
+ const serializeCheckpointState = (workspaceHandle, parkedSpendMicroUsd) => ({
2754
2774
  activeTools: [...activeTools],
2755
2775
  outputRef: { value: outputRef.value, set: outputRef.set },
2756
2776
  nestedStats: { ...nestedStats },
2757
2777
  consolidationNotes: undefined,
2758
2778
  readFileState: readFileStateForCheckpoint ? [...readFileStateForCheckpoint.entries()] : undefined,
2759
- repairBundle: internals?.repairBundle !== undefined
2760
- ? structuredClone(internals.repairBundle)
2761
- : resume?.seed.repairBundle !== undefined
2762
- ? structuredClone(resume.seed.repairBundle)
2763
- : undefined,
2779
+ repairBundle: repairBundleForCheckpoint(parkedSpendMicroUsd),
2764
2780
  workspaceHandle,
2765
2781
  handsCwd: handsCwdRef?.current,
2766
2782
  activeWorktree: worktreeSessionRef?.current ? { ...worktreeSessionRef.current } : undefined,
@@ -2978,7 +2994,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2978
2994
  leafId,
2979
2995
  gate,
2980
2996
  pendingAction: { kind: "plan_review" },
2981
- state: serializeCheckpointState(remoteHandle),
2997
+ state: serializeCheckpointState(remoteHandle, inFlightSpendMicroUsd()),
2982
2998
  status: "pending",
2983
2999
  createdAt: mintedAt,
2984
3000
  suspendedAt: now(),
@@ -3092,7 +3108,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3092
3108
  };
3093
3109
  const scope = durableApproval?.scope || checkpointScopeOf({ principal: spec.principal });
3094
3110
  const ttlMs = sanitizedTtlMs(durableApproval?.ttlMs);
3095
- const checkpointState = serializeCheckpointState(remoteHandle);
3111
+ const checkpointState = serializeCheckpointState(remoteHandle, inFlightSpendMicroUsd());
3096
3112
  const approvalLedger = debitLedger(priorLedger, liveSpendRef.get?.() ?? { costMicroUsd: 0, tokens: 0, turns: 0, walltimeMs: 0 }, resourceTotal, { countSlice: false });
3097
3113
  const mintedAt = Date.now();
3098
3114
  cp = {
@@ -3396,7 +3412,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3396
3412
  ? undefined
3397
3413
  : async (path) => {
3398
3414
  try {
3399
- const d = await denyNarrowingPolicy.check({ toolName: "Read", args: { file_path: path }, toolCallId: "changed-files-visibility-probe" }, abortController.signal);
3415
+ const d = refuseOutOfContractDecision(await denyNarrowingPolicy.check({ toolName: "Read", args: { file_path: path }, toolCallId: "changed-files-visibility-probe" }, abortController.signal));
3400
3416
  return d.action === "deny";
3401
3417
  }
3402
3418
  catch {
@@ -36,7 +36,7 @@ import { delimitUntrusted, inlineUntrusted, sanitizeUntrustedText } from "../unt
36
36
  import { reconcileInterruptedSession } from "../session-reconcile.js";
37
37
  import { RunnerSharedToolResultStore } from "../tool-result-store.js";
38
38
  import { formatDiagnosticsBlock } from "../lsp-diagnostics.js";
39
- import { toolPolicyNameSets } from "../tool-policy.js";
39
+ import { refuseOutOfContractDecision, toolPolicyNameSets } from "../tool-policy.js";
40
40
  import { defaultTaskRegistry } from "../task-registry.js";
41
41
  import { discloseDroppedPending, PendingSessionNotifications, renderTaskNotificationXml, SystemInjectionQueue, taskNotificationDedupKey } from "../task-notification.js";
42
42
  import { ToolDetachHub } from "../tool-detach.js";
@@ -1674,13 +1674,19 @@ export class Runner {
1674
1674
  }
1675
1675
  rs.telemetry.taskStart = Date.now();
1676
1676
  rs.telemetry.taskStartMonotonic = performance.now();
1677
- void Promise.resolve(this.sessions.noteTaskRun?.(prepared.sessionId, rs.telemetry.taskId)).catch((err) => {
1677
+ const discloseNoteTaskRunFailure = (err) => {
1678
1678
  try {
1679
1679
  this.deps.onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "config", sessionId: prepared.sessionId });
1680
1680
  }
1681
1681
  catch {
1682
1682
  }
1683
- });
1683
+ };
1684
+ try {
1685
+ void Promise.resolve(this.sessions.noteTaskRun?.(prepared.sessionId, rs.telemetry.taskId)).catch(discloseNoteTaskRunFailure);
1686
+ }
1687
+ catch (err) {
1688
+ discloseNoteTaskRunFailure(err);
1689
+ }
1684
1690
  rs.degrade.recordDegraded = (info, toModel) => {
1685
1691
  if (rs.degrade.degraded !== undefined)
1686
1692
  return;
@@ -3347,7 +3353,7 @@ export class Runner {
3347
3353
  return;
3348
3354
  }
3349
3355
  if (outcome.decision === "allow" && outcome.updatedInput !== undefined && prepared.basePolicyForResumeEdit) {
3350
- const rechecked = await prepared.basePolicyForResumeEdit.check({ toolName: pendingAction.toolName, args: resolvedArgs, toolCallId: pendingAction.toolCallId }, prepared.abortController.signal);
3356
+ const rechecked = refuseOutOfContractDecision(await prepared.basePolicyForResumeEdit.check({ toolName: pendingAction.toolName, args: resolvedArgs, toolCallId: pendingAction.toolCallId }, prepared.abortController.signal));
3351
3357
  if (rechecked.action === "deny") {
3352
3358
  emitEnd(true);
3353
3359
  const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, formatHookFeedback(`The approver EDITED this call's input; the edited call is denied by the deployment's tool policy and was not executed${rechecked.message ? `: ${rechecked.message}` : ""}.`), true));
@@ -3356,7 +3362,7 @@ export class Runner {
3356
3362
  }
3357
3363
  }
3358
3364
  if (prepared.denyNarrowingPolicy) {
3359
- const narrowed = await prepared.denyNarrowingPolicy.check({ toolName: pendingAction.toolName, args: resolvedArgs, toolCallId: pendingAction.toolCallId }, prepared.abortController.signal);
3365
+ const narrowed = refuseOutOfContractDecision(await prepared.denyNarrowingPolicy.check({ toolName: pendingAction.toolName, args: resolvedArgs, toolCallId: pendingAction.toolCallId }, prepared.abortController.signal));
3360
3366
  if (narrowed.action === "deny") {
3361
3367
  emitEnd(true);
3362
3368
  const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, formatHookFeedback(`The approved tool call "${pendingAction.toolName}" is now denied by a session rule and was not executed${narrowed.message ? `: ${narrowed.message}` : ""}.`), true));
@@ -1,6 +1,6 @@
1
1
  import { Type } from "typebox";
2
2
  import { Value } from "typebox/value";
3
- import { defineTool } from "../tools.js";
3
+ import { defineTool, errorResult } from "../tools.js";
4
4
  export const TOOL_SEARCH_NAME = "ToolSearch";
5
5
  const DEFER_AUTO_FRACTION = 0.1;
6
6
  const CHARS_PER_TOKEN = 4;
@@ -247,6 +247,11 @@ export function createToolSearchTool(opts) {
247
247
  }),
248
248
  effect: "read",
249
249
  execute: async (raw) => {
250
+ const staleSelect = (raw ?? {});
251
+ if (staleSelect.select !== undefined) {
252
+ return errorResult(`Error (${TOOL_SEARCH_NAME}): \`select\` is not a parameter (the array form is retired) — put the ` +
253
+ `selection in \`query\` instead: {"query":"select:ToolA,ToolB"}. Nothing was activated.`);
254
+ }
250
255
  const args = (raw ?? {});
251
256
  const { matched, missing } = resolveToolSearchDetailed(args, registry);
252
257
  const mounted = mountedNames?.() ?? new Set();
@@ -30,6 +30,7 @@ export declare function decisionText(d: PermissionResult): string | undefined;
30
30
  export interface ToolPolicy {
31
31
  check(req: ToolCallRequest, signal?: AbortSignal): PermissionResult | Promise<PermissionResult>;
32
32
  }
33
+ export declare function refuseOutOfContractDecision(d: PermissionResult): PermissionResult;
33
34
  export interface ToolPolicyNameSets {
34
35
  readonly allow?: readonly string[];
35
36
  readonly deny?: readonly string[];
@@ -6,6 +6,16 @@ export function decisionText(d) {
6
6
  return d.message;
7
7
  }
8
8
  const ALLOW = { action: "allow" };
9
+ const RETIRED_TEXT_FIELD = "reason";
10
+ const RETIRED_TEXT_FIELD_DENY_MESSAGE = `a permission decision carries the retired "${RETIRED_TEXT_FIELD}" field — rename it to "message" (the one text field ` +
11
+ `a decision carries); denied fail-closed rather than executing a decision whose text this layer cannot read`;
12
+ export function refuseOutOfContractDecision(d) {
13
+ if (typeof d !== "object" || d === null)
14
+ return d;
15
+ if (!Object.prototype.hasOwnProperty.call(d, RETIRED_TEXT_FIELD))
16
+ return d;
17
+ return { action: "deny", message: RETIRED_TEXT_FIELD_DENY_MESSAGE, decisionReason: "rule" };
18
+ }
9
19
  function withTimeout(p, ms, onTimeout) {
10
20
  if (ms === undefined) {
11
21
  return p;
@@ -86,7 +96,7 @@ export function createApprovalPolicy(opts) {
86
96
  if (okRaw === true)
87
97
  return ALLOW;
88
98
  if (okRaw !== false) {
89
- return { action: "deny", message: `approval callback for "${req.toolName}" returned an out-of-contract value (${typeof ok}) — denied fail-closed (return true, false, or the {allow} object)` };
99
+ return { action: "deny", message: `approval callback for "${req.toolName}" returned an out-of-contract value (${typeof ok}) — denied fail-closed (this policy's \`approve\` returns a boolean: return true or false)` };
90
100
  }
91
101
  return { action: "deny", message: `approval denied for "${req.toolName}"` };
92
102
  }
@@ -106,7 +116,7 @@ export function combinePolicies(...policies) {
106
116
  let current = req;
107
117
  let rewrite;
108
118
  for (const p of policies) {
109
- const d = await p.check(current, signal);
119
+ const d = refuseOutOfContractDecision(await p.check(current, signal));
110
120
  if (d.action === "deny") {
111
121
  return rewrite?.updatedInput !== undefined ? { ...d, updatedInput: rewrite.updatedInput } : d;
112
122
  }
package/dist/index.d.ts CHANGED
@@ -77,7 +77,7 @@ export { InMemoryCheckpointStore, CheckpointError, mintCheckpointToken, checkpoi
77
77
  export { InMemoryFileSnapshotStore, DEFAULT_SNAPSHOT_BOUNDS } from "./core/file-snapshot-store.js";
78
78
  export { captureManifest, applyManifest } from "./core/file-snapshot-store.js";
79
79
  export type { FileSnapshotStore, FileSnapshotResult, FileSnapshotError, FileSnapshotBounds } from "./core/file-snapshot-store.js";
80
- export { FileStorageBackend, FileSessionRepo, FileCheckpointStore, FileMemoryStore, FileToolResultStore, FileSessionPolicyStore, FileFileSnapshotStore, FileWorkflowJournalStore, MAX_JOURNAL_RESULT_BYTES, oversizeJournalResult, resolveDataRoot, sanitizeScope, sanitizePathComponent, createFileConsolidationLock, atomicWriteFile, writeThenLink, ensureDir, readJsonlRecords, AppendLog, type FileStorageBackendOptions, type FileCheckpointStoreOptions, } from "./stores/file/index.js";
80
+ export { FileStorageBackend, FileSessionRepo, FileCheckpointStore, FileMemoryStore, FileToolResultStore, FileSessionPolicyStore, FileFileSnapshotStore, FileWorkflowJournalStore, MAX_JOURNAL_RESULT_BYTES, oversizeJournalResult, resolveDataRoot, sanitizeScope, sanitizePathComponent, createFileConsolidationLock, atomicWriteFile, writeThenLink, ensureDir, readJsonlRecords, AppendLog, type FileStorageBackendOptions, type FileStorageCorruptReadInfo, type FileSessionRepoOptions, type FileFileSnapshotStoreOptions, type FileCheckpointStoreOptions, } from "./stores/file/index.js";
81
81
  export { CacheBreakDetector, type CacheBreakFinding, type ToolFingerprintInput } from "./core/cache-break-detector.js";
82
82
  export { maybeCompact, type MaybeCompactOptions, type CompactionWindowSafetyInfo } from "./core/auto-compaction.js";
83
83
  export { brainToRuntime } from "./core/runtime.js";
@@ -113,7 +113,7 @@ export { AUTO_MODE_BASE_PROMPT, AUTO_MODE_PERMISSIONS_EXTERNAL } from "./core/au
113
113
  export { createPermissionRulePolicy, validatePermissionRules, parsePermissionRule, wildcardMatch, type PermissionRule, type ParsedPermissionRule, type PermissionRuleIssue, type PermissionRuleCaps, type PermissionRulePolicyOptions, } from "./core/permission-rules.js";
114
114
  export { formatHookFeedback, runToolGate, type Hooks, type HookToolContext, type HookToolOutput, type PreToolUseResult, type PostToolUseResult, type UserPromptSubmitResult, type HookToolFailure, type PostToolUseFailureResult, type PostToolBatchCall, type PostToolBatchResult, type PreCompactContext, type PreCompactResult, type PostCompactContext, type StopFailureContext, type PermissionDeniedPayload, type PermissionDeniedSource, } from "./core/hooks.js";
115
115
  export { InMemoryMemoryStore, composeMemoryBlock, composeLayeredMemoryBlock, normalizeMemorySpec, type NormalizedMemorySpec, type MemorySpecInput, supportsConsolidation, supportsPeriodicConsolidation, firstSentence, expandLexicalTerms, lexicalSearchMatch, type Embedder, classifyPromotable, enforcePromotableWriteGate, guardedMemoryStore, MemoryGateError, detectSecret, enforceSecretWriteGate, enforceStructuredNoteSecretGate, type UtilityGate, type MemoryStore, type MemoryVectorMode, type ScoredMemory, type MemoryNoteHeader, type MemoryNoteRecord, type MemoryNoteType, type StructuredNoteInput, } from "./core/memory.js";
116
- export { MemoryEngine, FileMemoryEngineBackend, memoryBackendContract, assertMemoryBackendSearchEquivalence, type MemoryBackendContractHooks, buildMemoryInstruction, truncateIndex, scanEntryFiles, deriveRepoKey, deriveRepoMemoryDir, deriveControlPlaneDir, resolveMemoryEngineRoot, scopeDirName, ControlPlaneCorruptError, parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_INDEX_FILENAME, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_MAX_ENTRY_DEPTH, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE, renderAnnouncements, enqueueMemoryAnnouncement, drainMemoryAnnouncements, peekMemoryAnnouncements, bumpScanFuse, scanFuseCount, clearScanFuse, ANNOUNCEMENTS_FILE, MEMORY_ANNOUNCEMENTS_MAX, SCAN_FUSE_FILE, SCAN_FUSE_THRESHOLD, type MemoryAnnouncement, type ScanFinding, type MemoryEngineOptions, type MemoryInjection, type MemoryBackend, type MemoryEntry, type MemoryEntryFrontmatter, type MemoryEntryHeader, type ScoredMemoryEntry, type NotePatch, type PatchReport, type MaterializedFile, type MemorySessionHandle, type HarvestReport, type HarvestRejection, type HarvestRejectionCode, type ParsedEntryFile, type ScannedEntryFile, SCOPE_SEGMENT_MAX_ENCODED, PROJECT_MARKER_PATH, encodeScopeSegment, decodeScopeSegment, parseScopeKey, formatUserScope, formatOrgScope, formatProjScope, formatUserProjScope, isPersonalScope, assertScopeContractPlacement, formatProjectMarker, parseProjectMarker, resolveProjectId, PROJECT_ID_REGEX, type ParsedScopeKey, migrateScope, type MigrateScopeReport, materializeEntriesToFiles, harvestFilesToPatches, projectionsToWriteBack, screenInboundEntries, REMOTE_HARVEST_PER_FILE_BYTES, REMOTE_MASS_DELETION_FUSE_RATIO, type RemoteMemoryFile, type RemoteMemoryBaseline, type RemoteMaterialization, type RemoteHarvestResult, type InboundEntryFinding, deriveProjectMemoryDir, deriveProjectControlDir, recordProjectIdHint, lookupProjectIdHint, PROJECT_ID_HINTS_FILE, reconcileMemoryEntries, nextSyncBaseline, mergeRecallHits, type MemorySyncCursor, type MemorySyncConflict, type MemorySyncPlan, type RecallSource, syncMemoryScope, type MemorySyncTransport, type MemorySyncRequestBody, type MemorySyncResponseBody, type MemorySyncClientConflict, type SyncMemoryScopeOptions, type MemorySyncClientResult, } from "./core/memory-engine/index.js";
116
+ export { MemoryEngine, FileMemoryEngineBackend, memoryBackendContract, assertMemoryBackendSearchEquivalence, type MemoryBackendContractHooks, buildMemoryInstruction, truncateIndex, scanEntryFiles, deriveRepoKey, deriveRepoMemoryDir, deriveControlPlaneDir, deriveRepoControlPlaneDir, resolveMemoryEngineRoot, scopeDirName, ControlPlaneCorruptError, parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_INDEX_FILENAME, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_MAX_ENTRY_DEPTH, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE, renderAnnouncements, enqueueMemoryAnnouncement, drainMemoryAnnouncements, peekMemoryAnnouncements, bumpScanFuse, scanFuseCount, clearScanFuse, ANNOUNCEMENTS_FILE, MEMORY_ANNOUNCEMENTS_MAX, SCAN_FUSE_FILE, SCAN_FUSE_THRESHOLD, type MemoryAnnouncement, type ScanFinding, type MemoryEngineOptions, type MemoryInjection, type MemoryBackend, type MemoryEntry, type MemoryEntryFrontmatter, type MemoryEntryHeader, type ScoredMemoryEntry, type NotePatch, type PatchReport, type MaterializedFile, type MemorySessionHandle, type HarvestReport, type HarvestRejection, type HarvestRejectionCode, type ParsedEntryFile, type ScannedEntryFile, SCOPE_SEGMENT_MAX_ENCODED, PROJECT_MARKER_PATH, encodeScopeSegment, decodeScopeSegment, parseScopeKey, formatUserScope, formatOrgScope, formatProjScope, formatUserProjScope, isPersonalScope, assertScopeContractPlacement, formatProjectMarker, parseProjectMarker, resolveProjectId, PROJECT_ID_REGEX, type ParsedScopeKey, migrateScope, type MigrateScopeReport, materializeEntriesToFiles, harvestFilesToPatches, projectionsToWriteBack, screenInboundEntries, REMOTE_HARVEST_PER_FILE_BYTES, REMOTE_MASS_DELETION_FUSE_RATIO, type RemoteMemoryFile, type RemoteMemoryBaseline, type RemoteMaterialization, type RemoteHarvestResult, type InboundEntryFinding, deriveProjectMemoryDir, deriveProjectControlDir, recordProjectIdHint, lookupProjectIdHint, PROJECT_ID_HINTS_FILE, reconcileMemoryEntries, nextSyncBaseline, mergeRecallHits, type MemorySyncCursor, type MemorySyncConflict, type MemorySyncPlan, type RecallSource, syncMemoryScope, type MemorySyncTransport, type MemorySyncRequestBody, type MemorySyncResponseBody, type MemorySyncClientConflict, type SyncMemoryScopeOptions, type MemorySyncClientResult, } from "./core/memory-engine/index.js";
117
117
  export { termSet, jaccardDistance, cosineDistance } from "./core/memory-vector.js";
118
118
  export { encodeSurfacedKey, buildManifestText, validateSelectedIds, composeSelectiveBody, formatMemoryAge, resolveLinkedIds, RECALL_CAVEAT, DEFAULT_MAX_SELECTED, DEFAULT_MAX_LINKED, type MemorySelector, type MemorySelectRequest, type SelectiveRecallOptions, type SelectiveRecallResult, type LayeredRecallOptions, type LayeredRecallResult, type ScopedNoteHeader, type ScopedNoteRecord, } from "./core/memory-recall.js";
119
119
  export { runMemoryConsolidation, CONSOLIDATION_SYSTEM_PROMPT, DEFAULT_CONSOLIDATION_BAND, DEFAULT_CONSOLIDATION_SEARCH_LIMIT, DEFAULT_CONSOLIDATION_MAX_NOTES, DEFAULT_CONSOLIDATION_TIMEOUT_SEC, normalizeForExactMatch, type ConsolidationParams, type ConsolidationStats, type ConsolidationNote, type ConsolidationLLM, } from "./core/runner/memory-consolidation.js";
package/dist/index.js CHANGED
@@ -100,7 +100,7 @@ export { AUTO_MODE_BASE_PROMPT, AUTO_MODE_PERMISSIONS_EXTERNAL } from "./core/au
100
100
  export { createPermissionRulePolicy, validatePermissionRules, parsePermissionRule, wildcardMatch, } from "./core/permission-rules.js";
101
101
  export { formatHookFeedback, runToolGate, } from "./core/hooks.js";
102
102
  export { InMemoryMemoryStore, composeMemoryBlock, composeLayeredMemoryBlock, normalizeMemorySpec, supportsConsolidation, supportsPeriodicConsolidation, firstSentence, expandLexicalTerms, lexicalSearchMatch, classifyPromotable, enforcePromotableWriteGate, guardedMemoryStore, MemoryGateError, detectSecret, enforceSecretWriteGate, enforceStructuredNoteSecretGate, } from "./core/memory.js";
103
- export { MemoryEngine, FileMemoryEngineBackend, memoryBackendContract, assertMemoryBackendSearchEquivalence, buildMemoryInstruction, truncateIndex, scanEntryFiles, deriveRepoKey, deriveRepoMemoryDir, deriveControlPlaneDir, resolveMemoryEngineRoot, scopeDirName, ControlPlaneCorruptError, parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_INDEX_FILENAME, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_MAX_ENTRY_DEPTH, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE, renderAnnouncements, enqueueMemoryAnnouncement, drainMemoryAnnouncements, peekMemoryAnnouncements, bumpScanFuse, scanFuseCount, clearScanFuse, ANNOUNCEMENTS_FILE, MEMORY_ANNOUNCEMENTS_MAX, SCAN_FUSE_FILE, SCAN_FUSE_THRESHOLD, SCOPE_SEGMENT_MAX_ENCODED, PROJECT_MARKER_PATH, encodeScopeSegment, decodeScopeSegment, parseScopeKey, formatUserScope, formatOrgScope, formatProjScope, formatUserProjScope, isPersonalScope, assertScopeContractPlacement, formatProjectMarker, parseProjectMarker, resolveProjectId, PROJECT_ID_REGEX, migrateScope, materializeEntriesToFiles, harvestFilesToPatches, projectionsToWriteBack, screenInboundEntries, REMOTE_HARVEST_PER_FILE_BYTES, REMOTE_MASS_DELETION_FUSE_RATIO, deriveProjectMemoryDir, deriveProjectControlDir, recordProjectIdHint, lookupProjectIdHint, PROJECT_ID_HINTS_FILE, reconcileMemoryEntries, nextSyncBaseline, mergeRecallHits, syncMemoryScope, } from "./core/memory-engine/index.js";
103
+ export { MemoryEngine, FileMemoryEngineBackend, memoryBackendContract, assertMemoryBackendSearchEquivalence, buildMemoryInstruction, truncateIndex, scanEntryFiles, deriveRepoKey, deriveRepoMemoryDir, deriveControlPlaneDir, deriveRepoControlPlaneDir, resolveMemoryEngineRoot, scopeDirName, ControlPlaneCorruptError, parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_INDEX_FILENAME, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_MAX_ENTRY_DEPTH, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE, renderAnnouncements, enqueueMemoryAnnouncement, drainMemoryAnnouncements, peekMemoryAnnouncements, bumpScanFuse, scanFuseCount, clearScanFuse, ANNOUNCEMENTS_FILE, MEMORY_ANNOUNCEMENTS_MAX, SCAN_FUSE_FILE, SCAN_FUSE_THRESHOLD, SCOPE_SEGMENT_MAX_ENCODED, PROJECT_MARKER_PATH, encodeScopeSegment, decodeScopeSegment, parseScopeKey, formatUserScope, formatOrgScope, formatProjScope, formatUserProjScope, isPersonalScope, assertScopeContractPlacement, formatProjectMarker, parseProjectMarker, resolveProjectId, PROJECT_ID_REGEX, migrateScope, materializeEntriesToFiles, harvestFilesToPatches, projectionsToWriteBack, screenInboundEntries, REMOTE_HARVEST_PER_FILE_BYTES, REMOTE_MASS_DELETION_FUSE_RATIO, deriveProjectMemoryDir, deriveProjectControlDir, recordProjectIdHint, lookupProjectIdHint, PROJECT_ID_HINTS_FILE, reconcileMemoryEntries, nextSyncBaseline, mergeRecallHits, syncMemoryScope, } from "./core/memory-engine/index.js";
104
104
  export { termSet, jaccardDistance, cosineDistance } from "./core/memory-vector.js";
105
105
  export { encodeSurfacedKey, buildManifestText, validateSelectedIds, composeSelectiveBody, formatMemoryAge, resolveLinkedIds, RECALL_CAVEAT, DEFAULT_MAX_SELECTED, DEFAULT_MAX_LINKED, } from "./core/memory-recall.js";
106
106
  export { runMemoryConsolidation, CONSOLIDATION_SYSTEM_PROMPT, DEFAULT_CONSOLIDATION_BAND, DEFAULT_CONSOLIDATION_SEARCH_LIMIT, DEFAULT_CONSOLIDATION_MAX_NOTES, DEFAULT_CONSOLIDATION_TIMEOUT_SEC, normalizeForExactMatch, } from "./core/runner/memory-consolidation.js";
@@ -338,6 +338,10 @@ 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);
@@ -347,7 +351,7 @@ export async function createRunWorkflowTool(d) {
347
351
  script = resolved.script;
348
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
  }
@@ -101,7 +101,6 @@ export function assemblePrompt(inputs) {
101
101
  userSystemPrompt: inputs.userSystemPrompt,
102
102
  userAppendSystemPrompt: inputs.userAppendSystemPrompt,
103
103
  tools: inputs.tools,
104
- consolidationEnabled: false,
105
104
  ...facts,
106
105
  };
107
106
  let pack = SEMA_DEFAULT_PACK;
@@ -34,28 +34,66 @@ export function createCcFileMailboxStore(opts) {
34
34
  catch {
35
35
  }
36
36
  };
37
- const loadBox = (path) => {
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) => {
38
60
  let raw;
39
61
  try {
40
62
  raw = readFileSync(path, "utf8");
41
63
  }
42
64
  catch (err) {
43
65
  if (err.code !== "ENOENT")
44
- discloseCorrupt(path, `read failed (non-ENOENT): ${err.message}`);
66
+ note(path, `read failed (non-ENOENT): ${err.message}`);
45
67
  return [];
46
68
  }
69
+ let parsed;
47
70
  try {
48
- const parsed = JSON.parse(raw);
49
- if (!Array.isArray(parsed)) {
50
- discloseCorrupt(path, "inbox document is not an array");
51
- return [];
52
- }
53
- return parsed;
71
+ parsed = JSON.parse(raw);
54
72
  }
55
73
  catch {
56
- discloseCorrupt(path, "unparseable inbox JSON (torn write?)");
74
+ note(path, "unparseable inbox JSON (torn write?)");
57
75
  return [];
58
76
  }
77
+ if (!Array.isArray(parsed)) {
78
+ note(path, "inbox document is not an array");
79
+ return [];
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;
59
97
  };
60
98
  const saveBox = (path, box) => {
61
99
  atomicWriteFile(inboxDir, path, JSON.stringify(box, null, 2));
@@ -65,8 +103,9 @@ export function createCcFileMailboxStore(opts) {
65
103
  requireDefaultScope(scope);
66
104
  mkdirSync(inboxDir, { recursive: true });
67
105
  const path = inboxPath(handle);
106
+ const notes = deferredNotes();
68
107
  return withCcLock(path, () => {
69
- const box = loadBox(path);
108
+ const box = loadBox(path, notes.note);
70
109
  box.push({
71
110
  type: "message",
72
111
  ...(msg.from !== undefined ? { from: msg.from } : {}),
@@ -78,6 +117,8 @@ export function createCcFileMailboxStore(opts) {
78
117
  });
79
118
  saveBox(path, box);
80
119
  return box.length;
120
+ }).finally(() => {
121
+ notes.flush();
81
122
  });
82
123
  },
83
124
  claimLease: (scope, handle, owner, ttlMs, now) => {
@@ -87,7 +128,7 @@ export function createCcFileMailboxStore(opts) {
87
128
  const held = leases.get(key);
88
129
  if (held !== undefined && held.expiresAt > t && held.owner !== owner)
89
130
  return Promise.resolve(null);
90
- const box = loadBox(key);
131
+ const box = loadBox(key, discloseCorrupt);
91
132
  const maxSeq = box.length;
92
133
  const pending = box
93
134
  .map((e, i) => ({ e, seq: i + 1 }))
@@ -111,8 +152,9 @@ export function createCcFileMailboxStore(opts) {
111
152
  return Promise.resolve();
112
153
  if (!existsSync(path))
113
154
  return Promise.resolve();
155
+ const notes = deferredNotes();
114
156
  return withCcLock(path, () => {
115
- const box = loadBox(path);
157
+ const box = loadBox(path, notes.note);
116
158
  let changed = false;
117
159
  for (let i = 0; i < Math.min(upToSeq, box.length); i++) {
118
160
  if (box[i].read !== true) {
@@ -124,6 +166,8 @@ export function createCcFileMailboxStore(opts) {
124
166
  saveBox(path, box);
125
167
  if (held.maxSeq <= upToSeq)
126
168
  leases.delete(path);
169
+ }).finally(() => {
170
+ notes.flush();
127
171
  });
128
172
  },
129
173
  releaseLease: (scope, handle, owner) => {
@@ -136,7 +180,7 @@ export function createCcFileMailboxStore(opts) {
136
180
  },
137
181
  peekCount: (scope, handle) => {
138
182
  requireDefaultScope(scope);
139
- 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);
140
184
  },
141
185
  drop: (scope, handle) => {
142
186
  requireDefaultScope(scope);
@@ -160,7 +204,7 @@ export function createCcFileMailboxStore(opts) {
160
204
  if (!f.endsWith(".json"))
161
205
  continue;
162
206
  const path = join(inboxDir, f);
163
- const box = loadBox(path);
207
+ const box = loadBox(path, discloseCorrupt);
164
208
  const newest = box.reduce((acc, e) => {
165
209
  const t = typeof e.timestamp === "string" ? Date.parse(e.timestamp) : Number.NaN;
166
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;