@sema-agent/core 2.13.0 → 4.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 (184) hide show
  1. package/CHANGELOG.md +2345 -0
  2. package/dist/agents/cascade.d.ts +6 -1
  3. package/dist/agents/cascade.js +76 -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 +110 -3
  10. package/dist/agents/retain-ledger.d.ts +20 -2
  11. package/dist/agents/retain-ledger.js +38 -8
  12. package/dist/agents/roster-store.d.ts +4 -0
  13. package/dist/agents/roster-store.js +20 -1
  14. package/dist/agents/subagent.js +66 -64
  15. package/dist/agents/teacher.d.ts +1 -9
  16. package/dist/agents/teacher.js +23 -24
  17. package/dist/agents/team.js +7 -4
  18. package/dist/agents/verify.d.ts +15 -2
  19. package/dist/agents/verify.js +39 -3
  20. package/dist/bin/sema-tb.js +2 -2
  21. package/dist/brain/anthropic.js +24 -21
  22. package/dist/brain/circuit-breaker.d.ts +1 -0
  23. package/dist/brain/circuit-breaker.js +26 -9
  24. package/dist/brain/degrading.js +13 -1
  25. package/dist/brain/failover.js +2 -0
  26. package/dist/brain/openai.js +24 -21
  27. package/dist/brain/repetition.js +10 -1
  28. package/dist/brain/routing.js +5 -1
  29. package/dist/brain/status-sink.js +5 -1
  30. package/dist/brain/stream-engine.js +5 -2
  31. package/dist/brain/stream-shared.d.ts +1 -0
  32. package/dist/brain/stream-shared.js +14 -0
  33. package/dist/brain/terminal-cause.d.ts +4 -0
  34. package/dist/brain/terminal-cause.js +12 -0
  35. package/dist/brain/tool-call-id.d.ts +1 -0
  36. package/dist/brain/tool-call-id.js +3 -0
  37. package/dist/brain/tool-call-repair.js +34 -15
  38. package/dist/core/auto-compaction.js +33 -32
  39. package/dist/core/background-agent-store.d.ts +22 -22
  40. package/dist/core/background-agent-store.js +19 -23
  41. package/dist/core/checkpoint-store.d.ts +7 -13
  42. package/dist/core/checkpoint-store.js +39 -15
  43. package/dist/core/consolidate-scope.js +4 -3
  44. package/dist/core/context-edit.js +3 -0
  45. package/dist/core/context-guard.js +3 -0
  46. package/dist/core/file-snapshot-store.d.ts +8 -0
  47. package/dist/core/file-snapshot-store.js +4 -3
  48. package/dist/core/git-worktree-env.d.ts +6 -1
  49. package/dist/core/git-worktree-env.js +12 -1
  50. package/dist/core/hooks.d.ts +4 -1
  51. package/dist/core/hooks.js +35 -7
  52. package/dist/core/lsp-diagnostics.js +13 -7
  53. package/dist/core/lsp-protocol.d.ts +1 -1
  54. package/dist/core/lsp-protocol.js +20 -8
  55. package/dist/core/lsp-session.d.ts +12 -2
  56. package/dist/core/lsp-session.js +114 -49
  57. package/dist/core/lsp.d.ts +13 -1
  58. package/dist/core/lsp.js +65 -9
  59. package/dist/core/mailbox-store.d.ts +7 -10
  60. package/dist/core/mcp.d.ts +7 -10
  61. package/dist/core/mcp.js +7 -3
  62. package/dist/core/memory-engine/data-plane.js +4 -0
  63. package/dist/core/memory-engine/engine.d.ts +2 -2
  64. package/dist/core/memory-engine/engine.js +96 -31
  65. package/dist/core/memory-engine/file-backend.d.ts +2 -2
  66. package/dist/core/memory-engine/file-backend.js +38 -39
  67. package/dist/core/memory-engine/layout.d.ts +8 -0
  68. package/dist/core/memory-engine/layout.js +71 -4
  69. package/dist/core/memory-engine/scope-contract.js +9 -3
  70. package/dist/core/memory-engine/types.d.ts +1 -1
  71. package/dist/core/memory.js +3 -0
  72. package/dist/core/permission-rules.d.ts +2 -1
  73. package/dist/core/permission-rules.js +24 -3
  74. package/dist/core/runner/active-skill-scope.d.ts +0 -1
  75. package/dist/core/runner/active-skill-scope.js +1 -15
  76. package/dist/core/runner/assemble-result.d.ts +3 -8
  77. package/dist/core/runner/assemble-result.js +3 -5
  78. package/dist/core/runner/compaction-call-options.d.ts +7 -0
  79. package/dist/core/runner/compaction-call-options.js +30 -0
  80. package/dist/core/runner/prepare-task.d.ts +6 -23
  81. package/dist/core/runner/prepare-task.js +31 -53
  82. package/dist/core/runner/runtask.d.ts +1 -0
  83. package/dist/core/runner/runtask.js +86 -178
  84. package/dist/core/runner/session-rule-policy.js +4 -1
  85. package/dist/core/runner/teardown-bounded.d.ts +7 -0
  86. package/dist/core/runner/teardown-bounded.js +36 -0
  87. package/dist/core/runner/tool-disclosure.d.ts +2 -5
  88. package/dist/core/runner/turn-attachments.d.ts +5 -15
  89. package/dist/core/runner/turn-attachments.js +1 -1
  90. package/dist/core/runner/usage-accounting.d.ts +6 -0
  91. package/dist/core/runner/usage-accounting.js +9 -8
  92. package/dist/core/safe-notify.d.ts +17 -0
  93. package/dist/core/safe-notify.js +59 -0
  94. package/dist/core/secret-env.js +5 -1
  95. package/dist/core/sensitive-path-policy.js +11 -4
  96. package/dist/core/store-contracts/checkpoint-store-contract.js +1 -1
  97. package/dist/core/store-contracts/session-repo-contract.js +40 -0
  98. package/dist/core/stub-env.d.ts +2 -6
  99. package/dist/core/surrogate-safe-slice.d.ts +4 -0
  100. package/dist/core/surrogate-safe-slice.js +18 -0
  101. package/dist/core/task-registry-monitor.js +15 -14
  102. package/dist/core/task-registry-shared.d.ts +22 -0
  103. package/dist/core/task-registry-shared.js +16 -0
  104. package/dist/core/task-registry.d.ts +1 -0
  105. package/dist/core/task-registry.js +40 -57
  106. package/dist/core/tool-errors.js +5 -4
  107. package/dist/core/tool-name-aliases.js +2 -1
  108. package/dist/core/tool-policy.js +72 -19
  109. package/dist/core/tool-result-store.d.ts +1 -0
  110. package/dist/core/tool-result-store.js +17 -1
  111. package/dist/core/trace.d.ts +3 -9
  112. package/dist/core/types.d.ts +16 -12
  113. package/dist/core/untrusted-egress.js +7 -5
  114. package/dist/core/workflow-journal-store.d.ts +9 -20
  115. package/dist/core/workflow-run-store.d.ts +11 -10
  116. package/dist/core/workflow-run-store.js +22 -16
  117. package/dist/engine/compaction/compaction.d.ts +1 -0
  118. package/dist/engine/compaction/compaction.js +14 -5
  119. package/dist/engine/compaction/utils.js +4 -2
  120. package/dist/engine/execution-env/node-execution-env.d.ts +2 -24
  121. package/dist/engine/harness/agent-harness.d.ts +1 -0
  122. package/dist/engine/harness/agent-harness.js +13 -33
  123. package/dist/engine/harness/types.d.ts +29 -67
  124. package/dist/engine/llm/types.d.ts +1 -1
  125. package/dist/engine/loop/agent-loop.js +11 -4
  126. package/dist/engine/loop/types.d.ts +11 -9
  127. package/dist/engine/lsp/frame-decoder.js +6 -3
  128. package/dist/engine/lsp/node-lsp-manager.d.ts +2 -0
  129. package/dist/engine/lsp/node-lsp-manager.js +103 -26
  130. package/dist/engine/lsp/stdio-lsp-transport.js +16 -7
  131. package/dist/engine/session/memory-repo.d.ts +2 -6
  132. package/dist/engine/session/session.d.ts +5 -30
  133. package/dist/index.d.ts +3 -1
  134. package/dist/index.js +2 -1
  135. package/dist/internal/harness-types.d.ts +3 -1
  136. package/dist/internal/harness.d.ts +1 -1
  137. package/dist/internal/harness.js +1 -1
  138. package/dist/orchestration/run-spec.js +7 -4
  139. package/dist/orchestration/run-workflow-tool.js +3 -35
  140. package/dist/orchestration/workflow-governance.js +43 -9
  141. package/dist/orchestration/workflow-sandbox-conformance.js +85 -51
  142. package/dist/orchestration/workflow-script-store.js +34 -4
  143. package/dist/orchestration/workflow.d.ts +3 -11
  144. package/dist/orchestration/workflow.js +164 -204
  145. package/dist/prompt-assembly/artifact-store.d.ts +2 -0
  146. package/dist/prompt-assembly/artifact-store.js +39 -24
  147. package/dist/prompt-assembly/assemble.js +38 -7
  148. package/dist/prompt-assembly/epoch.js +19 -2
  149. package/dist/prompt-assembly/event-registry.js +3 -3
  150. package/dist/prompt-assembly/explain.js +1 -1
  151. package/dist/prompt-assembly/tool-catalog.d.ts +10 -9
  152. package/dist/prompt-assembly/tool-catalog.js +16 -1
  153. package/dist/prompt-assembly/turn-snapshot.d.ts +2 -5
  154. package/dist/prompt-assembly/turn-snapshot.js +4 -1
  155. package/dist/stores/file/background-agent-store.d.ts +3 -12
  156. package/dist/stores/file/background-agent-store.js +3 -41
  157. package/dist/stores/file/checkpoint-store.js +6 -6
  158. package/dist/stores/file/fs-atomic.d.ts +3 -0
  159. package/dist/stores/file/fs-atomic.js +53 -8
  160. package/dist/stores/file/index.d.ts +1 -0
  161. package/dist/stores/file/index.js +7 -0
  162. package/dist/stores/file/mailbox-store.d.ts +2 -6
  163. package/dist/stores/file/session-policy-store.js +15 -1
  164. package/dist/stores/file/session-store.d.ts +4 -6
  165. package/dist/stores/file/session-store.js +28 -1
  166. package/dist/stores/file/workflow-journal-store.d.ts +3 -11
  167. package/dist/stores/file/workflow-run-store.d.ts +3 -7
  168. package/dist/stores/file/workflow-run-store.js +4 -17
  169. package/dist/tools/fs/bash-readonly-classifier.d.ts +2 -0
  170. package/dist/tools/fs/bash-readonly-classifier.js +73 -21
  171. package/dist/tools/fs/fs-bash.d.ts +1 -1
  172. package/dist/tools/fs/fs-bash.js +407 -91
  173. package/dist/tools/fs/fs-pdf.d.ts +2 -12
  174. package/dist/tools/fs/fs-shared.js +2 -2
  175. package/dist/tools/fs/notebook.d.ts +2 -5
  176. package/dist/tools/fs/safety.d.ts +3 -0
  177. package/dist/tools/fs/safety.js +43 -14
  178. package/dist/tools/fs/search.js +5 -5
  179. package/dist/tools/loop-tick.js +1 -1
  180. package/dist/tools/monitor.d.ts +1 -0
  181. package/dist/tools/monitor.js +57 -9
  182. package/dist/tools/worktree.d.ts +4 -7
  183. package/dist/tools/worktree.js +235 -143
  184. package/package.json +9 -2
@@ -1,14 +1,15 @@
1
1
  import { Value } from "typebox/value";
2
2
  const MAX_ERROR_CHARS = 10_000;
3
3
  const HALF = MAX_ERROR_CHARS / 2;
4
+ import { isHighSurrogate, isLowSurrogate, sliceHeadSafe, sliceTailSafe } from "./surrogate-safe-slice.js";
4
5
  export function truncateError(s) {
5
6
  if (s.length <= MAX_ERROR_CHARS)
6
7
  return s;
7
- const removed = s.length - MAX_ERROR_CHARS;
8
- return `${s.slice(0, HALF)}\n\n... [${removed} characters truncated] ...\n\n${s.slice(s.length - HALF)}`;
8
+ const head = sliceHeadSafe(s, HALF);
9
+ const tail = sliceTailSafe(s, HALF);
10
+ const removed = s.length - head.length - tail.length;
11
+ return `${head}\n\n... [${removed} characters truncated] ...\n\n${tail}`;
9
12
  }
10
- const isHighSurrogate = (c) => c >= 0xd800 && c <= 0xdbff;
11
- const isLowSurrogate = (c) => c >= 0xdc00 && c <= 0xdfff;
12
13
  export function clipWithFilePointer(s, limit, fullOutputPath) {
13
14
  if (s.length <= limit)
14
15
  return s;
@@ -52,7 +52,8 @@ export const TOMBSTONED_TOOLS = {
52
52
  const MCP_PREFIX = "mcp__";
53
53
  const MCP_NAME_SEP = "__";
54
54
  export function canonicalToolName(name) {
55
- const renamed = OLD_TO_NEW[name] ?? name;
55
+ const mapped = Object.hasOwn(OLD_TO_NEW, name) ? OLD_TO_NEW[name] : undefined;
56
+ const renamed = typeof mapped === "string" ? mapped : name;
56
57
  if (renamed.includes(MCP_NAME_SEP) && !renamed.startsWith(MCP_PREFIX))
57
58
  return MCP_PREFIX + renamed;
58
59
  return renamed;
@@ -1,13 +1,14 @@
1
1
  import { homedir } from "node:os";
2
- import { isAbsolute, join, sep } from "node:path";
2
+ import { isAbsolute, join, normalize as normalizePath, sep } from "node:path";
3
3
  import { BASH_READONLY_DEFAULT_ALLOW, parseLeadingCommandName } from "../tools/fs/index.js";
4
+ import { writeTargetPath } from "../tools/fs/safety.js";
4
5
  import { canonicalToolName } from "./tool-name-aliases.js";
5
6
  export function decisionText(d) {
6
7
  return d.message ?? d.reason;
7
8
  }
8
9
  const ALLOW = { action: "allow" };
9
10
  function withTimeout(p, ms, onTimeout) {
10
- if (!ms || ms <= 0) {
11
+ if (ms === undefined) {
11
12
  return p;
12
13
  }
13
14
  return new Promise((resolve, reject) => {
@@ -24,7 +25,6 @@ function withTimeout(p, ms, onTimeout) {
24
25
  return;
25
26
  settle(resolve, onTimeout());
26
27
  }, ms);
27
- timer.unref?.();
28
28
  p.then((v) => settle(resolve, v), (e) => settle(reject, e));
29
29
  });
30
30
  }
@@ -50,6 +50,9 @@ export function createAllowDenyPolicy(opts) {
50
50
  };
51
51
  }
52
52
  export function createApprovalPolicy(opts) {
53
+ if (opts.approvalTimeoutMs !== undefined && !Number.isFinite(opts.approvalTimeoutMs)) {
54
+ throw new Error(`createApprovalPolicy: approvalTimeoutMs must be a finite number of ms (got ${opts.approvalTimeoutMs}) — omit it to wait indefinitely`);
55
+ }
53
56
  const need = new Set(opts.requireApproval.map(canonicalToolName));
54
57
  const deny = new Set((opts.deny ?? []).map(canonicalToolName));
55
58
  const auto = new Set((opts.autoAllow ?? []).map(canonicalToolName));
@@ -106,7 +109,7 @@ export function combinePolicies(...policies) {
106
109
  current = { ...current, args: d.updatedInput };
107
110
  rewrite = d;
108
111
  }
109
- if (d.action === "ask" && !asked) {
112
+ if (d.action === "ask" && (asked === undefined || (d.requiresRealApproval === true && asked.requiresRealApproval !== true))) {
110
113
  asked = d;
111
114
  }
112
115
  }
@@ -119,10 +122,13 @@ export function combinePolicies(...policies) {
119
122
  };
120
123
  }
121
124
  export const COARSE_SHELL_TOOLS = ["Bash", "Monitor"];
125
+ function canonicalToolNameSet(tools) {
126
+ return new Set((tools ?? COARSE_SHELL_TOOLS).map(canonicalToolName));
127
+ }
122
128
  export function createCoarseCommandNamePolicy(opts) {
123
129
  const allow = opts.allow ? new Set(opts.allow) : undefined;
124
130
  const deny = new Set(opts.deny ?? []);
125
- const shellTools = new Set(opts.tools ?? COARSE_SHELL_TOOLS);
131
+ const shellTools = canonicalToolNameSet(opts.tools);
126
132
  const defaultAction = opts.defaultAction ?? "ask";
127
133
  const fallback = (reason) => defaultAction === "deny"
128
134
  ? { action: "deny", reason, decisionReason: "rule" }
@@ -154,6 +160,7 @@ const DELETE_HEADS = new Set(["rm", "xargs", "Remove-Item"]);
154
160
  const DELETE_WRAPPERS = new Set([
155
161
  "sudo", "command", "nohup", "time", "do", "then", "else",
156
162
  "env", "nice", "ionice", "exec", "doas", "setsid", "stdbuf", "chroot", "eval",
163
+ "sh", "bash", "zsh", "dash", "ksh", "ash", "timeout", "case",
157
164
  ]);
158
165
  const STATEMENT_SPLIT = /[;&|\n]+/;
159
166
  const stripTokenEdges = (t) => t.replace(/^[("'{]+|[)"'}]+$/g, "");
@@ -209,7 +216,7 @@ export function findUnverifiableRecursiveDelete(command, safeVars = new Set(UNVE
209
216
  }
210
217
  return false;
211
218
  };
212
- const statements = command.split(STATEMENT_SPLIT);
219
+ const statements = command.replace(/\\\r?\n/g, " ").split(STATEMENT_SPLIT);
213
220
  for (const stmt of statements) {
214
221
  const tokens = stmt.trim().split(/\s+/).filter(Boolean);
215
222
  let i = 0;
@@ -230,6 +237,10 @@ export function findUnverifiableRecursiveDelete(command, safeVars = new Set(UNVE
230
237
  let sawWrapper = false;
231
238
  while (i < tokens.length) {
232
239
  const t = stripTokenEdges(tokens[i]).replace(/^\\/, "");
240
+ if (t === "") {
241
+ i++;
242
+ continue;
243
+ }
233
244
  if (DELETE_WRAPPERS.has(t)) {
234
245
  sawWrapper = true;
235
246
  i++;
@@ -237,8 +248,9 @@ export function findUnverifiableRecursiveDelete(command, safeVars = new Set(UNVE
237
248
  i++;
238
249
  continue;
239
250
  }
240
- if (t === "timeout" && i + 1 < tokens.length) {
241
- i += 2;
251
+ if (/\)$/.test(tokens[i]) && !DELETE_HEADS.has(t)) {
252
+ sawWrapper = true;
253
+ i++;
242
254
  continue;
243
255
  }
244
256
  break;
@@ -368,7 +380,7 @@ function hasRecursiveForce(argvTail) {
368
380
  }
369
381
  export function createUnverifiableDeletePolicy(opts) {
370
382
  const safeVars = new Set([...UNVERIFIABLE_DELETE_SAFE_VARS, ...(opts?.safeVars ?? [])]);
371
- const shellTools = new Set(opts?.tools ?? COARSE_SHELL_TOOLS);
383
+ const shellTools = canonicalToolNameSet(opts?.tools);
372
384
  return {
373
385
  check(req) {
374
386
  if (!shellTools.has(canonicalToolName(req.toolName)))
@@ -390,10 +402,30 @@ export function createUnverifiableDeletePolicy(opts) {
390
402
  },
391
403
  };
392
404
  }
405
+ function pathSegments(p) {
406
+ return p.split(/[\\/]/).filter(Boolean);
407
+ }
408
+ const CASE_INSENSITIVE_FS = process.platform === "darwin" || process.platform === "win32";
409
+ const foldPathCase = (s) => (CASE_INSENSITIVE_FS ? s.toLowerCase() : s);
410
+ function lexicalPath(p, home) {
411
+ const norm = p.normalize("NFC");
412
+ const expanded = norm === "~" ? home : /^~[\\/]/.test(norm) ? join(home, norm.slice(2)) : norm;
413
+ return normalizePath(expanded);
414
+ }
415
+ const RELATIVE_TAIL_MIN_SEGMENTS = 2;
416
+ function relativeContinuesDirTail(segs, dirSegs) {
417
+ const from = Math.min(RELATIVE_TAIL_MIN_SEGMENTS, dirSegs.length);
418
+ for (let k = from; k <= Math.min(segs.length, dirSegs.length); k++) {
419
+ const tail = dirSegs.slice(dirSegs.length - k);
420
+ if (tail.every((s, i) => s === segs[i]))
421
+ return true;
422
+ }
423
+ return false;
424
+ }
393
425
  export function createTranscriptIntegrityPolicy(opts) {
394
426
  const home = homedir();
395
427
  const defaultRoot = (process.env.AGENT_DATA_DIR ?? join(home, ".ai-agent")).normalize("NFC");
396
- const dirs = (opts?.sessionsDirs ?? [join(defaultRoot, "sessions")]).map((d) => d.normalize("NFC").replace(/[\\/]+$/, ""));
428
+ const dirs = (opts?.sessionsDirs ?? [join(defaultRoot, "sessions")]).map((d) => lexicalPath(d, home).replace(/(?<=.)[\\/]+$/, ""));
397
429
  const textualForms = dirs.flatMap((d) => {
398
430
  const forms = [d];
399
431
  if (d.startsWith(home + sep)) {
@@ -403,13 +435,35 @@ export function createTranscriptIntegrityPolicy(opts) {
403
435
  return forms;
404
436
  });
405
437
  const readAllow = new Set(opts?.readAllow ?? BASH_READONLY_DEFAULT_ALLOW);
406
- const shellTools = new Set(opts?.tools ?? COARSE_SHELL_TOOLS);
438
+ const shellTools = canonicalToolNameSet(opts?.tools);
439
+ const dirSegs = dirs.map((d) => pathSegments(d).map(foldPathCase));
407
440
  const inProtectedDir = (p) => {
408
- const norm = p.normalize("NFC");
409
- const abs = norm.startsWith("~" + sep) || norm === "~" ? join(home, norm.slice(1)) : norm;
410
- if (!isAbsolute(abs))
411
- return false;
412
- return dirs.some((d) => abs === d || abs.startsWith(d + sep));
441
+ const abs = lexicalPath(p, home);
442
+ const segs = pathSegments(abs).map(foldPathCase);
443
+ if (isAbsolute(abs)) {
444
+ return dirSegs.some((d) => d.length <= segs.length && d.every((s, i) => s === segs[i]));
445
+ }
446
+ return dirSegs.some((d) => relativeContinuesDirTail(segs, d));
447
+ };
448
+ const referencesProtectedDir = (cmd) => {
449
+ const hay = foldPathCase(cmd);
450
+ if (textualForms.some((f) => hay.includes(foldPathCase(f))))
451
+ return true;
452
+ for (const raw of cmd.split(/[\s;&|<>()]+/)) {
453
+ if (raw === "")
454
+ continue;
455
+ const unquoted = raw.replace(/["'`]/g, "");
456
+ if (unquoted === "")
457
+ continue;
458
+ const expanded = unquoted.startsWith("${HOME}")
459
+ ? home + unquoted.slice("${HOME}".length)
460
+ : unquoted.startsWith("$HOME")
461
+ ? home + unquoted.slice("$HOME".length)
462
+ : unquoted;
463
+ if (inProtectedDir(expanded))
464
+ return true;
465
+ }
466
+ return false;
413
467
  };
414
468
  const askReason = (what) => ({
415
469
  action: "ask",
@@ -428,7 +482,7 @@ export function createTranscriptIntegrityPolicy(opts) {
428
482
  if (typeof command !== "string")
429
483
  return ALLOW;
430
484
  const cmd = command.normalize("NFC");
431
- if (!textualForms.some((f) => cmd.includes(f)))
485
+ if (!referencesProtectedDir(cmd))
432
486
  return ALLOW;
433
487
  const parsed = parseLeadingCommandName(command);
434
488
  if ("name" in parsed && readAllow.has(parsed.name))
@@ -438,8 +492,7 @@ export function createTranscriptIntegrityPolicy(opts) {
438
492
  : "the command references the transcript directory and is not a single read-only command (fail-closed)");
439
493
  }
440
494
  if (toolName === "Write" || toolName === "Edit" || toolName === "NotebookEdit") {
441
- const a = req.args;
442
- const p = a?.file_path ?? a?.notebook_path;
495
+ const p = writeTargetPath(toolName, req.args);
443
496
  if (typeof p === "string" && inProtectedDir(p)) {
444
497
  return askReason(`"${p}" is inside the session-transcript directory`);
445
498
  }
@@ -45,6 +45,7 @@ export declare class ScopedToolResultStore implements ToolResultStore {
45
45
  }
46
46
  export declare function isVolatileOffloadStore(store: ToolResultStore): boolean;
47
47
  export declare const OFFLOAD_TOOL_NAME = "ReadToolResult";
48
+ export declare function createOffloadPersist(store: ToolResultStore, sessionId: string): (toolCallId: string, fullText: string) => string;
48
49
  export declare function offloadPagebackHint(ref: string, form: "preview" | "cleared", reachableTools?: ReadonlySet<string>): string;
49
50
  export declare const PERSISTED_OUTPUT_PREFIX = "<persisted-output ref=";
50
51
  export declare const DEFAULT_TOOL_RESULT_THRESHOLD_CHARS = 20000;
@@ -29,6 +29,10 @@ export class InMemoryToolResultStore {
29
29
  totalChars = 0;
30
30
  constructor(opts) {
31
31
  this.opts = opts;
32
+ const cap = opts?.maxTotalChars;
33
+ if (cap !== undefined && Number.isNaN(cap)) {
34
+ throw new Error("InMemoryToolResultStore: maxTotalChars must be a number (got NaN) — omit it for an unbounded store");
35
+ }
32
36
  }
33
37
  put(ref, content) {
34
38
  assertSafeToolResultRef(ref);
@@ -37,7 +41,7 @@ export class InMemoryToolResultStore {
37
41
  this.map.set(ref, content);
38
42
  this.totalChars += content.length;
39
43
  const cap = this.opts?.maxTotalChars;
40
- if (cap !== undefined && cap > 0) {
44
+ if (cap !== undefined && Number.isFinite(cap)) {
41
45
  for (const [oldRef, oldContent] of this.map) {
42
46
  if (this.totalChars <= cap || oldRef === ref)
43
47
  break;
@@ -93,6 +97,16 @@ export function isVolatileOffloadStore(store) {
93
97
  return store instanceof InMemoryToolResultStore;
94
98
  }
95
99
  export const OFFLOAD_TOOL_NAME = "ReadToolResult";
100
+ export function createOffloadPersist(store, sessionId) {
101
+ return (toolCallId, fullText) => {
102
+ const ref = buildToolResultRef(sessionId, toolCallId);
103
+ void Promise.resolve(store.put(ref, fullText)).catch((err) => {
104
+ const cause = err instanceof Error ? err.message : String(err);
105
+ void Promise.resolve(store.put(ref, `[offload LOST at write time: ${cause} — the inline preview is all that survived]`)).catch(() => undefined);
106
+ });
107
+ return ref;
108
+ };
109
+ }
96
110
  export function offloadPagebackHint(ref, form, reachableTools) {
97
111
  const activate = reachableTools !== undefined && !reachableTools.has(OFFLOAD_TOOL_NAME)
98
112
  ? `${OFFLOAD_TOOL_NAME} is not active yet — call ${TOOL_SEARCH_NAME} with {"query":"select:${OFFLOAD_TOOL_NAME}"} to activate it, then `
@@ -144,6 +158,8 @@ export function buildPreview(full, ref, sizes, reachableTools) {
144
158
  `</persisted-output>`);
145
159
  }
146
160
  export function withToolResultOffload(tool, store, thresholdChars, sessionId, reachableTools) {
161
+ if (!(Number.isFinite(thresholdChars) && thresholdChars > 0))
162
+ return tool;
147
163
  const wrappedExecute = async (toolCallId, params, signal, onUpdate) => {
148
164
  const res = await tool.execute(toolCallId, params, signal, onUpdate);
149
165
  if (totalTextChars(res.content) <= thresholdChars)
@@ -1,5 +1,6 @@
1
1
  import type { TaskStatus, ToolEffect } from "./types.js";
2
2
  import type { ThinkingLevel } from "../internal/harness-types.js";
3
+ import type { ToolManifestRow } from "../prompt-assembly/tool-catalog.js";
3
4
  export interface TraceMechanismsSummary {
4
5
  finalizeInjected?: true;
5
6
  nudgesSent?: number;
@@ -42,15 +43,7 @@ export type TraceEvent = {
42
43
  hash: string;
43
44
  contentHash?: string;
44
45
  }>;
45
- tools?: Array<{
46
- wireName: string;
47
- aliases: string[];
48
- contractId: string;
49
- implementationRevision: string;
50
- cardId: string;
51
- shapeDigest: string;
52
- wireSchemaDigest: string;
53
- }>;
46
+ tools?: ToolManifestRow[];
54
47
  totalChars: number;
55
48
  snapshot?: {
56
49
  cacheIdentity: string;
@@ -140,6 +133,7 @@ export type TraceEvent = {
140
133
  provider?: string;
141
134
  turn?: number;
142
135
  promptTokens: number;
136
+ totalInputTokens: number;
143
137
  completionTokens: number;
144
138
  cacheRead: number;
145
139
  cacheWrite: number;
@@ -57,6 +57,19 @@ export type ToolReturn = string | {
57
57
  terminate?: boolean;
58
58
  isError?: boolean;
59
59
  };
60
+ export interface NestedUsage {
61
+ tokens: number;
62
+ turns: number;
63
+ tasks: number;
64
+ costMicroUsd?: number;
65
+ }
66
+ export interface NestedUsageAccum {
67
+ tokens: number;
68
+ turns: number;
69
+ tasks: number;
70
+ costMicroUsd: number;
71
+ anyUnpriced: boolean;
72
+ }
60
73
  export interface ToolExecuteContext {
61
74
  toolCallId: string;
62
75
  signal?: AbortSignal;
@@ -81,12 +94,7 @@ export interface ToolExecuteContext {
81
94
  release: () => Promise<void>;
82
95
  } | null>;
83
96
  parentCwd?: string;
84
- reportUsage?: (usage: {
85
- tokens: number;
86
- turns: number;
87
- tasks: number;
88
- costMicroUsd?: number;
89
- }) => void;
97
+ reportUsage?: (usage: NestedUsage) => void;
90
98
  model?: Model;
91
99
  thinkingLevel?: ThinkingLevel;
92
100
  principal?: string;
@@ -412,12 +420,7 @@ export interface TaskResult {
412
420
  outputTokens?: number;
413
421
  cacheWriteTokensLong?: number;
414
422
  costMicroUsd?: number;
415
- nested?: {
416
- tokens: number;
417
- turns: number;
418
- tasks: number;
419
- costMicroUsd?: number;
420
- };
423
+ nested?: NestedUsage;
421
424
  memory?: {
422
425
  tokens: number;
423
426
  costMicroUsd: number;
@@ -524,6 +527,7 @@ export type TaskEvent = ({
524
527
  type: "turn_end";
525
528
  usage?: {
526
529
  inputTokens: number;
530
+ totalInputTokens: number;
527
531
  outputTokens: number;
528
532
  cacheReadTokens: number;
529
533
  cacheWriteTokens: number;
@@ -1,5 +1,6 @@
1
1
  import { delimitUntrusted } from "./untrusted-text.js";
2
2
  import { scrubSecrets } from "./arg-summary.js";
3
+ import { sliceHeadSafe } from "./surrogate-safe-slice.js";
3
4
  const URI_SCHEME = String.raw `[A-Za-z][A-Za-z0-9+.\-_]{0,63}`;
4
5
  const CREDENTIALS_IN_AUTHORITY = new RegExp(String.raw `(${URI_SCHEME}:\/\/)((?:[^/\s"'@]*@)+)`, "gi");
5
6
  const URL_OF_ANY_PROTOCOL = new RegExp(String.raw `${URI_SCHEME}:\/\/[^\s"'\`]+`, "gi");
@@ -52,16 +53,17 @@ function boundedString(value, max, redact) {
52
53
  if (s.length <= max)
53
54
  return s;
54
55
  const totalMarkers = countRedactionMarkers(s);
55
- const plainSuffix = (keepN) => `…[+${s.length - keepN} chars]`;
56
- const enrichedSuffix = (keepN) => {
57
- const lost = totalMarkers - countRedactionMarkers(s.slice(0, keepN));
58
- return lost > 0 ? `…[+${s.length - keepN} chars, ${lost} redaction marker(s) among them]` : plainSuffix(keepN);
56
+ const plainSuffix = (kept) => `…[+${s.length - kept.length} chars]`;
57
+ const enrichedSuffix = (kept) => {
58
+ const lost = totalMarkers - countRedactionMarkers(kept);
59
+ return lost > 0 ? `…[+${s.length - kept.length} chars, ${lost} redaction marker(s) among them]` : plainSuffix(kept);
59
60
  };
60
61
  const converge = (suffixFor) => {
61
62
  let keep = max;
62
63
  let out = "";
63
64
  for (let i = 0; i < 4; i++) {
64
- out = `${s.slice(0, keep)}${suffixFor(keep)}`;
65
+ const kept = sliceHeadSafe(s, keep);
66
+ out = `${kept}${suffixFor(kept)}`;
65
67
  if (out.length <= max)
66
68
  return out;
67
69
  if (keep === 0)
@@ -3,23 +3,20 @@ export interface WorkflowJournalEntry {
3
3
  callKey: string;
4
4
  result: TaskResult;
5
5
  }
6
+ export interface ResumeClaimArgs {
7
+ sourceRunId: string;
8
+ newRunId: string;
9
+ scope: string;
10
+ }
6
11
  export interface WorkflowJournalStore {
7
12
  load(runId: string, scope: string): Promise<WorkflowJournalEntry[]>;
8
13
  append(runId: string, scope: string, entry: WorkflowJournalEntry): Promise<void>;
9
14
  locator?(runId: string, scope: string): string | undefined;
10
- resumeClaim?(input: {
11
- sourceRunId: string;
12
- newRunId: string;
13
- scope: string;
14
- }): Promise<{
15
+ resumeClaim?(input: ResumeClaimArgs): Promise<{
15
16
  granted: boolean;
16
17
  holder?: string;
17
18
  }>;
18
- releaseResumeClaim?(input: {
19
- sourceRunId: string;
20
- newRunId: string;
21
- scope: string;
22
- }): Promise<void>;
19
+ releaseResumeClaim?(input: ResumeClaimArgs): Promise<void>;
23
20
  }
24
21
  export declare const MAX_JOURNAL_RESULT_BYTES: number;
25
22
  export declare function oversizeJournalResult(serialized: string): boolean;
@@ -31,19 +28,11 @@ export declare class InMemoryWorkflowJournalStore implements WorkflowJournalStor
31
28
  private readonly runs;
32
29
  private readonly resumeClaims;
33
30
  private claimsForScope;
34
- resumeClaim(input: {
35
- sourceRunId: string;
36
- newRunId: string;
37
- scope: string;
38
- }): Promise<{
31
+ resumeClaim(input: ResumeClaimArgs): Promise<{
39
32
  granted: boolean;
40
33
  holder?: string;
41
34
  }>;
42
- releaseResumeClaim(input: {
43
- sourceRunId: string;
44
- newRunId: string;
45
- scope: string;
46
- }): Promise<void>;
35
+ releaseResumeClaim(input: ResumeClaimArgs): Promise<void>;
47
36
  load(runId: string, scope: string): Promise<WorkflowJournalEntry[]>;
48
37
  append(runId: string, scope: string, entry: WorkflowJournalEntry): Promise<void>;
49
38
  }
@@ -21,22 +21,27 @@ export declare class WorkflowRunStoreError extends Error {
21
21
  readonly code: "workflow_run.already_exists";
22
22
  constructor(code: "workflow_run.already_exists", message: string);
23
23
  }
24
+ export interface WorkflowRunListByScopeOptions {
25
+ status?: WorkflowRunStatus;
26
+ limit?: number;
27
+ session?: string;
28
+ }
24
29
  export interface WorkflowRunStore {
25
30
  put(id: string, run: WorkflowRun): Promise<void>;
26
31
  get(id: string): Promise<WorkflowRun | null>;
27
32
  update(id: string, scope: string, run: WorkflowRun, expect?: {
28
33
  rev: number;
29
34
  }): Promise<boolean>;
30
- listByScope(scope: string, opts?: {
31
- status?: WorkflowRunStatus;
32
- limit?: number;
33
- session?: string;
34
- }): Promise<WorkflowRunSummary[]>;
35
+ listByScope(scope: string, opts?: WorkflowRunListByScopeOptions): Promise<WorkflowRunSummary[]>;
35
36
  reap(scope: string, now: number, opts?: {
36
37
  maxAgeMs?: number;
37
38
  keep?: number;
38
39
  }): Promise<number>;
39
40
  }
41
+ export declare function nextWorkflowRunOnUpdate(cur: WorkflowRun | undefined, id: string, scope: string, run: WorkflowRun, expect?: {
42
+ rev: number;
43
+ }): WorkflowRun | null;
44
+ export declare function queryWorkflowRuns(runs: Iterable<WorkflowRun>, scope: string, opts?: WorkflowRunListByScopeOptions): WorkflowRunSummary[];
40
45
  export declare class InMemoryWorkflowRunStore implements WorkflowRunStore {
41
46
  private runs;
42
47
  put(id: string, run: WorkflowRun): Promise<void>;
@@ -44,11 +49,7 @@ export declare class InMemoryWorkflowRunStore implements WorkflowRunStore {
44
49
  update(id: string, scope: string, run: WorkflowRun, expect?: {
45
50
  rev: number;
46
51
  }): Promise<boolean>;
47
- listByScope(scope: string, opts?: {
48
- status?: WorkflowRunStatus;
49
- limit?: number;
50
- session?: string;
51
- }): Promise<WorkflowRunSummary[]>;
52
+ listByScope(scope: string, opts?: WorkflowRunListByScopeOptions): Promise<WorkflowRunSummary[]>;
52
53
  reap(scope: string, now: number, opts?: {
53
54
  maxAgeMs?: number;
54
55
  keep?: number;
@@ -28,6 +28,25 @@ export class WorkflowRunStoreError extends Error {
28
28
  this.name = "WorkflowRunStoreError";
29
29
  }
30
30
  }
31
+ export function nextWorkflowRunOnUpdate(cur, id, scope, run, expect) {
32
+ if (!cur || cur.scope !== scope)
33
+ return null;
34
+ if (expect !== undefined && (cur.rev ?? 0) !== expect.rev)
35
+ return null;
36
+ const next = structuredClone(run);
37
+ next.id = id;
38
+ next.scope = scope;
39
+ next.rev = (cur.rev ?? 0) + 1;
40
+ return next;
41
+ }
42
+ export function queryWorkflowRuns(runs, scope, opts) {
43
+ const rows = [...runs].filter((r) => r.scope === scope &&
44
+ (opts?.status === undefined || r.status === opts.status) &&
45
+ (opts?.session === undefined || r.originatingSessionId === opts.session));
46
+ rows.sort((a, b) => b.createdAt - a.createdAt);
47
+ const limited = opts?.limit !== undefined ? rows.slice(0, Math.max(0, opts.limit)) : rows;
48
+ return limited.map(summarizeWorkflowRun);
49
+ }
31
50
  export class InMemoryWorkflowRunStore {
32
51
  runs = new Map();
33
52
  async put(id, run) {
@@ -44,27 +63,14 @@ export class InMemoryWorkflowRunStore {
44
63
  return run ? structuredClone(run) : null;
45
64
  }
46
65
  async update(id, scope, run, expect) {
47
- const cur = this.runs.get(id);
48
- if (!cur || cur.scope !== scope) {
66
+ const next = nextWorkflowRunOnUpdate(this.runs.get(id), id, scope, run, expect);
67
+ if (!next)
49
68
  return false;
50
- }
51
- if (expect !== undefined && (cur.rev ?? 0) !== expect.rev) {
52
- return false;
53
- }
54
- const next = structuredClone(run);
55
- next.id = id;
56
- next.scope = scope;
57
- next.rev = (cur.rev ?? 0) + 1;
58
69
  this.runs.set(id, next);
59
70
  return true;
60
71
  }
61
72
  async listByScope(scope, opts) {
62
- const rows = [...this.runs.values()].filter((r) => r.scope === scope &&
63
- (opts?.status === undefined || r.status === opts.status) &&
64
- (opts?.session === undefined || r.originatingSessionId === opts.session));
65
- rows.sort((a, b) => b.createdAt - a.createdAt);
66
- const limited = opts?.limit !== undefined ? rows.slice(0, Math.max(0, opts.limit)) : rows;
67
- return limited.map(summarizeWorkflowRun);
73
+ return queryWorkflowRuns(this.runs.values(), scope, opts);
68
74
  }
69
75
  async reap(scope, now, opts) {
70
76
  if (opts?.maxAgeMs === undefined && opts?.keep === undefined)
@@ -78,3 +78,4 @@ export interface CompactionPreparation {
78
78
  export declare function prepareCompaction(pathEntries: SessionTreeEntry[], settings: CompactionSettings, charsPerToken?: number, windowTokens?: number): Result<CompactionPreparation | undefined, CompactionError>;
79
79
  export { computeFileLists, serializeConversation } from "./utils.js";
80
80
  export declare function compact(preparation: CompactionPreparation, model: Model, apiKey: string | undefined, headers?: Record<string, string>, customInstructions?: string, signal?: AbortSignal, thinkingLevel?: ThinkingLevel, streamFn?: StreamFn, runtime?: AgentCoreCompletionRuntimeDeps, charsPerToken?: number, onInputTruncated?: (info: SummarizationInputTruncation) => void, onPtlRetry?: () => void): Promise<Result<CompactionResult, CompactionError>>;
81
+ export declare function turnPrefixSummarizationPrompt(customInstructions?: string): string;
@@ -132,6 +132,9 @@ export function shouldCompact(contextTokens, contextWindow, settings) {
132
132
  if (!settings.enabled) {
133
133
  return false;
134
134
  }
135
+ if (!Number.isFinite(contextWindow) || contextWindow <= 0) {
136
+ return false;
137
+ }
135
138
  return contextTokens > contextWindow - settings.reserveTokens;
136
139
  }
137
140
  export function estimateTokens(message, charsPerToken = DEFAULT_CHARS_PER_TOKEN) {
@@ -539,7 +542,7 @@ export function dryRunSummarizationClamp(preparation, model, customInstructions,
539
542
  if (isSplitTurn && turnPrefixMessages.length > 0) {
540
543
  const text = serializeConversation(convertToLlm(turnPrefixMessages));
541
544
  conversationChars += text.length;
542
- const clamped = clampSummarizationConversation(text, model, cpt, SUMMARIZATION_SYSTEM_PROMPT.length + TURN_PREFIX_SUMMARIZATION_PROMPT.length + 128, Math.floor(0.5 * summaryBudget));
545
+ const clamped = clampSummarizationConversation(text, model, cpt, SUMMARIZATION_SYSTEM_PROMPT.length + turnPrefixSummarizationPrompt(customInstructions).length + 128, Math.floor(0.5 * summaryBudget));
543
546
  dropped += Math.max(0, clamped.droppedChars);
544
547
  kept += clamped.keptChars;
545
548
  }
@@ -879,7 +882,7 @@ export async function compact(preparation, model, apiKey, headers, customInstruc
879
882
  ? generateSummary(messagesToSummarize, model, summaryBudget, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, runtime, charsPerToken, onInputTruncated, onPtlRetry)
880
883
  :
881
884
  Promise.resolve(ok(previousSummary ? stripFileOperationsFooter(previousSummary) : "No prior history.")),
882
- generateTurnPrefixSummary(turnPrefixMessages, model, summaryBudget, apiKey, headers, signal, thinkingLevel, streamFn, runtime, charsPerToken, onInputTruncated, onPtlRetry),
885
+ generateTurnPrefixSummary(turnPrefixMessages, model, summaryBudget, apiKey, headers, signal, customInstructions, thinkingLevel, streamFn, runtime, charsPerToken, onInputTruncated, onPtlRetry),
883
886
  ]);
884
887
  if (!historyResult.ok) {
885
888
  return err(historyResult.error);
@@ -913,16 +916,22 @@ export async function compact(preparation, model, apiKey, headers, customInstruc
913
916
  },
914
917
  });
915
918
  }
916
- async function generateTurnPrefixSummary(messages, model, summaryBudgetTokens, apiKey, headers, signal, thinkingLevel, streamFn, runtime, charsPerToken, onInputTruncated, onPtlRetry) {
919
+ export function turnPrefixSummarizationPrompt(customInstructions) {
920
+ return customInstructions
921
+ ? `${TURN_PREFIX_SUMMARIZATION_PROMPT}\n\nAdditional Instructions:\n${customInstructions}`
922
+ : TURN_PREFIX_SUMMARIZATION_PROMPT;
923
+ }
924
+ async function generateTurnPrefixSummary(messages, model, summaryBudgetTokens, apiKey, headers, signal, customInstructions, thinkingLevel, streamFn, runtime, charsPerToken, onInputTruncated, onPtlRetry) {
917
925
  const llmMessages = convertToLlm(messages);
926
+ const basePrompt = turnPrefixSummarizationPrompt(customInstructions);
918
927
  return summarizeWithPtlRetry({
919
928
  label: "Turn prefix summarization",
920
929
  truncationLabel: "turn_prefix",
921
930
  llmMessages,
922
- buildPromptText: (conversationText) => `<conversation>\n${conversationText}\n</conversation>\n\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`,
931
+ buildPromptText: (conversationText) => `<conversation>\n${conversationText}\n</conversation>\n\n${basePrompt}`,
923
932
  model,
924
933
  cpt: charsPerToken ?? model.charsPerToken ?? DEFAULT_CHARS_PER_TOKEN,
925
- fixedPromptChars: SUMMARIZATION_SYSTEM_PROMPT.length + TURN_PREFIX_SUMMARIZATION_PROMPT.length + 128,
934
+ fixedPromptChars: SUMMARIZATION_SYSTEM_PROMPT.length + basePrompt.length + 128,
926
935
  baseMaxTokens: Math.floor(0.5 * summaryBudgetTokens),
927
936
  apiKey,
928
937
  headers,
@@ -1,4 +1,5 @@
1
1
  import { normalizeLlmMessageContent } from "../harness/messages.js";
2
+ import { sliceHeadSafe } from "../../core/surrogate-safe-slice.js";
2
3
  import { cutEnginePrefix, cutEngineSegments, flattenableUserText } from "../../core/untrusted-text.js";
3
4
  import { fileArgPath } from "../../tools/fs/safety.js";
4
5
  export function createFileOps() {
@@ -287,11 +288,12 @@ function truncateForSummary(text, maxChars) {
287
288
  let keep = maxChars;
288
289
  let out = "";
289
290
  for (let i = 0; i < 4; i++) {
290
- out = `${text.slice(0, keep)}\n\n[... ${text.length - keep} more characters truncated]`;
291
+ const kept = sliceHeadSafe(text, keep);
292
+ out = `${kept}\n\n[... ${text.length - kept.length} more characters truncated]`;
291
293
  if (out.length <= maxChars)
292
294
  break;
293
295
  if (keep === 0)
294
- return maxChars >= 1 ? `${text.slice(0, maxChars - 1)}…` : "";
296
+ return maxChars >= 1 ? `${sliceHeadSafe(text, maxChars - 1)}…` : "";
295
297
  keep = Math.max(0, keep - (out.length - maxChars));
296
298
  }
297
299
  return out.length < text.length ? out : text;