@sema-agent/core 3.0.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 (127) hide show
  1. package/CHANGELOG.md +2345 -0
  2. package/dist/agents/cascade.d.ts +2 -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.js +27 -19
  9. package/dist/agents/retain-ledger.d.ts +2 -0
  10. package/dist/agents/retain-ledger.js +6 -5
  11. package/dist/agents/roster-store.js +3 -5
  12. package/dist/agents/subagent.js +66 -64
  13. package/dist/agents/teacher.d.ts +1 -9
  14. package/dist/agents/teacher.js +21 -24
  15. package/dist/agents/verify.js +15 -14
  16. package/dist/brain/anthropic.js +24 -21
  17. package/dist/brain/openai.js +24 -21
  18. package/dist/brain/stream-engine.js +5 -2
  19. package/dist/brain/stream-shared.d.ts +1 -0
  20. package/dist/brain/stream-shared.js +14 -0
  21. package/dist/brain/terminal-cause.d.ts +4 -0
  22. package/dist/brain/terminal-cause.js +12 -0
  23. package/dist/brain/tool-call-id.d.ts +1 -0
  24. package/dist/brain/tool-call-id.js +3 -0
  25. package/dist/brain/tool-call-repair.js +2 -1
  26. package/dist/core/auto-compaction.js +16 -25
  27. package/dist/core/background-agent-store.d.ts +22 -22
  28. package/dist/core/background-agent-store.js +19 -23
  29. package/dist/core/checkpoint-store.d.ts +6 -13
  30. package/dist/core/checkpoint-store.js +11 -5
  31. package/dist/core/file-snapshot-store.d.ts +8 -0
  32. package/dist/core/file-snapshot-store.js +4 -3
  33. package/dist/core/hooks.d.ts +3 -1
  34. package/dist/core/hooks.js +8 -18
  35. package/dist/core/lsp.js +4 -3
  36. package/dist/core/mailbox-store.d.ts +7 -10
  37. package/dist/core/mcp.d.ts +7 -10
  38. package/dist/core/mcp.js +7 -3
  39. package/dist/core/memory-engine/engine.d.ts +0 -1
  40. package/dist/core/memory-engine/engine.js +24 -34
  41. package/dist/core/memory-engine/file-backend.d.ts +1 -2
  42. package/dist/core/memory-engine/file-backend.js +25 -36
  43. package/dist/core/memory-engine/layout.d.ts +8 -0
  44. package/dist/core/memory-engine/layout.js +60 -4
  45. package/dist/core/memory-engine/scope-contract.js +9 -3
  46. package/dist/core/memory-engine/types.d.ts +1 -1
  47. package/dist/core/runner/active-skill-scope.d.ts +0 -1
  48. package/dist/core/runner/active-skill-scope.js +1 -15
  49. package/dist/core/runner/assemble-result.d.ts +2 -7
  50. package/dist/core/runner/assemble-result.js +3 -4
  51. package/dist/core/runner/compaction-call-options.d.ts +7 -0
  52. package/dist/core/runner/compaction-call-options.js +30 -0
  53. package/dist/core/runner/prepare-task.d.ts +6 -23
  54. package/dist/core/runner/prepare-task.js +20 -53
  55. package/dist/core/runner/runtask.d.ts +1 -0
  56. package/dist/core/runner/runtask.js +65 -156
  57. package/dist/core/runner/session-rule-policy.js +4 -1
  58. package/dist/core/runner/teardown-bounded.d.ts +7 -0
  59. package/dist/core/runner/teardown-bounded.js +36 -0
  60. package/dist/core/runner/tool-disclosure.d.ts +2 -5
  61. package/dist/core/runner/turn-attachments.d.ts +5 -15
  62. package/dist/core/runner/turn-attachments.js +1 -1
  63. package/dist/core/safe-notify.d.ts +17 -0
  64. package/dist/core/safe-notify.js +59 -0
  65. package/dist/core/sensitive-path-policy.js +2 -1
  66. package/dist/core/store-contracts/checkpoint-store-contract.js +1 -1
  67. package/dist/core/stub-env.d.ts +2 -6
  68. package/dist/core/surrogate-safe-slice.d.ts +4 -0
  69. package/dist/core/surrogate-safe-slice.js +18 -0
  70. package/dist/core/task-registry-monitor.js +8 -13
  71. package/dist/core/task-registry-shared.d.ts +22 -0
  72. package/dist/core/task-registry-shared.js +16 -0
  73. package/dist/core/task-registry.d.ts +1 -0
  74. package/dist/core/task-registry.js +40 -57
  75. package/dist/core/tool-errors.js +5 -4
  76. package/dist/core/tool-name-aliases.js +2 -1
  77. package/dist/core/tool-policy.js +10 -5
  78. package/dist/core/tool-result-store.d.ts +1 -0
  79. package/dist/core/tool-result-store.js +17 -1
  80. package/dist/core/trace.d.ts +2 -9
  81. package/dist/core/types.d.ts +15 -12
  82. package/dist/core/untrusted-egress.js +7 -5
  83. package/dist/core/workflow-journal-store.d.ts +9 -20
  84. package/dist/core/workflow-run-store.d.ts +11 -10
  85. package/dist/core/workflow-run-store.js +22 -16
  86. package/dist/engine/compaction/utils.js +4 -2
  87. package/dist/engine/execution-env/node-execution-env.d.ts +2 -24
  88. package/dist/engine/harness/agent-harness.d.ts +1 -0
  89. package/dist/engine/harness/agent-harness.js +13 -33
  90. package/dist/engine/harness/types.d.ts +29 -67
  91. package/dist/engine/llm/types.d.ts +1 -1
  92. package/dist/engine/loop/agent-loop.js +11 -4
  93. package/dist/engine/loop/types.d.ts +11 -9
  94. package/dist/engine/session/memory-repo.d.ts +2 -6
  95. package/dist/engine/session/session.d.ts +5 -30
  96. package/dist/index.d.ts +3 -1
  97. package/dist/index.js +2 -1
  98. package/dist/internal/harness-types.d.ts +3 -1
  99. package/dist/orchestration/run-spec.js +7 -4
  100. package/dist/orchestration/run-workflow-tool.js +3 -35
  101. package/dist/orchestration/workflow.d.ts +3 -11
  102. package/dist/orchestration/workflow.js +164 -204
  103. package/dist/prompt-assembly/tool-catalog.d.ts +10 -9
  104. package/dist/prompt-assembly/turn-snapshot.d.ts +2 -5
  105. package/dist/stores/file/background-agent-store.d.ts +3 -12
  106. package/dist/stores/file/background-agent-store.js +3 -41
  107. package/dist/stores/file/checkpoint-store.js +6 -6
  108. package/dist/stores/file/mailbox-store.d.ts +2 -6
  109. package/dist/stores/file/session-policy-store.js +15 -1
  110. package/dist/stores/file/session-store.d.ts +2 -6
  111. package/dist/stores/file/workflow-journal-store.d.ts +3 -11
  112. package/dist/stores/file/workflow-run-store.d.ts +3 -7
  113. package/dist/stores/file/workflow-run-store.js +4 -17
  114. package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -0
  115. package/dist/tools/fs/bash-readonly-classifier.js +60 -25
  116. package/dist/tools/fs/fs-bash.js +324 -96
  117. package/dist/tools/fs/fs-pdf.d.ts +2 -12
  118. package/dist/tools/fs/fs-shared.js +2 -2
  119. package/dist/tools/fs/notebook.d.ts +2 -5
  120. package/dist/tools/fs/safety.d.ts +3 -0
  121. package/dist/tools/fs/safety.js +43 -14
  122. package/dist/tools/fs/search.js +5 -5
  123. package/dist/tools/monitor.d.ts +1 -0
  124. package/dist/tools/monitor.js +7 -5
  125. package/dist/tools/worktree.d.ts +2 -7
  126. package/dist/tools/worktree.js +3 -1
  127. package/package.json +6 -2
@@ -1,4 +1,4 @@
1
- import { type ExecutionEnv, ExecutionError, FileError, type FileInfo, type Result } from "../internal/harness.js";
1
+ import { type ExecResult, type ExecutionEnv, ExecutionError, FileError, type FileInfo, type Result } from "../internal/harness.js";
2
2
  export declare class StubExecutionEnv implements ExecutionEnv {
3
3
  cwd: string;
4
4
  constructor(cwd?: string);
@@ -18,10 +18,6 @@ export declare class StubExecutionEnv implements ExecutionEnv {
18
18
  remove(path: string): Promise<Result<void, FileError>>;
19
19
  createTempDir(): Promise<Result<string, FileError>>;
20
20
  createTempFile(): Promise<Result<string, FileError>>;
21
- exec(): Promise<Result<{
22
- stdout: string;
23
- stderr: string;
24
- exitCode: number;
25
- }, ExecutionError>>;
21
+ exec(): Promise<Result<ExecResult, ExecutionError>>;
26
22
  cleanup(): Promise<void>;
27
23
  }
@@ -0,0 +1,4 @@
1
+ export declare const isHighSurrogate: (c: number) => boolean;
2
+ export declare const isLowSurrogate: (c: number) => boolean;
3
+ export declare function sliceHeadSafe(s: string, keep: number): string;
4
+ export declare function sliceTailSafe(s: string, keep: number): string;
@@ -0,0 +1,18 @@
1
+ export const isHighSurrogate = (c) => c >= 0xd800 && c <= 0xdbff;
2
+ export const isLowSurrogate = (c) => c >= 0xdc00 && c <= 0xdfff;
3
+ export function sliceHeadSafe(s, keep) {
4
+ if (keep <= 0)
5
+ return "";
6
+ if (s.length <= keep)
7
+ return s;
8
+ const head = s.slice(0, keep);
9
+ return keep > 0 && isHighSurrogate(head.charCodeAt(keep - 1)) ? head.slice(0, -1) : head;
10
+ }
11
+ export function sliceTailSafe(s, keep) {
12
+ if (keep <= 0)
13
+ return "";
14
+ if (s.length <= keep)
15
+ return s;
16
+ const tail = s.slice(-keep);
17
+ return keep > 0 && isLowSurrogate(tail.charCodeAt(0)) ? tail.slice(1) : tail;
18
+ }
@@ -1,5 +1,5 @@
1
1
  import { delimitUntrusted } from "./untrusted-text.js";
2
- import { assertOwnership, defaultMonitorTimers, MONITOR_MAX_TIMEOUT_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_BATCH_WINDOW_MS, MONITOR_MAX_BATCHES_PER_MINUTE, MONITOR_STORM_BURST, MONITOR_STORM_KILL_AFTER_MS, MONITOR_LINE_BUF_CAP, MONITOR_SPILL_CAP_CHARS, TASK_OUTPUT_MAX_CHARS, mintCompletionId, clipMonitorEvent, clipMonitorLine, terminalTaskSummary, accountDroppedBytes, rollSpoolText, statusFromBackground, droppedGapNote, firstDropNote, alreadyTerminalStopNote, clipTaskOutput, sleepPollStep, } from "./task-registry-shared.js";
2
+ import { assertOwnership, defaultMonitorTimers, MONITOR_MAX_TIMEOUT_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_BATCH_WINDOW_MS, MONITOR_MAX_BATCHES_PER_MINUTE, MONITOR_STORM_BURST, MONITOR_STORM_KILL_AFTER_MS, MONITOR_LINE_BUF_CAP, MONITOR_SPILL_CAP_CHARS, TASK_OUTPUT_MAX_CHARS, mintCompletionId, clipMonitorEvent, clipMonitorLine, terminalTaskSummary, accountDroppedBytes, renderSpoolBody, rollSpoolText, statusFromBackground, droppedGapNote, firstDropNote, alreadyTerminalStopNote, clipTaskOutput, sleepPollStep, } from "./task-registry-shared.js";
3
3
  import { buildToolResultRef, OFFLOAD_TOOL_NAME } from "./tool-result-store.js";
4
4
  export function registerMonitorLane(core, input) {
5
5
  assertOwnership(input, "registerMonitor");
@@ -318,18 +318,13 @@ export async function pollMonitorLane(handle, filter, deadline, signal) {
318
318
  const shape = (text) => clipTaskOutput(rx ? text.split(/\r?\n/).filter((l) => rx.test(l)).join("\n") : text, handle.outputFile);
319
319
  const running = handle.status === "running";
320
320
  const retrieval = running && deadline !== undefined && Date.now() >= deadline ? "timeout" : "success";
321
- const timedOutClause = spool.timedOut === true ? ` (timed out after ${spool.timeoutSec !== undefined ? `${spool.timeoutSec}s` : "its background time budget"})` : "";
322
- const displayStatus = (spool.exitCode !== undefined ? `exited(code ${spool.exitCode})` : running ? "running" : handle.status) + timedOutClause;
323
- const rolledNote = spool.rolledChars > 0 ? `[!] ${spool.rolledChars} earlier char(s) dropped from the spool (memory bound)${monitorSpillNote(handle)}\n` : "";
324
- const droppedNote = (spool.droppedBytes ?? 0) > 0
325
- ? `[!] ${spool.droppedBytes} earlier byte(s) permanently dropped before this window (8MB buffer evicted)\n`
326
- : spool.dropUnknown === true
327
- ? "[!] an unknown amount of earlier output was permanently dropped before this window (buffer evicted)\n"
328
- : "";
329
- const drainNote = spool.drainFailed === true ? "[!] the final output drain at stop failed — the tail of the output may be missing\n" : "";
330
- const stdoutHeading = droppedNote !== "" || drainNote !== "" ? "stdout (re-readable spool — INCOMPLETE, see note above)" : "stdout (full, re-readable)";
331
- const body = `status: ${displayStatus}\n${rolledNote}${droppedNote}${drainNote}--- ${stdoutHeading} ---\n${shape(spool.stdout) || "(none)"}\n` +
332
- `--- stderr ---\n${shape(spool.stderr) || "(none)"}`;
321
+ const body = renderSpoolBody(spool, {
322
+ running,
323
+ handleStatus: handle.status,
324
+ shape,
325
+ rolledExtraClause: monitorSpillNote(handle),
326
+ drainNote: spool.drainFailed === true ? "[!] the final output drain at stop failed — the tail of the output may be missing\n" : "",
327
+ });
333
328
  return {
334
329
  content: delimitUntrusted(`TaskOutput ${handle.id}`, body),
335
330
  details: {
@@ -252,6 +252,28 @@ export declare function accountDroppedBytes(spool: {
252
252
  truncated?: boolean;
253
253
  bytesDroppedBeforeCursor?: number;
254
254
  }): void;
255
+ export declare function spoolDropNote(acct: {
256
+ droppedBytes?: number;
257
+ dropUnknown?: true;
258
+ }): string;
259
+ export interface RenderableSpool {
260
+ stdout: string;
261
+ stderr: string;
262
+ rolledChars: number;
263
+ exitCode?: number;
264
+ timedOut?: boolean;
265
+ timeoutSec?: number;
266
+ droppedBytes?: number;
267
+ dropUnknown?: true;
268
+ }
269
+ export interface SpoolBodyOptions {
270
+ running: boolean;
271
+ handleStatus: SemaTaskStatus;
272
+ shape: (text: string) => string;
273
+ rolledExtraClause: string;
274
+ drainNote: string;
275
+ }
276
+ export declare function renderSpoolBody(spool: RenderableSpool, opts: SpoolBodyOptions): string;
255
277
  export declare function droppedGapNote(spool: {
256
278
  droppedBytes?: number;
257
279
  dropUnknown?: true;
@@ -102,6 +102,22 @@ export function accountDroppedBytes(spool, poll) {
102
102
  else if (poll.truncated === true)
103
103
  spool.dropUnknown = true;
104
104
  }
105
+ export function spoolDropNote(acct) {
106
+ if ((acct.droppedBytes ?? 0) > 0)
107
+ return `[!] ${acct.droppedBytes} earlier byte(s) permanently dropped before this window (8MB buffer evicted)\n`;
108
+ if (acct.dropUnknown === true)
109
+ return "[!] an unknown amount of earlier output was permanently dropped before this window (buffer evicted)\n";
110
+ return "";
111
+ }
112
+ export function renderSpoolBody(spool, opts) {
113
+ const timedOutClause = spool.timedOut === true ? ` (timed out after ${spool.timeoutSec !== undefined ? `${spool.timeoutSec}s` : "its background time budget"})` : "";
114
+ const displayStatus = (spool.exitCode !== undefined ? `exited(code ${spool.exitCode})` : opts.running ? "running" : opts.handleStatus) + timedOutClause;
115
+ const rolledNote = spool.rolledChars > 0 ? `[!] ${spool.rolledChars} earlier char(s) dropped from the spool (memory bound)${opts.rolledExtraClause}\n` : "";
116
+ const droppedNote = spoolDropNote(spool);
117
+ const stdoutHeading = droppedNote !== "" || opts.drainNote !== "" ? "stdout (re-readable spool — INCOMPLETE, see note above)" : "stdout (full, re-readable)";
118
+ return (`status: ${displayStatus}\n${rolledNote}${droppedNote}${opts.drainNote}--- ${stdoutHeading} ---\n${opts.shape(spool.stdout) || "(none)"}\n` +
119
+ `--- stderr ---\n${opts.shape(spool.stderr) || "(none)"}`);
120
+ }
105
121
  export function droppedGapNote(spool) {
106
122
  if ((spool.droppedBytes ?? 0) > 0)
107
123
  return ` [!] ${spool.droppedBytes} earlier byte(s) of output were permanently dropped before this window (execution-environment tail buffer evicted) — the event stream and this spool are INCOMPLETE.`;
@@ -69,6 +69,7 @@ export declare class TaskRegistry {
69
69
  private sessionReapHooks;
70
70
  private bgQuiescenceWatchers;
71
71
  private legacyToTaskId;
72
+ private readonly notifier;
72
73
  private readonly core;
73
74
  durableAgentArmed(id: string): boolean;
74
75
  durableAgentRowProbe(id: string): (() => Promise<boolean>) | undefined;
@@ -4,9 +4,10 @@ import { hasBackgroundShell } from "./background-shell.js";
4
4
  import { shutdownDebug } from "./shutdown-debug.js";
5
5
  import { canAccessAgentRecord, } from "./background-agent-store.js";
6
6
  import { delimitUntrusted } from "./untrusted-text.js";
7
+ import { createSafeNotifier } from "./safe-notify.js";
7
8
  import { registerMonitorLane, pollMonitorLane, stopMonitorLane } from "./task-registry-monitor.js";
8
9
  import { registerWorkflowLane, pollWorkflowLane, stopWorkflowLane } from "./task-registry-workflow.js";
9
- import { mintCompletionId, canAccessWorkflowRun, formatWorkflowRun, clipTaskOutput, assertOwnership, sleepPollStep, statusFromBackground, rollSpoolText, accountDroppedBytes, droppedGapNote, alreadyTerminalStopNote, terminalTaskSummary, TASK_OUTPUT_MAX_CHARS, MONITOR_BATCH_WINDOW_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, MONITOR_MAX_BATCHES_PER_MINUTE, canAccess, DURABLE_AGENT_HANDLE_RE, } from "./task-registry-shared.js";
10
+ import { mintCompletionId, canAccessWorkflowRun, formatWorkflowRun, clipTaskOutput, assertOwnership, sleepPollStep, statusFromBackground, rollSpoolText, accountDroppedBytes, renderSpoolBody, spoolDropNote, droppedGapNote, alreadyTerminalStopNote, terminalTaskSummary, TASK_OUTPUT_MAX_CHARS, MONITOR_BATCH_WINDOW_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, MONITOR_MAX_BATCHES_PER_MINUTE, canAccess, DURABLE_AGENT_HANDLE_RE, } from "./task-registry-shared.js";
10
11
  export { normalizeAgentName, DURABLE_AGENT_HEARTBEAT_MS, DURABLE_AGENT_HANDLE_RE, BG_AGENT_REAP_STOP_ERROR } from "./task-registry-shared.js";
11
12
  import { TASK_OUTPUT_TOOL_NAME, TASK_STOP_TOOL_NAME, TASK_OUTPUT_ALIASES, TASK_STOP_ALIASES, TASK_OUTPUT_CONTRACT, TASK_STOP_CONTRACT, TASK_OUTPUT_MISSING_ID_MESSAGE, TASK_STOP_MISSING_ID_MESSAGE, TASK_STOP_PARAMS, resolveTaskIdArg, REGISTRY_TASK_TOOL_CAPS, composeTaskOutputDescription, composeTaskOutputParams, composeTaskStopDescription, } from "./task-tool-shape.js";
12
13
  import { durableAgentArmedLane, durableAgentRowProbeLane, beginDurableClaimLane, endDurableClaimLane, reapDurableAgentsLane, releaseDurableTranscriptAnchorLane, bindBackgroundAgentSessionLane, registerBackgroundAgentLane, parkBackgroundAgentLane, reconcileParkedAgentsLane, claimParkedAgentLane, rollbackParkedClaimLane, consumeParkedFlipLane, finalizeParkedResumeLane, settleBackgroundAgentLane, abortBackgroundAgentsForOwnerLane, serveDurableAgentRowLane, resolveBackgroundAgentByNameLane, markRetainedContinuationLane, reviveBackgroundAgentLane, settleRevivedAgentLane, unmarkRetainedContinuationLane, attachAgentNotifyLane, deliverToRunningAgentLane, runningBackgroundAgentLabelsLane, runningAgentFooterLane, notFoundRunningAgentsTail, pollBackgroundAgentLane, stopBackgroundAgentLane, } from "./task-registry-agent.js";
@@ -47,6 +48,31 @@ function bashTerminalResult(spool) {
47
48
  const err = spool.stderr.slice(-2_000);
48
49
  return err.length > 0 ? `${out}${out.length > 0 ? "\n" : ""}--- stderr (tail) ---\n${err}` : out;
49
50
  }
51
+ async function drainBashTailBestEffort(handle) {
52
+ if (handle.outputFile === undefined)
53
+ return;
54
+ try {
55
+ const drained = await handle.env.pollBackground(handle.shellId);
56
+ if (drained.ok && handle.spool !== undefined)
57
+ accountDroppedBytes(handle.spool, drained.value);
58
+ if (drained.ok && (drained.value.stdout.length > 0 || drained.value.stderr.length > 0)) {
59
+ if (handle.spool !== undefined) {
60
+ const spool = handle.spool;
61
+ const cap = 2 * TASK_OUTPUT_MAX_CHARS;
62
+ spool.stdout = rollSpoolText(spool, spool.stdout + drained.value.stdout, cap);
63
+ spool.stderr = rollSpoolText(spool, spool.stderr + drained.value.stderr, cap);
64
+ const ap = await handle.env.appendFile(handle.outputFile, drained.value.stdout + drained.value.stderr);
65
+ if (!ap.ok)
66
+ spool.mirrorFailed = true;
67
+ }
68
+ else {
69
+ await handle.env.appendFile(handle.outputFile, drained.value.stdout + drained.value.stderr);
70
+ }
71
+ }
72
+ }
73
+ catch {
74
+ }
75
+ }
50
76
  function bashMirrorGapNote(handle) {
51
77
  return handle.outputFile !== undefined && handle.spool?.mirrorFailed === true
52
78
  ? " [!] The output file is INCOMPLETE (one or more writes to it failed) — trust the result text / TaskOutput over the file."
@@ -77,6 +103,7 @@ export class TaskRegistry {
77
103
  sessionReapHooks = new Set();
78
104
  bgQuiescenceWatchers = new Map();
79
105
  legacyToTaskId = new Map();
106
+ notifier = createSafeNotifier();
80
107
  core = (() => {
81
108
  const self = this;
82
109
  return {
@@ -407,22 +434,7 @@ export class TaskRegistry {
407
434
  handle.stoppedBy = handle.stopSource ?? source;
408
435
  handle.updatedAt = Date.now();
409
436
  mintCompletionId(handle);
410
- if (handle.outputFile !== undefined) {
411
- try {
412
- const drained = await handle.env.pollBackground(handle.shellId);
413
- if (drained.ok && (drained.value.stdout.length > 0 || drained.value.stderr.length > 0)) {
414
- const spool = (handle.spool ??= { stdout: "", stderr: "", rolledChars: 0 });
415
- const cap = 2 * TASK_OUTPUT_MAX_CHARS;
416
- spool.stdout = rollSpoolText(spool, spool.stdout + drained.value.stdout, cap);
417
- spool.stderr = rollSpoolText(spool, spool.stderr + drained.value.stderr, cap);
418
- const ap = await handle.env.appendFile(handle.outputFile, drained.value.stdout + drained.value.stderr);
419
- if (!ap.ok)
420
- spool.mirrorFailed = true;
421
- }
422
- }
423
- catch {
424
- }
425
- }
437
+ await drainBashTailBestEffort(handle);
426
438
  const resultText = handle.spool !== undefined ? bashTerminalResult(handle.spool) : "";
427
439
  this.notifyTerminalOnce(handle, () => handle.onTerminal?.({
428
440
  task_id: handle.id,
@@ -521,21 +533,13 @@ export class TaskRegistry {
521
533
  this.settleBackgroundAgent(handle.id, { status: "killed", error: "session released", cycle: handle.reviveCycle ?? 0 });
522
534
  if (reapTerminalNote !== undefined && handle.terminalNotified !== true) {
523
535
  handle.terminalNotified = true;
524
- try {
525
- reapTerminalNote();
526
- }
527
- catch {
528
- }
536
+ this.notifier.notify(() => reapTerminalNote(), "taskRegistry.reapTerminalNote");
529
537
  }
530
538
  reaped++;
531
539
  }
532
540
  this.pokeBgQuiescence(sessionId);
533
541
  for (const hook of this.sessionReapHooks) {
534
- try {
535
- hook(sessionId, scope);
536
- }
537
- catch {
538
- }
542
+ this.notifier.notify(() => hook(sessionId, scope), "taskRegistry.onSessionReap");
539
543
  }
540
544
  return reaped;
541
545
  }
@@ -585,11 +589,7 @@ export class TaskRegistry {
585
589
  this.bgQuiescenceWatchers.delete(oldest);
586
590
  queueMicrotask(() => {
587
591
  for (const victim of victims) {
588
- try {
589
- victim("evicted");
590
- }
591
- catch {
592
- }
592
+ this.notifier.notify(() => victim("evicted"), "taskRegistry.bgQuiescence.evicted");
593
593
  }
594
594
  });
595
595
  }
@@ -617,22 +617,14 @@ export class TaskRegistry {
617
617
  return;
618
618
  this.bgQuiescenceWatchers.delete(owner);
619
619
  for (const cb of set) {
620
- try {
621
- cb("quiescent");
622
- }
623
- catch {
624
- }
620
+ this.notifier.notify(() => cb("quiescent"), "taskRegistry.bgQuiescence.quiescent");
625
621
  }
626
622
  }
627
623
  notifyTerminalOnce(handle, send) {
628
624
  if (handle.terminalNotified === true)
629
625
  return;
630
626
  handle.terminalNotified = true;
631
- try {
632
- send();
633
- }
634
- catch {
635
- }
627
+ this.notifier.notify(() => send(), "taskRegistry.notifyTerminalOnce");
636
628
  this.pokeBgQuiescence(handle.owner);
637
629
  }
638
630
  startBashWatcher(id, onTerminal) {
@@ -1049,17 +1041,7 @@ The durable record is still marked running (last update ${new Date(row.updatedAt
1049
1041
  const shape = (text) => clipTaskOutput(rx ? text.split(/\r?\n/).filter((l) => rx.test(l)).join("\n") : text, handle.outputFile);
1050
1042
  const running = handle.status === "running";
1051
1043
  const retrieval = running && deadline !== undefined && Date.now() >= deadline ? "timeout" : "success";
1052
- const timedOutClause = spool.timedOut === true ? ` (timed out after ${spool.timeoutSec !== undefined ? `${spool.timeoutSec}s` : "its background time budget"})` : "";
1053
- const displayStatus = (spool.exitCode !== undefined ? `exited(code ${spool.exitCode})` : running ? "running" : handle.status) + timedOutClause;
1054
- const rolledNote = spool.rolledChars > 0 ? `[!] ${spool.rolledChars} earlier char(s) dropped from the spool (memory bound)\n` : "";
1055
- const droppedNote = (spool.droppedBytes ?? 0) > 0
1056
- ? `[!] ${spool.droppedBytes} earlier byte(s) permanently dropped before this window (8MB buffer evicted)\n`
1057
- : spool.dropUnknown === true
1058
- ? "[!] an unknown amount of earlier output was permanently dropped before this window (buffer evicted)\n"
1059
- : "";
1060
- const stdoutHeading = droppedNote !== "" ? "stdout (re-readable spool — INCOMPLETE, see note above)" : "stdout (full, re-readable)";
1061
- const body = `status: ${displayStatus}\n${rolledNote}${droppedNote}--- ${stdoutHeading} ---\n${shape(spool.stdout) || "(none)"}\n` +
1062
- `--- stderr ---\n${shape(spool.stderr) || "(none)"}`;
1044
+ const body = renderSpoolBody(spool, { running, handleStatus: handle.status, shape, rolledExtraClause: "", drainNote: "" });
1063
1045
  return {
1064
1046
  content: delimitUntrusted(`TaskOutput ${handle.id}`, body),
1065
1047
  details: {
@@ -1089,7 +1071,7 @@ The durable record is still marked running (last update ${new Date(row.updatedAt
1089
1071
  };
1090
1072
  let stdout = "";
1091
1073
  let stderr = "";
1092
- let dropped = 0;
1074
+ const dropAcct = {};
1093
1075
  for (;;) {
1094
1076
  const r = await handle.env.pollBackground(handle.shellId);
1095
1077
  if (!r.ok) {
@@ -1107,7 +1089,7 @@ The durable record is still marked running (last update ${new Date(row.updatedAt
1107
1089
  }
1108
1090
  stdout = roll(stdout + r.value.stdout);
1109
1091
  stderr = roll(stderr + r.value.stderr);
1110
- dropped += r.value.bytesDroppedBeforeCursor ?? 0;
1092
+ accountDroppedBytes(dropAcct, r.value);
1111
1093
  handle.status = statusFromBackground(r.value.status, r.value.exitCode);
1112
1094
  if (handle.status === "killed" && handle.stoppedBy === undefined)
1113
1095
  handle.stoppedBy = handle.stopSource ?? (r.value.timedOut === true ? "timeout" : "system");
@@ -1120,7 +1102,7 @@ The durable record is still marked running (last update ${new Date(row.updatedAt
1120
1102
  const shape = (text) => clipTaskOutput(rx ? text.split(/\r?\n/).filter((l) => rx.test(l)).join("\n") : text, handle.outputFile);
1121
1103
  const timedOutClause = r.value.timedOut === true ? ` (timed out after ${r.value.timeoutSec !== undefined ? `${r.value.timeoutSec}s` : "its background time budget"})` : "";
1122
1104
  const displayStatus = (r.value.status === "exited" ? `exited(code ${r.value.exitCode})` : r.value.status) + timedOutClause;
1123
- const droppedNote = (dropped > 0 ? `[!] ${dropped} earlier byte(s) permanently dropped before this window (8MB buffer evicted)\n` : "") +
1105
+ const droppedNote = spoolDropNote(dropAcct) +
1124
1106
  (rolledChars > 0 ? `[!] ${rolledChars} accumulated char(s) dropped from the middle during the blocked wait (memory bound)\n` : "");
1125
1107
  const body = `status: ${displayStatus}\n${droppedNote}--- new stdout ---\n${shape(stdout) || "(none)"}\n` +
1126
1108
  `--- new stderr ---\n${shape(stderr) || "(none)"}`;
@@ -1136,7 +1118,7 @@ The durable record is still marked running (last update ${new Date(row.updatedAt
1136
1118
  details: {
1137
1119
  status: r.value.status,
1138
1120
  exitCode: r.value.exitCode,
1139
- bytesDroppedBeforeCursor: dropped,
1121
+ bytesDroppedBeforeCursor: dropAcct.droppedBytes ?? 0,
1140
1122
  ...(r.value.timedOut === true ? { timedOut: true, ...(r.value.timeoutSec !== undefined ? { timeoutSec: r.value.timeoutSec } : {}) } : {}),
1141
1123
  },
1142
1124
  },
@@ -1205,6 +1187,7 @@ The durable record is still marked running (last update ${new Date(row.updatedAt
1205
1187
  handle.status = "killed";
1206
1188
  handle.updatedAt = Date.now();
1207
1189
  mintCompletionId(handle);
1190
+ await drainBashTailBestEffort(handle);
1208
1191
  this.notifyTerminalOnce(handle, () => explicitStopBashTerminalNote(handle, false));
1209
1192
  return {
1210
1193
  content: `Terminated ${handle.id}.`,
@@ -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;
@@ -8,7 +8,7 @@ export function decisionText(d) {
8
8
  }
9
9
  const ALLOW = { action: "allow" };
10
10
  function withTimeout(p, ms, onTimeout) {
11
- if (!ms || ms <= 0) {
11
+ if (ms === undefined) {
12
12
  return p;
13
13
  }
14
14
  return new Promise((resolve, reject) => {
@@ -25,7 +25,6 @@ function withTimeout(p, ms, onTimeout) {
25
25
  return;
26
26
  settle(resolve, onTimeout());
27
27
  }, ms);
28
- timer.unref?.();
29
28
  p.then((v) => settle(resolve, v), (e) => settle(reject, e));
30
29
  });
31
30
  }
@@ -51,6 +50,9 @@ export function createAllowDenyPolicy(opts) {
51
50
  };
52
51
  }
53
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
+ }
54
56
  const need = new Set(opts.requireApproval.map(canonicalToolName));
55
57
  const deny = new Set((opts.deny ?? []).map(canonicalToolName));
56
58
  const auto = new Set((opts.autoAllow ?? []).map(canonicalToolName));
@@ -403,6 +405,8 @@ export function createUnverifiableDeletePolicy(opts) {
403
405
  function pathSegments(p) {
404
406
  return p.split(/[\\/]/).filter(Boolean);
405
407
  }
408
+ const CASE_INSENSITIVE_FS = process.platform === "darwin" || process.platform === "win32";
409
+ const foldPathCase = (s) => (CASE_INSENSITIVE_FS ? s.toLowerCase() : s);
406
410
  function lexicalPath(p, home) {
407
411
  const norm = p.normalize("NFC");
408
412
  const expanded = norm === "~" ? home : /^~[\\/]/.test(norm) ? join(home, norm.slice(2)) : norm;
@@ -432,17 +436,18 @@ export function createTranscriptIntegrityPolicy(opts) {
432
436
  });
433
437
  const readAllow = new Set(opts?.readAllow ?? BASH_READONLY_DEFAULT_ALLOW);
434
438
  const shellTools = canonicalToolNameSet(opts?.tools);
435
- const dirSegs = dirs.map(pathSegments);
439
+ const dirSegs = dirs.map((d) => pathSegments(d).map(foldPathCase));
436
440
  const inProtectedDir = (p) => {
437
441
  const abs = lexicalPath(p, home);
438
- const segs = pathSegments(abs);
442
+ const segs = pathSegments(abs).map(foldPathCase);
439
443
  if (isAbsolute(abs)) {
440
444
  return dirSegs.some((d) => d.length <= segs.length && d.every((s, i) => s === segs[i]));
441
445
  }
442
446
  return dirSegs.some((d) => relativeContinuesDirTail(segs, d));
443
447
  };
444
448
  const referencesProtectedDir = (cmd) => {
445
- if (textualForms.some((f) => cmd.includes(f)))
449
+ const hay = foldPathCase(cmd);
450
+ if (textualForms.some((f) => hay.includes(foldPathCase(f))))
446
451
  return true;
447
452
  for (const raw of cmd.split(/[\s;&|<>()]+/)) {
448
453
  if (raw === "")
@@ -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;
@@ -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;
@@ -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
  }