@sema-agent/core 3.0.0 → 5.0.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 (153) hide show
  1. package/CHANGELOG.md +2382 -0
  2. package/dist/agents/cascade.d.ts +3 -1
  3. package/dist/agents/cascade.js +31 -32
  4. package/dist/agents/cumulative-stats.d.ts +29 -0
  5. package/dist/agents/cumulative-stats.js +29 -0
  6. package/dist/agents/observer.d.ts +4 -0
  7. package/dist/agents/observer.js +11 -10
  8. package/dist/agents/repair-loop.d.ts +2 -0
  9. package/dist/agents/repair-loop.js +48 -24
  10. package/dist/agents/retain-ledger.d.ts +2 -0
  11. package/dist/agents/retain-ledger.js +6 -5
  12. package/dist/agents/roster-store.d.ts +1 -0
  13. package/dist/agents/roster-store.js +3 -5
  14. package/dist/agents/send-message-tool.d.ts +1 -0
  15. package/dist/agents/send-message-tool.js +2 -0
  16. package/dist/agents/subagent.d.ts +3 -1
  17. package/dist/agents/subagent.js +79 -94
  18. package/dist/agents/teacher.d.ts +1 -9
  19. package/dist/agents/teacher.js +21 -24
  20. package/dist/agents/tool-filter.js +6 -7
  21. package/dist/agents/verify.d.ts +1 -0
  22. package/dist/agents/verify.js +15 -14
  23. package/dist/brain/anthropic.js +24 -21
  24. package/dist/brain/openai.js +24 -21
  25. package/dist/brain/stream-engine.js +5 -2
  26. package/dist/brain/stream-shared.d.ts +1 -0
  27. package/dist/brain/stream-shared.js +14 -0
  28. package/dist/brain/terminal-cause.d.ts +4 -0
  29. package/dist/brain/terminal-cause.js +12 -0
  30. package/dist/brain/tool-call-id.d.ts +1 -0
  31. package/dist/brain/tool-call-id.js +3 -0
  32. package/dist/brain/tool-call-repair.js +2 -1
  33. package/dist/core/arg-summary.d.ts +21 -1
  34. package/dist/core/arg-summary.js +61 -14
  35. package/dist/core/auto-compaction.d.ts +1 -0
  36. package/dist/core/auto-compaction.js +16 -25
  37. package/dist/core/background-agent-store.d.ts +22 -22
  38. package/dist/core/background-agent-store.js +19 -23
  39. package/dist/core/checkpoint-store.d.ts +6 -13
  40. package/dist/core/checkpoint-store.js +11 -5
  41. package/dist/core/file-snapshot-store.d.ts +8 -0
  42. package/dist/core/file-snapshot-store.js +4 -3
  43. package/dist/core/fs-write-gate-policy.js +2 -3
  44. package/dist/core/hooks.d.ts +4 -1
  45. package/dist/core/hooks.js +8 -18
  46. package/dist/core/lsp.js +4 -3
  47. package/dist/core/mailbox-store.d.ts +7 -10
  48. package/dist/core/mcp.d.ts +7 -10
  49. package/dist/core/mcp.js +7 -9
  50. package/dist/core/memory-engine/engine.d.ts +0 -1
  51. package/dist/core/memory-engine/engine.js +24 -34
  52. package/dist/core/memory-engine/file-backend.d.ts +1 -2
  53. package/dist/core/memory-engine/file-backend.js +25 -36
  54. package/dist/core/memory-engine/layout.d.ts +8 -0
  55. package/dist/core/memory-engine/layout.js +60 -4
  56. package/dist/core/memory-engine/scope-contract.js +9 -3
  57. package/dist/core/memory-engine/types.d.ts +1 -1
  58. package/dist/core/permission-rules.js +2 -3
  59. package/dist/core/runner/active-skill-scope.d.ts +0 -1
  60. package/dist/core/runner/active-skill-scope.js +2 -17
  61. package/dist/core/runner/assemble-result.d.ts +2 -7
  62. package/dist/core/runner/assemble-result.js +3 -4
  63. package/dist/core/runner/compaction-call-options.d.ts +7 -0
  64. package/dist/core/runner/compaction-call-options.js +30 -0
  65. package/dist/core/runner/prepare-task.d.ts +6 -23
  66. package/dist/core/runner/prepare-task.js +36 -61
  67. package/dist/core/runner/runtask.d.ts +1 -0
  68. package/dist/core/runner/runtask.js +84 -172
  69. package/dist/core/runner/session-rule-policy.js +7 -5
  70. package/dist/core/runner/teardown-bounded.d.ts +7 -0
  71. package/dist/core/runner/teardown-bounded.js +36 -0
  72. package/dist/core/runner/tool-disclosure.d.ts +2 -5
  73. package/dist/core/runner/turn-attachments.d.ts +5 -15
  74. package/dist/core/runner/turn-attachments.js +1 -1
  75. package/dist/core/safe-notify.d.ts +17 -0
  76. package/dist/core/safe-notify.js +59 -0
  77. package/dist/core/sensitive-path-policy.js +4 -4
  78. package/dist/core/session-reconcile.js +1 -2
  79. package/dist/core/skill-tool-specifier.js +2 -3
  80. package/dist/core/skills-directory.js +2 -3
  81. package/dist/core/store-contracts/checkpoint-store-contract.js +1 -1
  82. package/dist/core/stub-env.d.ts +2 -6
  83. package/dist/core/surrogate-safe-slice.d.ts +4 -0
  84. package/dist/core/surrogate-safe-slice.js +18 -0
  85. package/dist/core/task-registry-monitor.js +8 -13
  86. package/dist/core/task-registry-shared.d.ts +23 -1
  87. package/dist/core/task-registry-shared.js +16 -0
  88. package/dist/core/task-registry.d.ts +6 -1
  89. package/dist/core/task-registry.js +52 -87
  90. package/dist/core/task-tool-shape.d.ts +0 -2
  91. package/dist/core/task-tool-shape.js +2 -5
  92. package/dist/core/tool-errors.js +5 -4
  93. package/dist/core/tool-name-aliases.d.ts +1 -2
  94. package/dist/core/tool-name-aliases.js +42 -59
  95. package/dist/core/tool-policy.js +21 -17
  96. package/dist/core/tool-result-store.d.ts +1 -0
  97. package/dist/core/tool-result-store.js +17 -1
  98. package/dist/core/trace.d.ts +9 -9
  99. package/dist/core/types.d.ts +15 -12
  100. package/dist/core/untrusted-egress.d.ts +4 -2
  101. package/dist/core/untrusted-egress.js +28 -14
  102. package/dist/core/workflow-journal-store.d.ts +9 -20
  103. package/dist/core/workflow-run-store.d.ts +11 -10
  104. package/dist/core/workflow-run-store.js +22 -16
  105. package/dist/engine/compaction/utils.js +4 -2
  106. package/dist/engine/execution-env/node-execution-env.d.ts +2 -24
  107. package/dist/engine/execution-env/node-execution-env.js +1 -1
  108. package/dist/engine/harness/agent-harness.d.ts +1 -0
  109. package/dist/engine/harness/agent-harness.js +13 -33
  110. package/dist/engine/harness/types.d.ts +29 -67
  111. package/dist/engine/llm/types.d.ts +1 -1
  112. package/dist/engine/loop/agent-loop.js +12 -14
  113. package/dist/engine/loop/types.d.ts +11 -9
  114. package/dist/engine/session/memory-repo.d.ts +2 -6
  115. package/dist/engine/session/session.d.ts +5 -30
  116. package/dist/index.d.ts +6 -3
  117. package/dist/index.js +4 -3
  118. package/dist/internal/harness-types.d.ts +3 -1
  119. package/dist/orchestration/run-spec.js +6 -4
  120. package/dist/orchestration/run-workflow-tool.js +3 -36
  121. package/dist/orchestration/workflow-governance.js +2 -1
  122. package/dist/orchestration/workflow.d.ts +4 -11
  123. package/dist/orchestration/workflow.js +164 -204
  124. package/dist/prompt-assembly/packs/sema-default.js +1 -3
  125. package/dist/prompt-assembly/tool-catalog.d.ts +10 -9
  126. package/dist/prompt-assembly/turn-snapshot.d.ts +2 -5
  127. package/dist/prompts/default.js +1 -3
  128. package/dist/prompts/simple-sections.d.ts +0 -1
  129. package/dist/prompts/simple-sections.js +0 -1
  130. package/dist/stores/file/background-agent-store.d.ts +3 -12
  131. package/dist/stores/file/background-agent-store.js +3 -41
  132. package/dist/stores/file/checkpoint-store.js +6 -6
  133. package/dist/stores/file/mailbox-store.d.ts +2 -6
  134. package/dist/stores/file/session-policy-store.js +15 -1
  135. package/dist/stores/file/session-store.d.ts +2 -6
  136. package/dist/stores/file/workflow-journal-store.d.ts +3 -11
  137. package/dist/stores/file/workflow-run-store.d.ts +3 -7
  138. package/dist/stores/file/workflow-run-store.js +4 -17
  139. package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -0
  140. package/dist/tools/fs/bash-readonly-classifier.js +60 -25
  141. package/dist/tools/fs/fs-bash.js +330 -104
  142. package/dist/tools/fs/fs-pdf.d.ts +2 -12
  143. package/dist/tools/fs/fs-shared.js +2 -2
  144. package/dist/tools/fs/fs-write.js +0 -1
  145. package/dist/tools/fs/notebook.d.ts +2 -5
  146. package/dist/tools/fs/safety.d.ts +3 -0
  147. package/dist/tools/fs/safety.js +43 -14
  148. package/dist/tools/fs/search.js +5 -5
  149. package/dist/tools/monitor.d.ts +1 -0
  150. package/dist/tools/monitor.js +7 -5
  151. package/dist/tools/worktree.d.ts +2 -7
  152. package/dist/tools/worktree.js +3 -1
  153. package/package.json +6 -2
@@ -3,7 +3,8 @@ import type { RepairBundle } from "../agents/repair-loop.js";
3
3
  import type { WorkspaceHandle } from "./remote-env.js";
4
4
  import type { ConsolidationNote } from "./runner/memory-consolidation.js";
5
5
  import type { SessionPermissionRules } from "./session-policy-store.js";
6
- import type { TaskResult } from "./types.js";
6
+ import type { NestedUsageAccum, TaskResult } from "./types.js";
7
+ import type { ActiveWorktreeSession } from "../internal/harness-types.js";
7
8
  export type CheckpointToken = string & {
8
9
  readonly __brand: "CheckpointToken";
9
10
  };
@@ -112,12 +113,7 @@ export interface CheckpointState {
112
113
  value?: unknown;
113
114
  set?: boolean;
114
115
  };
115
- nestedStats: {
116
- tokens: number;
117
- turns: number;
118
- tasks: number;
119
- costMicroUsd: number;
120
- };
116
+ nestedStats: NestedUsageAccum;
121
117
  consolidationNotes?: ConsolidationNote[];
122
118
  readFileState?: Array<[string, ReadEntry]>;
123
119
  repairBundle?: RepairBundle;
@@ -131,12 +127,7 @@ export interface CheckpointState {
131
127
  description?: string;
132
128
  }>;
133
129
  handsCwd?: string;
134
- activeWorktree?: {
135
- worktreeDir: string;
136
- originalCwd: string;
137
- baseSha: string;
138
- entered?: boolean;
139
- };
130
+ activeWorktree?: ActiveWorktreeSession;
140
131
  inheritedGate?: {
141
132
  ancestorRules?: ReadonlyArray<{
142
133
  sessionId: string;
@@ -273,6 +264,8 @@ export declare function validatePendingSteer(steer: {
273
264
  text: string;
274
265
  trusted: boolean;
275
266
  };
267
+ export declare function checkpointRowMatches(cp: Checkpoint | undefined, scope: string, status: Checkpoint["status"]): cp is Checkpoint;
268
+ export declare function checkpointOccMatches(cp: Checkpoint, expect?: ResolveExpectation): boolean;
276
269
  export type CheckpointFaultMode = "resolve-after-commit" | "resolve-before-commit";
277
270
  export declare class InMemoryCheckpointStore implements CheckpointStore {
278
271
  private cps;
@@ -237,6 +237,12 @@ export function validatePendingSteer(steer) {
237
237
  }
238
238
  return { text: steer.text, trusted: steer.trusted };
239
239
  }
240
+ export function checkpointRowMatches(cp, scope, status) {
241
+ return cp !== undefined && cp.scope === scope && cp.status === status;
242
+ }
243
+ export function checkpointOccMatches(cp, expect) {
244
+ return expect === undefined || (cp.rev ?? 0) === expect.rev;
245
+ }
240
246
  export class InMemoryCheckpointStore {
241
247
  cps = new Map();
242
248
  fault = null;
@@ -256,10 +262,10 @@ export class InMemoryCheckpointStore {
256
262
  throw new Error("injected fault: resolve before commit");
257
263
  }
258
264
  const cp = this.cps.get(token);
259
- if (!cp || cp.scope !== scope || cp.status !== "pending") {
265
+ if (!checkpointRowMatches(cp, scope, "pending")) {
260
266
  return false;
261
267
  }
262
- if (expect !== undefined && (cp.rev ?? 0) !== expect.rev) {
268
+ if (!checkpointOccMatches(cp, expect)) {
263
269
  return false;
264
270
  }
265
271
  cp.status = "resolved";
@@ -276,7 +282,7 @@ export class InMemoryCheckpointStore {
276
282
  }
277
283
  async reopen(token, scope, reason) {
278
284
  const cp = this.cps.get(token);
279
- if (!cp || cp.scope !== scope || cp.status !== "resolved") {
285
+ if (!checkpointRowMatches(cp, scope, "resolved")) {
280
286
  return false;
281
287
  }
282
288
  cp.status = "pending";
@@ -287,7 +293,7 @@ export class InMemoryCheckpointStore {
287
293
  async setPendingSteer(token, scope, steer) {
288
294
  const clean = validatePendingSteer(steer);
289
295
  const cp = this.cps.get(token);
290
- if (!cp || cp.scope !== scope || cp.status !== "pending") {
296
+ if (!checkpointRowMatches(cp, scope, "pending")) {
291
297
  return false;
292
298
  }
293
299
  cp.state.pendingSteer = clean;
@@ -295,7 +301,7 @@ export class InMemoryCheckpointStore {
295
301
  }
296
302
  async expire(token, scope) {
297
303
  const cp = this.cps.get(token);
298
- if (!cp || cp.scope !== scope || cp.status !== "pending") {
304
+ if (!checkpointRowMatches(cp, scope, "pending")) {
299
305
  return false;
300
306
  }
301
307
  cp.status = "expired";
@@ -1,3 +1,5 @@
1
+ declare function relUnder(root: string, child: string): string | null;
2
+ declare function joinUnder(root: string, rel: string): string;
1
3
  import type { ExecutionEnv } from "../internal/harness-types.js";
2
4
  export interface FileSnapshotError {
3
5
  code: "too_large" | "enumerate_failed" | "read_failed" | "restore_failed" | "not_found" | "aborted";
@@ -51,3 +53,9 @@ export declare class InMemoryFileSnapshotStore implements FileSnapshotStore {
51
53
  importManifest(scope: string, key: string, manifest: Map<string, string>, srcGetBlob: (hash: string) => Promise<Uint8Array | undefined>): Promise<FileSnapshotResult>;
52
54
  reap(scope: string, keepKeys: string[]): Promise<number>;
53
55
  }
56
+ export declare const __testables: {
57
+ joinUnder: typeof joinUnder;
58
+ relUnder: typeof relUnder;
59
+ stripTrailSep: (p: string) => string;
60
+ };
61
+ export {};
@@ -1,8 +1,8 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { isAbsolutePathForm, isWinFormPath } from "../tools/fs/safety.js";
2
3
  const normSlash = (p) => p.replace(/\\/g, "/");
3
- const stripTrailSep = (p) => p.replace(/[\\/]+$/, "");
4
+ const stripTrailSep = (p) => (isWinFormPath(p) ? p.replace(/[\\/]+$/, "") : p.replace(/\/+$/, ""));
4
5
  const foldDrive = (p) => (/^[A-Za-z]:(\/|$)/.test(p) ? p[0].toLowerCase() + p.slice(1) : p);
5
- const isWinFormPath = (p) => /^[A-Za-z]:[\\/]/.test(p) || p.startsWith("\\\\") || (!p.startsWith("/") && p.includes("\\"));
6
6
  function relUnder(root, child) {
7
7
  const fold = isWinFormPath(root) ? normSlash : (p) => p;
8
8
  const r = foldDrive(fold(stripTrailSep(root)));
@@ -18,7 +18,7 @@ function joinUnder(root, rel) {
18
18
  return stripTrailSep(root) + (win ? "\\" : "/") + (win ? rel.replace(/\//g, "\\") : rel);
19
19
  }
20
20
  function isUnsafeRelPath(relN) {
21
- return relN.startsWith("/") || /^[A-Za-z]:/.test(relN) || relN.split("/").includes("..");
21
+ return isAbsolutePathForm(relN) || /^[A-Za-z]:/.test(relN) || relN.split("/").includes("..");
22
22
  }
23
23
  export const DEFAULT_SNAPSHOT_BOUNDS = {
24
24
  maxFiles: 10000,
@@ -256,3 +256,4 @@ export class InMemoryFileSnapshotStore {
256
256
  return removed;
257
257
  }
258
258
  }
259
+ export const __testables = { joinUnder, relUnder, stripTrailSep };
@@ -1,15 +1,14 @@
1
1
  import { canonicalizeTarget, writeTargetPath } from "../tools/fs/safety.js";
2
- import { canonicalToolName } from "./tool-name-aliases.js";
3
2
  import { PATH_WRITE_TOOLS, isWithin } from "./runner/session-rule-policy.js";
4
3
  const ask = (message) => ({ action: "ask", message, decisionReason: "rule" });
5
4
  export function createFsWriteGatePolicy(opts) {
6
5
  const { env, rootPath, defaultWrite } = opts;
7
- const gated = new Set([...PATH_WRITE_TOOLS, "NotebookEdit"].map(canonicalToolName));
6
+ const gated = new Set([...PATH_WRITE_TOOLS, "NotebookEdit"]);
8
7
  const acceptDirs = opts.acceptDirs && opts.acceptDirs.length > 0 ? opts.acceptDirs : undefined;
9
8
  const exemptDirs = opts.exemptDirs && opts.exemptDirs.length > 0 ? opts.exemptDirs : undefined;
10
9
  return {
11
10
  async check(req, signal) {
12
- const canonical = canonicalToolName(req.toolName);
11
+ const canonical = req.toolName;
13
12
  if (!gated.has(canonical))
14
13
  return { action: "allow" };
15
14
  const path = writeTargetPath(canonical, req.args);
@@ -7,7 +7,9 @@ export interface Hooks {
7
7
  userPromptSubmit?(prompt: string): UserPromptSubmitResult | undefined | Promise<UserPromptSubmitResult | undefined>;
8
8
  stop?(ctx: StopHookContext): StopHookResult | undefined | Promise<StopHookResult | undefined>;
9
9
  postToolUseFailure?(toolName: string, input: unknown, failure: HookToolFailure, ctx: HookToolContext): PostToolUseFailureResult | undefined | Promise<PostToolUseFailureResult | undefined>;
10
- postToolBatch?(batch: PostToolBatchCall[]): PostToolBatchResult | undefined | Promise<PostToolBatchResult | undefined>;
10
+ postToolBatch?(batch: PostToolBatchCall[], meta?: {
11
+ injectedThisTurn: "final_verification" | "finalize";
12
+ }): PostToolBatchResult | undefined | Promise<PostToolBatchResult | undefined>;
11
13
  preCompact?(ctx: PreCompactContext): PreCompactResult | undefined | Promise<PreCompactResult | undefined>;
12
14
  postCompact?(ctx: PostCompactContext): void | Promise<void>;
13
15
  stopFailure?(ctx: StopFailureContext): void | Promise<void>;
@@ -102,6 +104,7 @@ export interface ToolGateResult {
102
104
  preToolContext: string[];
103
105
  }
104
106
  export interface ToolGateInput {
107
+ onNotifyError?: (failure: import("./safe-notify.js").SafeNotifyFailure) => void;
105
108
  event: {
106
109
  toolCallId: string;
107
110
  toolName: string;
@@ -1,6 +1,7 @@
1
1
  import { decisionText } from "./tool-policy.js";
2
2
  import { inlineUntrusted } from "./untrusted-text.js";
3
3
  import { ASK_USER_QUESTION_TOOL_NAME } from "./ask-question.js";
4
+ import { createSafeNotifier } from "./safe-notify.js";
4
5
  export function cloneObserverInput(input) {
5
6
  try {
6
7
  return structuredClone(input);
@@ -23,12 +24,8 @@ function preToolUseCrashReason(subject, err) {
23
24
  `This is a failure of the deployment's hook, NOT of the tool itself — an identical retry reaches the ` +
24
25
  `same hook and fails the same way. Hook error: ${cause || "(no message)"}`);
25
26
  }
26
- function traceHookCrash(input, err) {
27
- try {
28
- input.onHookError?.(err);
29
- }
30
- catch {
31
- }
27
+ function traceHookCrash(input, err, notifier) {
28
+ notifier.notify(() => input.onHookError?.(err), "toolGate.onHookError");
32
29
  }
33
30
  function withProbeTimeout(p, ms, signal) {
34
31
  if (ms === undefined && signal === undefined)
@@ -62,6 +59,7 @@ export async function runToolGate(input) {
62
59
  let currentInput = event.input;
63
60
  const preToolContext = [];
64
61
  let hookAsk;
62
+ const notifier = createSafeNotifier(input.onNotifyError !== undefined ? { onError: input.onNotifyError } : undefined);
65
63
  if (preToolUse) {
66
64
  let r;
67
65
  try {
@@ -69,13 +67,9 @@ export async function runToolGate(input) {
69
67
  }
70
68
  catch (err) {
71
69
  const reason = preToolUseCrashReason(`this call to "${toolName}"`, err);
72
- traceHookCrash(input, err);
70
+ traceHookCrash(input, err, notifier);
73
71
  if (input.permissionDenied) {
74
- try {
75
- await input.permissionDenied({ toolName, input: cloneObserverInput(currentInput), toolCallId, reason, source: "hook" });
76
- }
77
- catch {
78
- }
72
+ await notifier.notifyAsync(() => input.permissionDenied?.({ toolName, input: cloneObserverInput(currentInput), toolCallId, reason, source: "hook" }), "toolGate.permissionDenied");
79
73
  }
80
74
  return { block: true, reason: formatHookFeedback(reason), preToolContext };
81
75
  }
@@ -216,7 +210,7 @@ export async function runToolGate(input) {
216
210
  hr = await preToolUse(toolName, editArgs, { toolCallId, toolName });
217
211
  }
218
212
  catch (err) {
219
- traceHookCrash(input, err);
213
+ traceHookCrash(input, err, notifier);
220
214
  editDenied = { action: "deny", reason: preToolUseCrashReason(`the approved edit for "${toolName}"`, err) };
221
215
  denySource = "hook";
222
216
  break;
@@ -288,11 +282,7 @@ export async function runToolGate(input) {
288
282
  currentInput = decision.updatedInput;
289
283
  }
290
284
  if (input.permissionDenied) {
291
- try {
292
- await input.permissionDenied({ toolName, input: cloneObserverInput(currentInput), toolCallId, reason: denyReason, source: denySource });
293
- }
294
- catch {
295
- }
285
+ await notifier.notifyAsync(() => input.permissionDenied?.({ toolName, input: cloneObserverInput(currentInput), toolCallId, reason: denyReason, source: denySource }), "toolGate.permissionDenied");
296
286
  }
297
287
  return {
298
288
  block: true,
package/dist/core/lsp.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { uriToPath } from "./lsp-protocol.js";
2
+ import { foldRedundantPathSegments, isAbsolutePathForm } from "../tools/fs/safety.js";
2
3
  import { Type } from "typebox";
3
4
  import { defineTool, errorResult } from "./tools.js";
4
5
  export const LSP_OPERATIONS = [
@@ -67,10 +68,10 @@ export function settleOnAbort(job, signal, onAbort) {
67
68
  });
68
69
  }
69
70
  export function resolveLspPath(filePath, cwd) {
70
- const isAbsForm = filePath.startsWith("/") || /^[A-Za-z]:[\\/]/.test(filePath) || filePath.startsWith("\\\\");
71
+ const isAbsForm = isAbsolutePathForm(filePath);
71
72
  if (isAbsForm || !cwd)
72
- return filePath;
73
- return cwd.replace(/[\\/]+$/, "") + (cwd.includes("\\") ? "\\" : "/") + filePath;
73
+ return foldRedundantPathSegments(filePath);
74
+ return foldRedundantPathSegments(cwd.replace(/[\\/]+$/, "") + (cwd.includes("\\") ? "\\" : "/") + filePath);
74
75
  }
75
76
  export const MAX_LSP_FILE_BYTES = 10 * 1000 * 1000;
76
77
  function fmt(result, ctx) {
@@ -8,12 +8,13 @@ export interface MailboxLease {
8
8
  messages: MailboxMessage[];
9
9
  maxSeq: number;
10
10
  }
11
+ export interface MailboxAppendMessage {
12
+ from?: string;
13
+ content: string;
14
+ sentAt: number;
15
+ }
11
16
  export interface MailboxStore {
12
- append(scope: string, handle: string, msg: {
13
- from?: string;
14
- content: string;
15
- sentAt: number;
16
- }): Promise<number>;
17
+ append(scope: string, handle: string, msg: MailboxAppendMessage): Promise<number>;
17
18
  claimLease(scope: string, handle: string, owner: string, ttlMs: number, now?: number): Promise<MailboxLease | null>;
18
19
  ack(scope: string, handle: string, owner: string, upToSeq: number): Promise<void>;
19
20
  releaseLease(scope: string, handle: string, owner: string): Promise<void>;
@@ -28,11 +29,7 @@ export declare class InMemoryMailboxStore implements MailboxStore {
28
29
  private boxes;
29
30
  private key;
30
31
  private box;
31
- append(scope: string, handle: string, msg: {
32
- from?: string;
33
- content: string;
34
- sentAt: number;
35
- }): Promise<number>;
32
+ append(scope: string, handle: string, msg: MailboxAppendMessage): Promise<number>;
36
33
  claimLease(scope: string, handle: string, owner: string, ttlMs: number, now?: number): Promise<MailboxLease | null>;
37
34
  ack(scope: string, handle: string, owner: string, upToSeq: number): Promise<void>;
38
35
  releaseLease(scope: string, handle: string, owner: string): Promise<void>;
@@ -8,6 +8,11 @@ export interface McpToolAxis {
8
8
  egress?: true;
9
9
  effect?: ToolEffect;
10
10
  }
11
+ export interface McpDroppedTool {
12
+ server: string;
13
+ tool: string;
14
+ reason: string;
15
+ }
11
16
  export interface MaterializedMcp {
12
17
  tools: AgentTool[];
13
18
  toolAxes: McpToolAxis[];
@@ -23,11 +28,7 @@ export interface MaterializedMcp {
23
28
  pendingRemovals: string[];
24
29
  };
25
30
  warnings: Error[];
26
- droppedTools: Array<{
27
- server: string;
28
- tool: string;
29
- reason: string;
30
- }>;
31
+ droppedTools: McpDroppedTool[];
31
32
  statuses: McpServerStatus[];
32
33
  refresh: (server?: string) => Promise<McpRefreshResult[]>;
33
34
  dispose: () => Promise<void>;
@@ -41,11 +42,7 @@ export interface McpRefreshResult {
41
42
  removed: string[];
42
43
  tools?: AgentTool[];
43
44
  axes?: McpToolAxis[];
44
- dropped?: Array<{
45
- server: string;
46
- tool: string;
47
- reason: string;
48
- }>;
45
+ dropped?: McpDroppedTool[];
49
46
  error?: string;
50
47
  }
51
48
  export interface McpServerStatus {
package/dist/core/mcp.js CHANGED
@@ -7,6 +7,7 @@ import { tmpdir } from "node:os";
7
7
  import { join } from "node:path";
8
8
  import { CallToolResultSchema, ElicitRequestSchema, ErrorCode, ListResourcesResultSchema, McpError } from "@modelcontextprotocol/sdk/types.js";
9
9
  import { MCP_IMAGE_MAX_BASE64, sharpImageResizer } from "./image-downsample.js";
10
+ import { sliceHeadSafe, sliceTailSafe } from "./surrogate-safe-slice.js";
10
11
  import { truncateError } from "./tool-errors.js";
11
12
  import { delimitUntrusted, inlineUntrusted, sanitizeUntrustedText } from "./untrusted-text.js";
12
13
  import { validateJsonSchemaShape } from "./runner/strict-output-schema.js";
@@ -75,8 +76,10 @@ export function truncateMcpErrorText(s) {
75
76
  let head = MCP_ERROR_HEAD_CHARS;
76
77
  let out = "";
77
78
  for (let i = 0; i < 4; i++) {
78
- const removed = s.length - head - MCP_ERROR_TAIL_CHARS;
79
- out = `${s.slice(0, head)}\n\n... [${removed} characters truncated] ...\n\n${s.slice(s.length - MCP_ERROR_TAIL_CHARS)}`;
79
+ const headText = sliceHeadSafe(s, head);
80
+ const tailText = sliceTailSafe(s, MCP_ERROR_TAIL_CHARS);
81
+ const removed = s.length - headText.length - tailText.length;
82
+ out = `${headText}\n\n... [${removed} characters truncated] ...\n\n${tailText}`;
80
83
  if (out.length <= max)
81
84
  break;
82
85
  head = Math.max(0, head - (out.length - max));
@@ -753,9 +756,6 @@ function applyCallerAxisOverride(name, hint, override) {
753
756
  const LIST_MCP_RESOURCES = "ListMcpResourcesTool";
754
757
  const READ_MCP_RESOURCE = "ReadMcpResourceTool";
755
758
  const READ_MCP_RESOURCE_DIR = "ReadMcpResourceDirTool";
756
- const LEGACY_LIST_MCP_RESOURCES = "ListMcpResources";
757
- const LEGACY_READ_MCP_RESOURCE = "ReadMcpResource";
758
- const LEGACY_READ_MCP_RESOURCE_DIR = "ReadMcpResourceDir";
759
759
  const MCP_SKILLS_EXTENSION = "io.modelcontextprotocol/skills";
760
760
  const MAX_DIR_READ_PAGES = 20;
761
761
  const DIR_READ_NOT_A_DIRECTORY_RE = /not a directory|isn'?t a directory|not a folder/i;
@@ -840,7 +840,6 @@ function buildResourceTools(resourceServers) {
840
840
  if (listable.length > 0) {
841
841
  tools.push({
842
842
  name: LIST_MCP_RESOURCES,
843
- aliases: [LEGACY_LIST_MCP_RESOURCES],
844
843
  description: "List the resources available from connected MCP servers (uri / name / description). Pass `server` to limit " +
845
844
  "to one server, or omit it to list across all. Resource metadata is external/untrusted data.",
846
845
  label: LIST_MCP_RESOURCES,
@@ -900,7 +899,6 @@ function buildResourceTools(resourceServers) {
900
899
  if (readable.length > 0) {
901
900
  tools.push({
902
901
  name: READ_MCP_RESOURCE,
903
- aliases: [LEGACY_READ_MCP_RESOURCE],
904
902
  description: "Read a specific MCP resource by `server` + `uri` (from ListMcpResourcesTool). Its content is external/untrusted data.",
905
903
  label: READ_MCP_RESOURCE,
906
904
  executionMode: "parallel",
@@ -950,7 +948,6 @@ function buildResourceTools(resourceServers) {
950
948
  if (listable.length > 0) {
951
949
  tools.push({
952
950
  name: READ_MCP_RESOURCE_DIR,
953
- aliases: [LEGACY_READ_MCP_RESOURCE_DIR],
954
951
  description: "List the MCP resources UNDER a directory `uri` on `server` — its child resources (those whose uri is " +
955
952
  "nested below it; subdirectories carry mimeType \"inode/directory\"). Use it to browse a hierarchical " +
956
953
  "resource namespace after ListMcpResourcesTool. Resource metadata is external/untrusted data.",
@@ -1008,7 +1005,8 @@ function buildResourceTools(resourceServers) {
1008
1005
  const res = await rs.client
1009
1006
  .listResources(undefined, { signal: watchdog.combinedSignal, timeout: timeoutMs })
1010
1007
  .catch((err) => rethrowHonestMcpError(err, { server, what, timeoutMs, writeEffect: false, signal, attributeServer: true, idle: { signal: watchdog.idleSignal, idleMs } }));
1011
- const children = res.resources.filter((r) => typeof r.uri === "string" && r.uri !== uri && r.uri.startsWith(uri));
1008
+ const dirPrefix = uri.endsWith("/") ? uri : `${uri}/`;
1009
+ const children = res.resources.filter((r) => typeof r.uri === "string" && r.uri !== uri && r.uri.startsWith(dirPrefix));
1012
1010
  return renderDirChildren(server, uri, children);
1013
1011
  }
1014
1012
  finally {
@@ -60,7 +60,6 @@ export declare class MemoryEngine {
60
60
  private rebuildIndex;
61
61
  private committedContentFor;
62
62
  private gateDerivedIndex;
63
- private quarantineFile;
64
63
  private readonlyDirNamesUnderRoot;
65
64
  private writeIfChanged;
66
65
  private writeBackProjection;
@@ -1,12 +1,12 @@
1
1
  import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
2
- import { basename, dirname, join, relative, sep } from "node:path";
2
+ import { dirname, join, relative, sep } from "node:path";
3
3
  import { uuidv7 } from "../../internal/harness.js";
4
4
  import { MAX_MEMORY_BYTES, composeMemoryBlock, firstSentence } from "../memory.js";
5
5
  import { inlineUntrusted } from "../untrusted-text.js";
6
6
  import { formatMemoryAge } from "../memory-recall.js";
7
7
  import { computeEntryRev, parseEntryFile, serializeEntryFile } from "./frontmatter.js";
8
8
  import { DEFAULT_MAX_ENTRY_DEPTH, MEMORY_INDEX_FILENAME, scanEntryFiles } from "./file-backend.js";
9
- import { SCAN_FUSE_THRESHOLD, readIndexRevs, writeIndexRevs, bumpScanFuse, canonicalize, claimRootScope, clearScanFuse, deriveControlPlaneDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, ensureDirExists, isContainedIn, registerScope, registeredScopes, resolveMemoryEngineRoot, scopeDirFor, } from "./layout.js";
9
+ import { QUARANTINE_DIR, SCAN_FUSE_THRESHOLD, quarantineAndTombstone, readIndexRevs, writeIndexRevs, bumpScanFuse, canonicalize, claimRootScope, clearScanFuse, deriveControlPlaneDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, ensureDirExists, isContainedIn, registerScope, registeredScopes, resolveMemoryEngineRoot, scopeDirFor, } from "./layout.js";
10
10
  import { scanMemoryFileName, scanMemoryWrite, scanRemediation } from "./scan.js";
11
11
  export const MEMORY_INSTRUCTION_TEMPLATE = `# Memory
12
12
 
@@ -290,6 +290,8 @@ export class MemoryEngine {
290
290
  report.rejections.push({ path: rel, code: "symlink", reason: "symlinks are not harvested into memory (containment gate)" });
291
291
  else if (kind === "depth")
292
292
  report.rejections.push({ path: rel, code: "nested_too_deep", reason: `memory files nested deeper than ${this.maxDepth} levels are not harvested` });
293
+ else if (kind === "unreadable")
294
+ report.rejections.push({ path: rel, code: "unreadable", reason: "the path could not be read (lstat/readdir failed) — its content was NOT harvested this pass" });
293
295
  },
294
296
  });
295
297
  const records = [];
@@ -414,7 +416,7 @@ export class MemoryEngine {
414
416
  scanFindings.push(...scanMemoryWrite(f.text));
415
417
  const finding = scanFindings[0];
416
418
  if (finding !== undefined) {
417
- const q = this.quarantineFile(f.canonical, f.text);
419
+ const q = quarantineAndTombstone(f.canonical, f.text, join(this.controlDir, QUARANTINE_DIR), this.now);
418
420
  let contained = q.removed;
419
421
  const priorId = idByBasePath.get(f.canonical);
420
422
  if (priorId !== undefined) {
@@ -704,8 +706,8 @@ export class MemoryEngine {
704
706
  return undefined;
705
707
  let detail = "";
706
708
  try {
707
- const dest = join(this.controlDir, "quarantine", `${this.now()}-${MEMORY_INDEX_FILENAME}`);
708
- mkdirSync(dirname(dest), { recursive: true });
709
+ const dest = join(this.controlDir, QUARANTINE_DIR, `${this.now()}-${MEMORY_INDEX_FILENAME}`);
710
+ ensureDirExists(dirname(dest));
709
711
  writeFileSync(dest, text, "utf8");
710
712
  }
711
713
  catch (err) {
@@ -725,35 +727,6 @@ export class MemoryEngine {
725
727
  reason: `memory index blocked: ${finding.reason} (${cleared ? "index cleared and rebuilt from entry frontmatter; the offending prose was captured to quarantine" : "index still carries the offending content"})${detail}`,
726
728
  };
727
729
  }
728
- quarantineFile(path, content) {
729
- const dest = join(this.controlDir, "quarantine", `${this.now()}-${basename(path)}`);
730
- let captured;
731
- let detail;
732
- try {
733
- mkdirSync(dirname(dest), { recursive: true });
734
- writeFileSync(dest, content, "utf8");
735
- captured = dest;
736
- }
737
- catch (err) {
738
- detail = `quarantine copy failed: ${err instanceof Error ? err.message : String(err)}`;
739
- }
740
- let removed = false;
741
- try {
742
- rmSync(path, { force: true });
743
- removed = true;
744
- }
745
- catch (rmErr) {
746
- try {
747
- writeFileSync(path, "---\ndeleted: true\n---\n", "utf8");
748
- removed = true;
749
- detail = `${detail !== undefined ? `${detail}; ` : ""}delete failed (${rmErr instanceof Error ? rmErr.message : String(rmErr)}) — tombstoned in place`;
750
- }
751
- catch (clearErr) {
752
- detail = `${detail !== undefined ? `${detail}; ` : ""}suspect file could not be deleted or cleared: ${clearErr instanceof Error ? clearErr.message : String(clearErr)}`;
753
- }
754
- }
755
- return { ...(captured !== undefined ? { dest: captured } : {}), removed, ...(detail !== undefined ? { detail } : {}) };
756
- }
757
730
  readonlyDirNamesUnderRoot(handle) {
758
731
  const names = new Set();
759
732
  for (const [scope, dirName] of Object.entries(registeredScopes(this.controlDir))) {
@@ -797,12 +770,14 @@ export class MemoryEngine {
797
770
  chmodScopeTree(dir, dirMode, fileMode, opts = {}) {
798
771
  if (!existsSync(dir))
799
772
  return;
773
+ const sweepSkipped = [];
800
774
  const walk = (d, isRoot) => {
801
775
  let names = [];
802
776
  try {
803
777
  names = readdirSync(d);
804
778
  }
805
779
  catch {
780
+ sweepSkipped.push(d);
806
781
  return;
807
782
  }
808
783
  for (const name of names) {
@@ -814,6 +789,7 @@ export class MemoryEngine {
814
789
  st = statSync(p);
815
790
  }
816
791
  catch {
792
+ sweepSkipped.push(p);
817
793
  continue;
818
794
  }
819
795
  if (st.isDirectory())
@@ -825,6 +801,20 @@ export class MemoryEngine {
825
801
  chmodSafe(d, dirMode);
826
802
  };
827
803
  walk(dir, true);
804
+ if (sweepSkipped.length > 0) {
805
+ try {
806
+ enqueueMemoryAnnouncement(this.controlDir, {
807
+ kind: "gate",
808
+ at: this.now(),
809
+ items: sweepSkipped
810
+ .slice(0, 12)
811
+ .map((p) => `mode sweep could not read ${JSON.stringify(inlineUntrusted(relative(dir, p) || "."))} — files under it keep their previous mode`)
812
+ .concat(sweepSkipped.length > 12 ? [`…and ${sweepSkipped.length - 12} more sweepSkipped path(s)`] : []),
813
+ });
814
+ }
815
+ catch {
816
+ }
817
+ }
828
818
  }
829
819
  }
830
820
  const MAX_ANNOUNCEMENT_ITEMS = 12;
@@ -9,7 +9,7 @@ export interface ScannedEntryFile {
9
9
  export declare function scanEntryFiles(dir: string, opts?: {
10
10
  maxDepth?: number;
11
11
  exclude?: ReadonlySet<string>;
12
- onSkip?: (path: string, kind: "symlink" | "depth" | "nonmd" | "dotfile") => void;
12
+ onSkip?: (path: string, kind: "symlink" | "depth" | "nonmd" | "dotfile" | "unreadable") => void;
13
13
  }): ScannedEntryFile[];
14
14
  export declare class FileMemoryEngineBackend implements MemoryBackend {
15
15
  readonly directoryRoot: string;
@@ -26,7 +26,6 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
26
26
  drainInboundFindings(): HarvestRejection[];
27
27
  readCommittedShadow(id: string): string | undefined;
28
28
  private shadowPath;
29
- private quarantineFile;
30
29
  private loadLedger;
31
30
  private saveLedger;
32
31
  private recoverJournal;