@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
@@ -9,8 +9,7 @@ import { ASK_USER_QUESTION_TOOL_NAME } from "../ask-question.js";
9
9
  import { computeCostMicroUsd, modelCostToPricing } from "../pricing.js";
10
10
  import { emitTrace } from "../trace.js";
11
11
  import { emitTaskOutcome } from "../task-outcome.js";
12
- import { DEGENERATE_MESSAGE } from "../../brain/repetition.js";
13
- import { WALLTIME_CUTOFF_MESSAGE } from "../../brain/walltime.js";
12
+ import { isDegenerateCutMessage, isWalltimeCutMessage } from "../../brain/terminal-cause.js";
14
13
  import { primaryActivityArg } from "../arg-summary.js";
15
14
  import { resolveReasoning } from "../../brain/reasoning.js";
16
15
  import { readDegradation } from "../../brain/degrading.js";
@@ -24,8 +23,10 @@ import { toImageContent } from "./image.js";
24
23
  import { OUTPUT_TOOL_NAME, SKILLS_LISTING_PROBE_HEADER, resolveOutputRetries } from "./synthetic-tools.js";
25
24
  import { cacheFamilyOf, usageCostMicroUsd } from "./usage-accounting.js";
26
25
  import { assembleResult, errorCodeOf } from "./assemble-result.js";
27
- import { ATTACHMENT_BYTE_CAP, CHANGED_FILES_MAX, AGENT_LISTING_REMOVED_HEADER, SKILLS_LISTING_DELTA_HEADER, SKILLS_LISTING_REMOVED_HEADER, advanceCadenceClock, agentListingDeltaHeader, agentListingInitialHeader, replayAnnouncedListing, replayAnnouncedModels, collectDateChange, collectDueAttachments, collectInstructionsChange, commitAgentListing, commitInstructionsChange, commitSkillsListing, createAttachmentState, rebaseCadenceWindows, reduceToolEnd, renderAgentListingDelta, renderMcpDroppedTools, renderMcpInstructionsDelta, renderOrphanedBackgroundTasks, selectMcpDroppedBatch, renderSkillsListingDelta, renderToolsDelta, stampWriteAnchor } from "./turn-attachments.js";
26
+ import { ATTACHMENT_BYTE_CAP, CHANGED_FILES_MAX, AGENT_LISTING_REMOVED_HEADER, SKILLS_LISTING_DELTA_HEADER, SKILLS_LISTING_REMOVED_HEADER, advanceCadenceClock, agentListingDeltaHeader, agentListingInitialHeader, replayAnnouncedListing, replayAnnouncedModels, clipToBytes, collectDateChange, collectDueAttachments, collectInstructionsChange, commitAgentListing, commitInstructionsChange, commitSkillsListing, createAttachmentState, rebaseCadenceWindows, reduceToolEnd, renderAgentListingDelta, renderMcpDroppedTools, renderMcpInstructionsDelta, renderOrphanedBackgroundTasks, selectMcpDroppedBatch, renderSkillsListingDelta, renderToolsDelta, stampWriteAnchor } from "./turn-attachments.js";
27
+ import { buildWorkingFileAttachments, centerAdoptionOption, emitInputTruncated } from "./compaction-call-options.js";
28
28
  import { prepareTask } from "./prepare-task.js";
29
+ import { settleTeardownLeg } from "./teardown-bounded.js";
29
30
  import { TOOL_SEARCH_NAME } from "./tool-disclosure.js";
30
31
  import { hasVerifiableStructureSignal } from "./grounding-signal.js";
31
32
  import { hasDestroy, isIsolated } from "../remote-env.js";
@@ -35,7 +36,6 @@ import { delimitUntrusted, inlineUntrusted, sanitizeUntrustedText } from "../unt
35
36
  import { reconcileInterruptedSession } from "../session-reconcile.js";
36
37
  import { RunnerSharedToolResultStore } from "../tool-result-store.js";
37
38
  import { formatDiagnosticsBlock } from "../lsp-diagnostics.js";
38
- import { canonicalToolName } from "../tool-name-aliases.js";
39
39
  import { toolPolicyNameSets } from "../tool-policy.js";
40
40
  import { defaultTaskRegistry } from "../task-registry.js";
41
41
  import { discloseDroppedPending, PendingSessionNotifications, renderTaskNotificationXml, SystemInjectionQueue, taskNotificationDedupKey } from "../task-notification.js";
@@ -87,20 +87,6 @@ function batchResponseDigest(result) {
87
87
  ? `${text.slice(0, BATCH_RESPONSE_MAX_CHARS)}…[+${text.length - BATCH_RESPONSE_MAX_CHARS} chars truncated]`
88
88
  : text;
89
89
  }
90
- function clipToByteBudget(s, maxBytes) {
91
- if (Buffer.byteLength(s, "utf8") <= maxBytes)
92
- return s;
93
- let lo = 0;
94
- let hi = s.length;
95
- while (lo < hi) {
96
- const mid = (lo + hi + 1) >> 1;
97
- if (Buffer.byteLength(s.slice(0, mid), "utf8") <= maxBytes)
98
- lo = mid;
99
- else
100
- hi = mid - 1;
101
- }
102
- return s.slice(0, lo);
103
- }
104
90
  const BATCH_CONTEXT_MIN_KEEP_BYTES = 160;
105
91
  const BATCH_TRUNCATION_MARKER = "\n…[truncated]";
106
92
  const SUGGESTIONS_DEFAULT_COUNT = 3;
@@ -118,6 +104,10 @@ function resolveMaxTurns(limits) {
118
104
  }
119
105
  return DEFAULT_MAX_TURNS;
120
106
  }
107
+ function resumeDecisionWasNegative(resume) {
108
+ const o = resume.outcome;
109
+ return o.decision === "reject" || o.decision === "deny";
110
+ }
121
111
  const DEFERRED_REISSUE = "[DEFERRED] This tool call shared a batch with a call that suspended for durable approval, so it was " +
122
112
  "NOT executed on resume. If you still need it, issue it again now.";
123
113
  function deepJsonEqual(a, b) {
@@ -504,16 +494,17 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
504
494
  const batch = rs.turn.toolBatch;
505
495
  rs.turn.toolBatch = [];
506
496
  batchArgs?.clear();
507
- if (!boundarySteered && !rs.counters.finalizeInjected && !finalVerifyInjectedThisTurn && !prepared.abortController.signal.aborted) {
497
+ const injectedThisTurn = finalVerifyInjectedThisTurn ? "final_verification" : rs.counters.finalizeInjected ? "finalize" : undefined;
498
+ if (!boundarySteered && !prepared.abortController.signal.aborted) {
508
499
  try {
509
- const r = await postToolBatchHook(batch);
510
- if (r?.additionalContext) {
500
+ const r = await postToolBatchHook(batch, injectedThisTurn !== undefined ? { injectedThisTurn } : undefined);
501
+ if (r?.additionalContext && injectedThisTurn === undefined) {
511
502
  const body = sanitizeUntrustedText(r.additionalContext);
512
503
  const budget = ATTACHMENT_BYTE_CAP - boundaryAttachmentBytes;
513
504
  if (budget >= BATCH_CONTEXT_MIN_KEEP_BYTES) {
514
- const clipped = clipToByteBudget(body, budget) === body
505
+ const clipped = clipToBytes(body, budget) === body
515
506
  ? body
516
- : `${clipToByteBudget(body, budget - Buffer.byteLength(BATCH_TRUNCATION_MARKER, "utf8"))}${BATCH_TRUNCATION_MARKER}`;
507
+ : `${clipToBytes(body, budget - Buffer.byteLength(BATCH_TRUNCATION_MARKER, "utf8"))}${BATCH_TRUNCATION_MARKER}`;
517
508
  batchContextBlock = `<system-reminder>\n${clipped}\n</system-reminder>`;
518
509
  }
519
510
  }
@@ -603,10 +594,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
603
594
  const comp = await maybeCompact({
604
595
  session: prepared.session,
605
596
  epochDeclaredSections: prepared.epochDeclaredSections,
606
- ...(() => {
607
- const ca = prepared.centerCompactionCandidate?.();
608
- return ca !== undefined ? { centerAdoption: ca } : {};
609
- })(),
597
+ ...centerAdoptionOption(prepared),
610
598
  model: event.model,
611
599
  compactionModel: prepared.compModel,
612
600
  brain: compactionBrain,
@@ -618,22 +606,11 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
618
606
  ...(manualCancelSignal !== undefined ? { manualCancelSignal } : {}),
619
607
  minTokens: forceManual ? 0 : rs.counters.compactionFloor,
620
608
  force: forceManual,
621
- onInputTruncated: (info) => emitTrace(rs.telemetry.tracer, () => ({ kind: "compaction.input_truncated", version: 1, taskId: rs.telemetry.taskId, label: info.label, droppedChars: info.droppedChars, keptChars: info.keptChars, ts: Date.now() })),
609
+ onInputTruncated: emitInputTruncated(rs.telemetry.tracer, rs.telemetry.taskId),
622
610
  forceUnderThreshold: trimPressure,
623
611
  overheadTokens: prepared.promptOverheadTokens,
624
612
  ...(prepared.activeTools.size > 0 ? { activeTools: [...prepared.activeTools] } : {}),
625
- workingFileAttachments: spec.compaction?.attachWorkingFiles !== false && prepared.readTaskFile
626
- ? {
627
- readFile: prepared.readTaskFile,
628
- ...(prepared.recentlyReadFiles ? { recentlyReadFiles: prepared.recentlyReadFiles } : {}),
629
- ...(prepared.normalizeAttachmentPath ? { normalizePath: prepared.normalizeAttachmentPath } : {}),
630
- ...(prepared.isDedupStubResult ? { isDedupStubResult: prepared.isDedupStubResult } : {}),
631
- ...(prepared.instructionSources
632
- ? { instructionSourcePaths: prepared.instructionSources.filter((s) => s.contentHash !== null).map((s) => s.path) }
633
- : {}),
634
- ...(typeof spec.compaction?.attachWorkingFiles === "object" ? spec.compaction.attachWorkingFiles : undefined),
635
- }
636
- : undefined,
613
+ workingFileAttachments: buildWorkingFileAttachments(spec, prepared),
637
614
  ...(prepared.onCompactionApplied ? { onApplied: prepared.onCompactionApplied } : {}),
638
615
  ...runnerHooks.seamCCompactionOptions(prepared),
639
616
  ...windowSafetyOptions(event.model),
@@ -770,6 +747,13 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
770
747
  function makeHarnessHandlers(prepared, stats, rs, deps) {
771
748
  const { spec, queue, internals, buildFinalizeText, ident, nudgeSchedule, parentToolCallId, subagentName, pushContent, emitCommitted, startedToolCallIds, toolStartAt, turnToolSpan, writeFamilyOf, toolLabels, postToolBatchHook, batchArgs } = deps;
772
749
  let lastWorkspaceCwd = prepared.cwdRef?.current;
750
+ const announceWorkspaceMove = () => {
751
+ const cwdNow = prepared.cwdRef?.current;
752
+ if (cwdNow !== undefined && cwdNow !== lastWorkspaceCwd) {
753
+ lastWorkspaceCwd = cwdNow;
754
+ queue.push({ type: "workspace_changed", cwd: cwdNow, ...ident() });
755
+ }
756
+ };
773
757
  const onMessageUpdate = (event) => {
774
758
  const ev = event.assistantMessageEvent;
775
759
  if (rs.turn.callStartAt === undefined)
@@ -891,7 +875,7 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
891
875
  if (m.role === "assistant" && typeof m.stopReason === "string") {
892
876
  rs.turn.turnStopReason = m.stopReason;
893
877
  }
894
- if (m.role === "assistant" && m.errorMessage === WALLTIME_CUTOFF_MESSAGE) {
878
+ if (m.role === "assistant" && isWalltimeCutMessage(m)) {
895
879
  rs.counters.callCutoffs += 1;
896
880
  if (prepared.callCapRef)
897
881
  prepared.callCapRef.finalizeMode = true;
@@ -1022,7 +1006,7 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
1022
1006
  }));
1023
1007
  internals?.onActivity?.({ phase: "end", toolCallId: event.toolCallId, toolName: event.toolName, isError: event.isError, at: toolNow });
1024
1008
  if (prepared.lspDiagnostics && !event.isError) {
1025
- const name = canonicalToolName(event.toolName);
1009
+ const name = event.toolName;
1026
1010
  if (name === "Edit" || name === "Write" || name === "NotebookEdit") {
1027
1011
  const d = event.result?.details;
1028
1012
  const p = typeof d?.filePath === "string" ? d.filePath : typeof d?.notebookPath === "string" ? d.notebookPath : undefined;
@@ -1048,11 +1032,7 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
1048
1032
  })(),
1049
1033
  ...ident(),
1050
1034
  });
1051
- const cwdNow = prepared.cwdRef?.current;
1052
- if (cwdNow !== undefined && cwdNow !== lastWorkspaceCwd) {
1053
- lastWorkspaceCwd = cwdNow;
1054
- queue.push({ type: "workspace_changed", cwd: cwdNow, ...ident() });
1055
- }
1035
+ announceWorkspaceMove();
1056
1036
  if (spec.outputSchema && event.toolName === OUTPUT_TOOL_NAME) {
1057
1037
  if (event.isError) {
1058
1038
  rs.degrade.outputErrorStreak += 1;
@@ -1130,7 +1110,7 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
1130
1110
  }
1131
1111
  }
1132
1112
  };
1133
- return { onMessageUpdate, onMessageEnd, onToolStart, onToolEnd, onTurnEnd };
1113
+ return { onMessageUpdate, onMessageEnd, onToolStart, onToolEnd, onTurnEnd, announceWorkspaceMove };
1134
1114
  }
1135
1115
  export class Runner {
1136
1116
  deps;
@@ -1313,7 +1293,7 @@ export class Runner {
1313
1293
  const settled = run.catch(async (err) => {
1314
1294
  let code = errorCodeOf(err);
1315
1295
  const reopenReason = code === "resume.tool_unavailable" ? "tool_unavailable" : "env_failed";
1316
- const reopenable = resume !== undefined && resume.pendingActionStarted !== true && reopenReason !== undefined;
1296
+ const reopenable = resume !== undefined && resume.pendingActionStarted !== true && reopenReason !== undefined && !(resumeDecisionWasNegative(resume) && resume.decisionDelivered === true);
1317
1297
  let reopenCommitted = false;
1318
1298
  if (reopenable && resume?.onEnvRestoreFailed) {
1319
1299
  try {
@@ -1333,11 +1313,7 @@ export class Runner {
1333
1313
  }
1334
1314
  if (resume && !reopenCommitted) {
1335
1315
  this.parentConstraintRegistry.delete(resume.cp.token);
1336
- try {
1337
- await this.sessions.unpin?.(resume.cp.sessionId);
1338
- }
1339
- catch {
1340
- }
1316
+ await settleTeardownLeg(() => this.sessions.unpin?.(resume.cp.sessionId), "sessions.unpin (resume-failure leg)", (e) => this.deps.onError?.(e, { phase: "config", sessionId: resume.cp.sessionId }));
1341
1317
  }
1342
1318
  if (resultValue !== undefined) {
1343
1319
  try {
@@ -1940,18 +1916,18 @@ export class Runner {
1940
1916
  }
1941
1917
  }
1942
1918
  rs.attach.attachmentsInjected = 0;
1943
- const todoToolMounted = rs.attach.attachState !== undefined && prepared.tools.some((t) => canonicalToolName(t.name) === "TodoWrite");
1944
- const taskToolsMounted = rs.attach.attachState !== undefined && prepared.tools.some((t) => canonicalToolName(t.name) === "TaskCreate");
1919
+ const todoToolMounted = rs.attach.attachState !== undefined && prepared.tools.some((t) => t.name === "TodoWrite");
1920
+ const taskToolsMounted = rs.attach.attachState !== undefined && prepared.tools.some((t) => t.name === "TaskCreate");
1945
1921
  const writeFamilyByName = new Map();
1946
1922
  if (rs.attach.attachState !== undefined) {
1947
1923
  for (const t of prepared.tools) {
1948
- const family = writeFamilyOfCanonical(canonicalToolName(t.name));
1924
+ const family = writeFamilyOfCanonical(t.name);
1949
1925
  if (family !== undefined)
1950
1926
  for (const n of [t.name, ...(t.aliases ?? [])])
1951
1927
  writeFamilyByName.set(n, family);
1952
1928
  }
1953
1929
  }
1954
- const writeFamilyOf = (name) => writeFamilyByName.get(name) ?? writeFamilyOfCanonical(canonicalToolName(name));
1930
+ const writeFamilyOf = (name) => writeFamilyByName.get(name) ?? writeFamilyOfCanonical(name);
1955
1931
  const toolStartAt = new Map();
1956
1932
  const turnToolSpan = {};
1957
1933
  const startedToolCallIds = new Set();
@@ -2117,7 +2093,7 @@ export class Runner {
2117
2093
  const postToolBatchHook = (spec.hooks ?? this.deps.hooks)?.postToolBatch;
2118
2094
  const batchArgs = postToolBatchHook ? new Map() : undefined;
2119
2095
  rs.turn.toolBatch = [];
2120
- const { onMessageUpdate, onMessageEnd, onToolStart, onToolEnd, onTurnEnd } = makeHarnessHandlers(prepared, stats, rs, {
2096
+ const { onMessageUpdate, onMessageEnd, onToolStart, onToolEnd, onTurnEnd, announceWorkspaceMove } = makeHarnessHandlers(prepared, stats, rs, {
2121
2097
  spec, queue, internals, buildFinalizeText, ident, nudgeSchedule, parentToolCallId, subagentName,
2122
2098
  pushContent, emitCommitted, startedToolCallIds, toolStartAt, turnToolSpan, writeFamilyOf,
2123
2099
  toolLabels, postToolBatchHook, batchArgs,
@@ -2324,9 +2300,9 @@ export class Runner {
2324
2300
  truncatedOutput: {},
2325
2301
  malformedToolUse: {},
2326
2302
  thinkingOnly: {},
2327
- degenerateOutput: { detect: (m) => m.errorMessage === DEGENERATE_MESSAGE },
2303
+ degenerateOutput: { detect: (m) => isDegenerateCutMessage(m) },
2328
2304
  walltimeCutoff: {
2329
- detect: (m) => m.errorMessage === WALLTIME_CUTOFF_MESSAGE,
2305
+ detect: (m) => isWalltimeCutMessage(m),
2330
2306
  hasLiveWriteoutWindow: () => {
2331
2307
  const ref = prepared.callCapRef;
2332
2308
  if (ref === undefined)
@@ -2349,10 +2325,7 @@ export class Runner {
2349
2325
  const comp = await maybeCompact({
2350
2326
  session: prepared.session,
2351
2327
  epochDeclaredSections: prepared.epochDeclaredSections,
2352
- ...(() => {
2353
- const ca = prepared.centerCompactionCandidate?.();
2354
- return ca !== undefined ? { centerAdoption: ca } : {};
2355
- })(),
2328
+ ...centerAdoptionOption(prepared),
2356
2329
  model: prepared.harness.getModel(),
2357
2330
  compactionModel: prepared.compModel,
2358
2331
  brain: compactionBrain,
@@ -2365,19 +2338,8 @@ export class Runner {
2365
2338
  force: true,
2366
2339
  overheadTokens: prepared.promptOverheadTokens,
2367
2340
  ...(prepared.activeTools.size > 0 ? { activeTools: [...prepared.activeTools] } : {}),
2368
- onInputTruncated: (info) => emitTrace(rs.telemetry.tracer, () => ({ kind: "compaction.input_truncated", version: 1, taskId: rs.telemetry.taskId, label: info.label, droppedChars: info.droppedChars, keptChars: info.keptChars, ts: Date.now() })),
2369
- workingFileAttachments: spec.compaction?.attachWorkingFiles !== false && prepared.readTaskFile
2370
- ? {
2371
- readFile: prepared.readTaskFile,
2372
- ...(prepared.recentlyReadFiles ? { recentlyReadFiles: prepared.recentlyReadFiles } : {}),
2373
- ...(prepared.normalizeAttachmentPath ? { normalizePath: prepared.normalizeAttachmentPath } : {}),
2374
- ...(prepared.isDedupStubResult ? { isDedupStubResult: prepared.isDedupStubResult } : {}),
2375
- ...(prepared.instructionSources
2376
- ? { instructionSourcePaths: prepared.instructionSources.filter((s) => s.contentHash !== null).map((s) => s.path) }
2377
- : {}),
2378
- ...(typeof spec.compaction?.attachWorkingFiles === "object" ? spec.compaction.attachWorkingFiles : undefined),
2379
- }
2380
- : undefined,
2341
+ onInputTruncated: emitInputTruncated(rs.telemetry.tracer, rs.telemetry.taskId),
2342
+ workingFileAttachments: buildWorkingFileAttachments(spec, prepared),
2381
2343
  ...(prepared.onCompactionApplied ? { onApplied: prepared.onCompactionApplied } : {}),
2382
2344
  ...this.seamCCompactionOptions(prepared),
2383
2345
  ...windowSafetyOptions(prepared.harness.getModel()),
@@ -2499,6 +2461,9 @@ export class Runner {
2499
2461
  resume.pendingActionStarted = true;
2500
2462
  }
2501
2463
  });
2464
+ if (resume.outcome.gate === "policy_ask")
2465
+ resume.decisionDelivered = true;
2466
+ announceWorkspaceMove();
2502
2467
  }
2503
2468
  if (walltimeExhaustedResume) {
2504
2469
  timeout.fired = true;
@@ -2530,6 +2495,8 @@ export class Runner {
2530
2495
  if (steer.trusted)
2531
2496
  engineSegments.push({ start, end: continuation.length });
2532
2497
  }
2498
+ if (resume !== undefined)
2499
+ resume.decisionDelivered = true;
2533
2500
  final = await withBrainSinks(() => prepared.harness.prompt(continuation, { engineSegments }));
2534
2501
  }
2535
2502
  }
@@ -2640,17 +2607,8 @@ export class Runner {
2640
2607
  }
2641
2608
  catch (err) {
2642
2609
  if (errorCodeOf(err) === "resume.tool_unavailable") {
2643
- try {
2644
- await prepared.mcp.dispose();
2645
- }
2646
- catch {
2647
- }
2648
- try {
2649
- if (prepared.ownedEnv && hasDestroy(prepared.ownedEnv))
2650
- await prepared.ownedEnv.destroy();
2651
- }
2652
- catch {
2653
- }
2610
+ await settleTeardownLeg(() => prepared.mcp.dispose(), "mcp.dispose (tool_unavailable leg)", (e) => this.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
2611
+ await settleTeardownLeg(() => (prepared.ownedEnv && hasDestroy(prepared.ownedEnv) ? prepared.ownedEnv.destroy() : undefined), "ownedEnv.destroy (tool_unavailable leg)", (e) => this.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
2654
2612
  throw err;
2655
2613
  }
2656
2614
  threw = err;
@@ -2739,7 +2697,7 @@ export class Runner {
2739
2697
  tokens: prepared.nestedStats.tokens,
2740
2698
  turns: prepared.nestedStats.turns,
2741
2699
  tasks: prepared.nestedStats.tasks,
2742
- costMicroUsd: prepared.nestedStats.costMicroUsd || undefined,
2700
+ ...(prepared.nestedStats.anyUnpriced ? {} : { costMicroUsd: prepared.nestedStats.costMicroUsd }),
2743
2701
  };
2744
2702
  }
2745
2703
  if (stats.totalInputTokens > 0) {
@@ -2824,6 +2782,8 @@ export class Runner {
2824
2782
  resume.pendingActionStarted !== true &&
2825
2783
  result.status !== "completed" &&
2826
2784
  result.status !== "suspended" &&
2785
+ result.status !== "needs_review" &&
2786
+ !(resumeDecisionWasNegative(resume) && resume.decisionDelivered === true) &&
2827
2787
  resume.onEnvRestoreFailed !== undefined) {
2828
2788
  try {
2829
2789
  await resume.onEnvRestoreFailed("env_failed");
@@ -2844,15 +2804,11 @@ export class Runner {
2844
2804
  const curWt = prepared.worktreeSessionRef?.current;
2845
2805
  const changed = curCwd !== wss.restoredHandsCwd || curWt?.worktreeDir !== wss.restoredWorktreeDir;
2846
2806
  if (curCwd !== undefined || curWt !== undefined || changed || wss.baselineUnknown === true) {
2847
- try {
2848
- await prepared.session.appendWorkspaceState({
2849
- taskRoot: wss.rootCanonical,
2850
- ...(curCwd !== undefined ? { handsCwd: curCwd } : {}),
2851
- ...(curWt !== undefined ? { activeWorktree: { ...curWt } } : {}),
2852
- });
2853
- }
2854
- catch {
2855
- }
2807
+ await settleTeardownLeg(() => prepared.session.appendWorkspaceState({
2808
+ taskRoot: wss.rootCanonical,
2809
+ ...(curCwd !== undefined ? { handsCwd: curCwd } : {}),
2810
+ ...(curWt !== undefined ? { activeWorktree: { ...curWt } } : {}),
2811
+ }), "session.appendWorkspaceState (settle leg)", (e) => this.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
2856
2812
  }
2857
2813
  }
2858
2814
  }
@@ -2860,8 +2816,8 @@ export class Runner {
2860
2816
  if (stopFailureHook !== undefined &&
2861
2817
  result.status === "failed" &&
2862
2818
  final?.stopReason === "error" &&
2863
- final.errorMessage !== DEGENERATE_MESSAGE &&
2864
- final.errorMessage !== WALLTIME_CUTOFF_MESSAGE &&
2819
+ !isDegenerateCutMessage(final) &&
2820
+ !isWalltimeCutMessage(final) &&
2865
2821
  threw === undefined &&
2866
2822
  rs.limits.budgetHit === undefined &&
2867
2823
  !rs.degrade.outputInvalid &&
@@ -2902,19 +2858,12 @@ export class Runner {
2902
2858
  }
2903
2859
  }
2904
2860
  if (resume && committedToken === undefined) {
2905
- try {
2906
- await this.sessions.unpin?.(resume.cp.sessionId);
2907
- }
2908
- catch (err) {
2909
- try {
2910
- this.deps.onError?.(err, { phase: "config", sessionId: prepared.sessionId });
2911
- }
2912
- catch {
2913
- }
2914
- }
2861
+ await settleTeardownLeg(() => this.sessions.unpin?.(resume.cp.sessionId), "sessions.unpin (resumed-terminal leg)", (e) => this.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
2862
+ }
2863
+ if (prepared.memoryEngineSession) {
2864
+ const mes = prepared.memoryEngineSession;
2865
+ await settleTeardownLeg(() => mes.harvest(), "memoryEngineSession.harvest (settle leg)", (e) => this.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
2915
2866
  }
2916
- if (prepared.memoryEngineSession)
2917
- await prepared.memoryEngineSession.harvest();
2918
2867
  emitTrace(rs.telemetry.tracer, () => ({
2919
2868
  kind: "task.end",
2920
2869
  version: 1,
@@ -2968,28 +2917,19 @@ export class Runner {
2968
2917
  isIsolated(prepared.ownedEnv) &&
2969
2918
  prepared.suspendRef.token === undefined &&
2970
2919
  prepared.reviewRef.token === undefined;
2971
- try {
2972
- await defaultTaskRegistry.settleKilledForOwner({ owner: spec.taskId ?? prepared.sessionId, scope: spec.principal ?? "default", sessionId: prepared.sessionId }, {
2973
- source: abortedLive &&
2974
- !timeout.fired &&
2975
- !rs.limits.turnsExceeded &&
2976
- rs.limits.budgetHit === undefined &&
2977
- prepared.suspendRef.token === undefined &&
2978
- prepared.reviewRef.token === undefined
2979
- ? "user"
2980
- : "parent",
2981
- skipSessionScoped: true,
2982
- envDying: ownedEnvDying,
2983
- retainProcesses: spec.retainBackgroundProcesses === true,
2984
- });
2985
- }
2986
- catch (e) {
2987
- try {
2988
- this.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId });
2989
- }
2990
- catch {
2991
- }
2992
- }
2920
+ await settleTeardownLeg(() => defaultTaskRegistry.settleKilledForOwner({ owner: spec.taskId ?? prepared.sessionId, scope: spec.principal ?? "default", sessionId: prepared.sessionId }, {
2921
+ source: abortedLive &&
2922
+ !timeout.fired &&
2923
+ !rs.limits.turnsExceeded &&
2924
+ rs.limits.budgetHit === undefined &&
2925
+ prepared.suspendRef.token === undefined &&
2926
+ prepared.reviewRef.token === undefined
2927
+ ? "user"
2928
+ : "parent",
2929
+ skipSessionScoped: true,
2930
+ envDying: ownedEnvDying,
2931
+ retainProcesses: spec.retainBackgroundProcesses === true,
2932
+ }), "taskRegistry.settleKilledForOwner", (e) => this.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
2993
2933
  const bgEnv = prepared.ownedEnv ?? this.deps.executionEnv;
2994
2934
  const retainBgBySpec = spec.retainBackgroundProcesses === true && !ownedEnvDying;
2995
2935
  if (bgEnv && hasBackgroundShell(bgEnv) && !retainBgBySpec) {
@@ -3001,7 +2941,7 @@ export class Runner {
3001
2941
  ...defaultTaskRegistry.timeoutResidentShellIds(bgEnv),
3002
2942
  ...defaultTaskRegistry.retainedShellIds(bgEnv),
3003
2943
  ];
3004
- await sweepBackgroundShells(bgEnv, defaultTaskRegistry, keepAlive.length > 0 ? { except: keepAlive } : undefined);
2944
+ await settleTeardownLeg(() => sweepBackgroundShells(bgEnv, defaultTaskRegistry, keepAlive.length > 0 ? { except: keepAlive } : undefined), "sweepBackgroundShells", (e) => this.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
3005
2945
  }
3006
2946
  catch (e) {
3007
2947
  this.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId });
@@ -3016,14 +2956,9 @@ export class Runner {
3016
2956
  scope: spec.principal ?? "default",
3017
2957
  }, { skipSessionScoped: true });
3018
2958
  if (prepared.subagentRetain) {
3019
- try {
3020
- await prepared.subagentRetain.disposeAll();
3021
- }
3022
- catch (e) {
3023
- this.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId });
3024
- }
2959
+ await settleTeardownLeg(() => prepared.subagentRetain.disposeAll(), "subagentRetain.disposeAll", (e) => this.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
3025
2960
  }
3026
- await this.teardownOwnedEnv(prepared);
2961
+ await settleTeardownLeg(() => this.teardownOwnedEnv(prepared), "teardownOwnedEnv", (e) => this.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
3027
2962
  }
3028
2963
  }
3029
2964
  async suggestNextPrompts(spec, prepared, result) {
@@ -3358,7 +3293,7 @@ export class Runner {
3358
3293
  return;
3359
3294
  if (resume.outcome.gate === "policy_ask" &&
3360
3295
  resume.outcome.decision === "allow" &&
3361
- !prepared.tools.some((t) => t.name === canonicalToolName(pendingAction.toolName))) {
3296
+ !prepared.tools.some((t) => t.name === pendingAction.toolName)) {
3362
3297
  const e = new Error(`the approved tool "${pendingAction.toolName}" is no longer available on resume — refusing to continue as if it ran; the checkpoint is reopened for a retry with the tool present`);
3363
3298
  e.code = "resume.tool_unavailable";
3364
3299
  throw e;
@@ -3423,7 +3358,7 @@ export class Runner {
3423
3358
  return;
3424
3359
  }
3425
3360
  }
3426
- const tool = prepared.tools.find((t) => t.name === canonicalToolName(pendingAction.toolName));
3361
+ const tool = prepared.tools.find((t) => t.name === pendingAction.toolName);
3427
3362
  if (!tool) {
3428
3363
  const e = new Error(`the approved tool "${pendingAction.toolName}" is no longer available on resume`);
3429
3364
  e.code = "resume.tool_unavailable";
@@ -3443,7 +3378,7 @@ export class Runner {
3443
3378
  }
3444
3379
  const executedIsError = res.isError === true;
3445
3380
  emitEnd(executedIsError);
3446
- onResolvedToolSuccess?.(canonicalToolName(pendingAction.toolName), executedIsError ? undefined : res.details);
3381
+ onResolvedToolSuccess?.(pendingAction.toolName, executedIsError ? undefined : res.details);
3447
3382
  const eid = await prepared.session.appendMessage({
3448
3383
  role: "toolResult",
3449
3384
  toolCallId: pendingAction.toolCallId,
@@ -3525,10 +3460,7 @@ export class Runner {
3525
3460
  const finishComp = await maybeCompact({
3526
3461
  session: prepared.session,
3527
3462
  epochDeclaredSections: prepared.epochDeclaredSections,
3528
- ...(() => {
3529
- const ca = prepared.centerCompactionCandidate?.();
3530
- return ca !== undefined ? { centerAdoption: ca } : {};
3531
- })(),
3463
+ ...centerAdoptionOption(prepared),
3532
3464
  model: prepared.model,
3533
3465
  compactionModel: prepared.compModel,
3534
3466
  brain: opts?.brain ?? this.deps.brain,
@@ -3539,28 +3471,9 @@ export class Runner {
3539
3471
  minTokens: opts?.minTokens,
3540
3472
  overheadTokens: prepared.promptOverheadTokens,
3541
3473
  ...(prepared.activeTools.size > 0 ? { activeTools: [...prepared.activeTools] } : {}),
3542
- onInputTruncated: (info) => emitTrace(spec.tracer ?? this.deps.tracer, () => ({
3543
- kind: "compaction.input_truncated",
3544
- version: 1,
3545
- taskId: spec.taskId ?? prepared.sessionId,
3546
- label: info.label,
3547
- droppedChars: info.droppedChars,
3548
- keptChars: info.keptChars,
3549
- ts: Date.now(),
3550
- })),
3474
+ onInputTruncated: emitInputTruncated(spec.tracer ?? this.deps.tracer, spec.taskId ?? prepared.sessionId),
3551
3475
  ...(opts?.windowSafety ?? {}),
3552
- workingFileAttachments: spec.compaction?.attachWorkingFiles !== false && prepared.readTaskFile
3553
- ? {
3554
- readFile: prepared.readTaskFile,
3555
- ...(prepared.recentlyReadFiles ? { recentlyReadFiles: prepared.recentlyReadFiles } : {}),
3556
- ...(prepared.normalizeAttachmentPath ? { normalizePath: prepared.normalizeAttachmentPath } : {}),
3557
- ...(prepared.isDedupStubResult ? { isDedupStubResult: prepared.isDedupStubResult } : {}),
3558
- ...(prepared.instructionSources
3559
- ? { instructionSourcePaths: prepared.instructionSources.filter((s) => s.contentHash !== null).map((s) => s.path) }
3560
- : {}),
3561
- ...(typeof spec.compaction?.attachWorkingFiles === "object" ? spec.compaction.attachWorkingFiles : undefined),
3562
- }
3563
- : undefined,
3476
+ workingFileAttachments: buildWorkingFileAttachments(spec, prepared),
3564
3477
  ...(prepared.onCompactionApplied ? { onApplied: prepared.onCompactionApplied } : {}),
3565
3478
  ...this.seamCCompactionOptions(prepared),
3566
3479
  ...this.compactionHookOptions(spec, prepared.sessionId, "auto"),
@@ -3629,7 +3542,6 @@ function startTimeout(harness, abortController, remainingMs, softSuspendable = f
3629
3542
  abortController.abort();
3630
3543
  void harness.abort();
3631
3544
  }, hardMs);
3632
- timer.unref?.();
3633
3545
  state.clear = () => clearTimeout(timer);
3634
3546
  }
3635
3547
  return state;
@@ -1,10 +1,12 @@
1
1
  import { canonicalizeTarget, fileArgPath } from "../../tools/fs/safety.js";
2
+ import { isWinFormPath } from "../../tools/fs/safety.js";
2
3
  import { createCoarseCommandNamePolicy } from "../tool-policy.js";
3
- import { canonicalToolName } from "../tool-name-aliases.js";
4
4
  export const PATH_WRITE_TOOLS = new Set(["Write", "Edit", "MultiEdit"]);
5
5
  export function isWithin(root, p) {
6
+ if (!root)
7
+ return false;
6
8
  const norm = (x) => {
7
- let n = x.replace(/\\/g, "/");
9
+ let n = isWinFormPath(x) ? x.replace(/\\/g, "/") : x;
8
10
  if (/^[A-Za-z]:(\/|$)/.test(n))
9
11
  n = n[0].toLowerCase() + n.slice(1);
10
12
  return n;
@@ -19,8 +21,8 @@ export function isWithin(root, p) {
19
21
  const deny = (reason) => ({ action: "deny", reason, decisionReason: "rule" });
20
22
  export function createSessionRulePolicy(rules, opts) {
21
23
  const { env, rootPath, toolEffects } = opts;
22
- const toolDeny = new Set((rules.toolDeny ?? []).map(canonicalToolName));
23
- const toolAllow = rules.toolAllow ? new Set(rules.toolAllow.map(canonicalToolName)) : undefined;
24
+ const toolDeny = new Set(rules.toolDeny ?? []);
25
+ const toolAllow = rules.toolAllow ? new Set(rules.toolAllow) : undefined;
24
26
  const cmdPolicy = rules.commandAllow || rules.commandDeny
25
27
  ? createCoarseCommandNamePolicy({
26
28
  ...(rules.commandAllow ? { allow: rules.commandAllow } : {}),
@@ -31,7 +33,7 @@ export function createSessionRulePolicy(rules, opts) {
31
33
  const allowDirs = rules.allowDirs && rules.allowDirs.length > 0 ? rules.allowDirs : undefined;
32
34
  return {
33
35
  async check(req, signal) {
34
- const toolName = canonicalToolName(req.toolName);
36
+ const toolName = req.toolName;
35
37
  if (toolDeny.has(toolName))
36
38
  return deny(`tool "${req.toolName}" is denied by a session rule`);
37
39
  if (toolAllow && !toolAllow.has(toolName)) {
@@ -0,0 +1,7 @@
1
+ export declare const TEARDOWN_LEG_MAX_MS = 15000;
2
+ export declare function setTeardownLegBudgetForTests(ms: number | undefined): void;
3
+ export declare class TeardownLegTimeoutError extends Error {
4
+ readonly leg: string;
5
+ constructor(leg: string, budgetMs: number);
6
+ }
7
+ export declare function settleTeardownLeg(leg: () => Promise<unknown> | unknown, label: string, report?: (err: Error) => void): Promise<void>;
@@ -0,0 +1,36 @@
1
+ export const TEARDOWN_LEG_MAX_MS = 15_000;
2
+ let teardownLegBudgetMs = TEARDOWN_LEG_MAX_MS;
3
+ export function setTeardownLegBudgetForTests(ms) {
4
+ teardownLegBudgetMs = ms ?? TEARDOWN_LEG_MAX_MS;
5
+ }
6
+ export class TeardownLegTimeoutError extends Error {
7
+ leg;
8
+ constructor(leg, budgetMs) {
9
+ super(`teardown leg "${leg}" did not settle within ${budgetMs}ms — proceeding without it. ` +
10
+ `The leg keeps running detached; whatever it owns (process/env/store row) may be leaked until it settles on its own.`);
11
+ this.name = "TeardownLegTimeoutError";
12
+ this.leg = leg;
13
+ }
14
+ }
15
+ export async function settleTeardownLeg(leg, label, report) {
16
+ let timer;
17
+ try {
18
+ await Promise.race([
19
+ Promise.resolve().then(leg),
20
+ new Promise((_, reject) => {
21
+ timer = setTimeout(() => reject(new TeardownLegTimeoutError(label, teardownLegBudgetMs)), teardownLegBudgetMs);
22
+ }),
23
+ ]);
24
+ }
25
+ catch (e) {
26
+ try {
27
+ report?.(e instanceof Error ? e : new Error(String(e)));
28
+ }
29
+ catch {
30
+ }
31
+ }
32
+ finally {
33
+ if (timer !== undefined)
34
+ clearTimeout(timer);
35
+ }
36
+ }
@@ -2,6 +2,7 @@ import { type TSchema } from "typebox";
2
2
  import type { AgentMessage, AgentTool, AgentToolResult, AgentToolUpdateCallback, ToolExecutionMode } from "../../internal/harness-types.js";
3
3
  import type { Model } from "../../internal/llm.js";
4
4
  import type { ToolSpec } from "../types.js";
5
+ import type { ToolFingerprintInput } from "../cache-break-detector.js";
5
6
  export declare const TOOL_SEARCH_NAME = "ToolSearch";
6
7
  export declare const TOOL_SEARCH_DEFAULT_MAX_RESULTS = 5;
7
8
  export interface DeferredToolInfo {
@@ -14,11 +15,7 @@ export declare function safeName(name: string): string;
14
15
  export declare function classifyDeferred(opts: {
15
16
  specs: ReadonlyArray<ToolSpec>;
16
17
  mcpToolNames: ReadonlyArray<string>;
17
- fullTools: ReadonlyArray<{
18
- name: string;
19
- description: string;
20
- parameters: unknown;
21
- }>;
18
+ fullTools: ReadonlyArray<ToolFingerprintInput>;
22
19
  deferMode?: "auto";
23
20
  model: Model;
24
21
  deferNames?: ReadonlyArray<string>;