@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
@@ -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";
@@ -87,20 +88,6 @@ function batchResponseDigest(result) {
87
88
  ? `${text.slice(0, BATCH_RESPONSE_MAX_CHARS)}…[+${text.length - BATCH_RESPONSE_MAX_CHARS} chars truncated]`
88
89
  : text;
89
90
  }
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
91
  const BATCH_CONTEXT_MIN_KEEP_BYTES = 160;
105
92
  const BATCH_TRUNCATION_MARKER = "\n…[truncated]";
106
93
  const SUGGESTIONS_DEFAULT_COUNT = 3;
@@ -118,6 +105,10 @@ function resolveMaxTurns(limits) {
118
105
  }
119
106
  return DEFAULT_MAX_TURNS;
120
107
  }
108
+ function resumeDecisionWasNegative(resume) {
109
+ const o = resume.outcome;
110
+ return o.decision === "reject" || o.decision === "deny";
111
+ }
121
112
  const DEFERRED_REISSUE = "[DEFERRED] This tool call shared a batch with a call that suspended for durable approval, so it was " +
122
113
  "NOT executed on resume. If you still need it, issue it again now.";
123
114
  function deepJsonEqual(a, b) {
@@ -341,8 +332,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
341
332
  const scan = await Promise.race([
342
333
  prepared.detectExternalChanges(maxFiles),
343
334
  new Promise((resolve) => {
344
- const t = setTimeout(() => resolve({ changed: [], evicted: [] }), SCAN_CEILING_MS);
345
- t.unref?.();
335
+ setTimeout(() => resolve({ changed: [], evicted: [] }), SCAN_CEILING_MS);
346
336
  }),
347
337
  ]);
348
338
  for (const p of scan.evicted)
@@ -475,8 +465,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
475
465
  probed = await Promise.race([
476
466
  rs.attach.instrProbe(prepared.instructionSources),
477
467
  new Promise((resolve) => {
478
- const t = setTimeout(() => resolve(null), PROBE_CEILING_MS);
479
- t.unref?.();
468
+ setTimeout(() => resolve(null), PROBE_CEILING_MS);
480
469
  }),
481
470
  ]);
482
471
  }
@@ -506,16 +495,17 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
506
495
  const batch = rs.turn.toolBatch;
507
496
  rs.turn.toolBatch = [];
508
497
  batchArgs?.clear();
509
- if (!boundarySteered && !rs.counters.finalizeInjected && !finalVerifyInjectedThisTurn && !prepared.abortController.signal.aborted) {
498
+ const injectedThisTurn = finalVerifyInjectedThisTurn ? "final_verification" : rs.counters.finalizeInjected ? "finalize" : undefined;
499
+ if (!boundarySteered && !prepared.abortController.signal.aborted) {
510
500
  try {
511
- const r = await postToolBatchHook(batch);
512
- if (r?.additionalContext) {
501
+ const r = await postToolBatchHook(batch, injectedThisTurn !== undefined ? { injectedThisTurn } : undefined);
502
+ if (r?.additionalContext && injectedThisTurn === undefined) {
513
503
  const body = sanitizeUntrustedText(r.additionalContext);
514
504
  const budget = ATTACHMENT_BYTE_CAP - boundaryAttachmentBytes;
515
505
  if (budget >= BATCH_CONTEXT_MIN_KEEP_BYTES) {
516
- const clipped = clipToByteBudget(body, budget) === body
506
+ const clipped = clipToBytes(body, budget) === body
517
507
  ? body
518
- : `${clipToByteBudget(body, budget - Buffer.byteLength(BATCH_TRUNCATION_MARKER, "utf8"))}${BATCH_TRUNCATION_MARKER}`;
508
+ : `${clipToBytes(body, budget - Buffer.byteLength(BATCH_TRUNCATION_MARKER, "utf8"))}${BATCH_TRUNCATION_MARKER}`;
519
509
  batchContextBlock = `<system-reminder>\n${clipped}\n</system-reminder>`;
520
510
  }
521
511
  }
@@ -605,10 +595,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
605
595
  const comp = await maybeCompact({
606
596
  session: prepared.session,
607
597
  epochDeclaredSections: prepared.epochDeclaredSections,
608
- ...(() => {
609
- const ca = prepared.centerCompactionCandidate?.();
610
- return ca !== undefined ? { centerAdoption: ca } : {};
611
- })(),
598
+ ...centerAdoptionOption(prepared),
612
599
  model: event.model,
613
600
  compactionModel: prepared.compModel,
614
601
  brain: compactionBrain,
@@ -620,22 +607,11 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
620
607
  ...(manualCancelSignal !== undefined ? { manualCancelSignal } : {}),
621
608
  minTokens: forceManual ? 0 : rs.counters.compactionFloor,
622
609
  force: forceManual,
623
- 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() })),
610
+ onInputTruncated: emitInputTruncated(rs.telemetry.tracer, rs.telemetry.taskId),
624
611
  forceUnderThreshold: trimPressure,
625
612
  overheadTokens: prepared.promptOverheadTokens,
626
613
  ...(prepared.activeTools.size > 0 ? { activeTools: [...prepared.activeTools] } : {}),
627
- workingFileAttachments: spec.compaction?.attachWorkingFiles !== false && prepared.readTaskFile
628
- ? {
629
- readFile: prepared.readTaskFile,
630
- ...(prepared.recentlyReadFiles ? { recentlyReadFiles: prepared.recentlyReadFiles } : {}),
631
- ...(prepared.normalizeAttachmentPath ? { normalizePath: prepared.normalizeAttachmentPath } : {}),
632
- ...(prepared.isDedupStubResult ? { isDedupStubResult: prepared.isDedupStubResult } : {}),
633
- ...(prepared.instructionSources
634
- ? { instructionSourcePaths: prepared.instructionSources.filter((s) => s.contentHash !== null).map((s) => s.path) }
635
- : {}),
636
- ...(typeof spec.compaction?.attachWorkingFiles === "object" ? spec.compaction.attachWorkingFiles : undefined),
637
- }
638
- : undefined,
614
+ workingFileAttachments: buildWorkingFileAttachments(spec, prepared),
639
615
  ...(prepared.onCompactionApplied ? { onApplied: prepared.onCompactionApplied } : {}),
640
616
  ...runnerHooks.seamCCompactionOptions(prepared),
641
617
  ...windowSafetyOptions(event.model),
@@ -811,16 +787,18 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
811
787
  rs.turn.turnUsageMissing = true;
812
788
  const u = m.usage;
813
789
  stats.tokens += u.totalTokens || 0;
814
- const { totalInputTokens: turnInput, costMicroUsd: turnCostMicroUsd } = usageCostMicroUsd(rs.telemetry.cacheFamily, u, rs.telemetry.pricing);
790
+ const { totalInputTokens: turnTotal, uncachedInputTokens: turnUncached, costMicroUsd: turnCostMicroUsd, } = usageCostMicroUsd(rs.telemetry.cacheFamily, u, rs.telemetry.pricing);
815
791
  if (!rs.telemetry.pricingConfigured)
816
792
  rs.telemetry.unpricedSpend = true;
817
- stats.promptTokens += turnInput;
793
+ stats.promptTokens += turnUncached;
794
+ stats.totalInputTokens += turnTotal;
818
795
  stats.cachedTokens += u.cacheRead || 0;
819
796
  stats.cacheWriteTokens += u.cacheWrite || 0;
820
797
  stats.outputTokens += u.output || 0;
821
798
  stats.costMicroUsd += turnCostMicroUsd;
822
- rs.turn.turnUsage ??= { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, costMicroUsd: 0 };
823
- rs.turn.turnUsage.inputTokens += turnInput;
799
+ rs.turn.turnUsage ??= { inputTokens: 0, totalInputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, costMicroUsd: 0 };
800
+ rs.turn.turnUsage.inputTokens += turnUncached;
801
+ rs.turn.turnUsage.totalInputTokens += turnTotal;
824
802
  rs.turn.turnUsage.outputTokens += u.output || 0;
825
803
  rs.turn.turnUsage.cacheReadTokens += u.cacheRead || 0;
826
804
  rs.turn.turnUsage.cacheWriteTokens += u.cacheWrite || 0;
@@ -846,7 +824,8 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
846
824
  model: m.model,
847
825
  provider: m.provider,
848
826
  turn: stats.turns + 1,
849
- promptTokens: turnInput,
827
+ promptTokens: turnUncached,
828
+ totalInputTokens: turnTotal,
850
829
  completionTokens: u.output || 0,
851
830
  cacheRead: u.cacheRead || 0,
852
831
  cacheWrite: u.cacheWrite || 0,
@@ -890,7 +869,7 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
890
869
  if (m.role === "assistant" && typeof m.stopReason === "string") {
891
870
  rs.turn.turnStopReason = m.stopReason;
892
871
  }
893
- if (m.role === "assistant" && m.errorMessage === WALLTIME_CUTOFF_MESSAGE) {
872
+ if (m.role === "assistant" && isWalltimeCutMessage(m)) {
894
873
  rs.counters.callCutoffs += 1;
895
874
  if (prepared.callCapRef)
896
875
  prepared.callCapRef.finalizeMode = true;
@@ -1262,7 +1241,6 @@ export class Runner {
1262
1241
  let t;
1263
1242
  const timeout = new Promise((res2) => {
1264
1243
  t = setTimeout(() => res2(undefined), READY_TIMEOUT_MS);
1265
- t.unref?.();
1266
1244
  });
1267
1245
  return Promise.race([p, timeout]).finally(() => clearTimeout(t));
1268
1246
  };
@@ -1313,7 +1291,7 @@ export class Runner {
1313
1291
  const settled = run.catch(async (err) => {
1314
1292
  let code = errorCodeOf(err);
1315
1293
  const reopenReason = code === "resume.tool_unavailable" ? "tool_unavailable" : "env_failed";
1316
- const reopenable = resume !== undefined && resume.pendingActionStarted !== true && reopenReason !== undefined;
1294
+ const reopenable = resume !== undefined && resume.pendingActionStarted !== true && reopenReason !== undefined && !(resumeDecisionWasNegative(resume) && resume.decisionDelivered === true);
1317
1295
  let reopenCommitted = false;
1318
1296
  if (reopenable && resume?.onEnvRestoreFailed) {
1319
1297
  try {
@@ -1333,11 +1311,7 @@ export class Runner {
1333
1311
  }
1334
1312
  if (resume && !reopenCommitted) {
1335
1313
  this.parentConstraintRegistry.delete(resume.cp.token);
1336
- try {
1337
- await this.sessions.unpin?.(resume.cp.sessionId);
1338
- }
1339
- catch {
1340
- }
1314
+ 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
1315
  }
1342
1316
  if (resultValue !== undefined) {
1343
1317
  try {
@@ -1650,7 +1624,7 @@ export class Runner {
1650
1624
  }
1651
1625
  }
1652
1626
  onReady({ harness: prepared.harness, abortController: prepared.abortController });
1653
- const stats = { turns: 0, tokens: 0, toolCalls: 0, promptTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, cacheWriteTokensLong: 0, outputTokens: 0, costMicroUsd: 0 };
1627
+ const stats = { turns: 0, tokens: 0, toolCalls: 0, promptTokens: 0, totalInputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, cacheWriteTokensLong: 0, outputTokens: 0, costMicroUsd: 0 };
1654
1628
  prepared.liveSpendRef.get = () => ({ costMicroUsd: stats.costMicroUsd, tokens: stats.tokens, turns: stats.turns, walltimeMs: Math.round(performance.now() - rs.telemetry.taskStartMonotonic) });
1655
1629
  if (resume &&
1656
1630
  resume.cp.suspendedAt !== undefined &&
@@ -2132,9 +2106,10 @@ export class Runner {
2132
2106
  const priced = this.deps.pricing?.[m.id] !== undefined || m.cost !== undefined;
2133
2107
  if (!priced)
2134
2108
  rs.telemetry.unpricedSpend = true;
2135
- const { totalInputTokens, costMicroUsd } = usageCostMicroUsd(fam, u, price);
2109
+ const { totalInputTokens, uncachedInputTokens, costMicroUsd } = usageCostMicroUsd(fam, u, price);
2136
2110
  stats.tokens += u.totalTokens || 0;
2137
- stats.promptTokens += totalInputTokens;
2111
+ stats.promptTokens += uncachedInputTokens;
2112
+ stats.totalInputTokens += totalInputTokens;
2138
2113
  stats.cachedTokens += u.cacheRead || 0;
2139
2114
  stats.cacheWriteTokens += u.cacheWrite || 0;
2140
2115
  stats.outputTokens += u.output || 0;
@@ -2142,7 +2117,7 @@ export class Runner {
2142
2117
  stats.compactionMicroUsd = (stats.compactionMicroUsd ?? 0) + costMicroUsd;
2143
2118
  emitTrace(rs.telemetry.tracer, () => ({
2144
2119
  kind: "brain.call", version: 1, taskId: rs.telemetry.taskId, model: m.id, provider: m.provider,
2145
- promptTokens: totalInputTokens, completionTokens: u.output || 0,
2120
+ promptTokens: uncachedInputTokens, totalInputTokens, completionTokens: u.output || 0,
2146
2121
  cacheRead: u.cacheRead || 0, cacheWrite: u.cacheWrite || 0,
2147
2122
  latencyMs: 0, ...(priced ? { costMicroUsd } : {}), ...(typeof msg?.stopReason === "string" ? { stopReason: msg.stopReason } : {}), ts: Date.now(),
2148
2123
  }));
@@ -2323,9 +2298,9 @@ export class Runner {
2323
2298
  truncatedOutput: {},
2324
2299
  malformedToolUse: {},
2325
2300
  thinkingOnly: {},
2326
- degenerateOutput: { detect: (m) => m.errorMessage === DEGENERATE_MESSAGE },
2301
+ degenerateOutput: { detect: (m) => isDegenerateCutMessage(m) },
2327
2302
  walltimeCutoff: {
2328
- detect: (m) => m.errorMessage === WALLTIME_CUTOFF_MESSAGE,
2303
+ detect: (m) => isWalltimeCutMessage(m),
2329
2304
  hasLiveWriteoutWindow: () => {
2330
2305
  const ref = prepared.callCapRef;
2331
2306
  if (ref === undefined)
@@ -2348,10 +2323,7 @@ export class Runner {
2348
2323
  const comp = await maybeCompact({
2349
2324
  session: prepared.session,
2350
2325
  epochDeclaredSections: prepared.epochDeclaredSections,
2351
- ...(() => {
2352
- const ca = prepared.centerCompactionCandidate?.();
2353
- return ca !== undefined ? { centerAdoption: ca } : {};
2354
- })(),
2326
+ ...centerAdoptionOption(prepared),
2355
2327
  model: prepared.harness.getModel(),
2356
2328
  compactionModel: prepared.compModel,
2357
2329
  brain: compactionBrain,
@@ -2364,19 +2336,8 @@ export class Runner {
2364
2336
  force: true,
2365
2337
  overheadTokens: prepared.promptOverheadTokens,
2366
2338
  ...(prepared.activeTools.size > 0 ? { activeTools: [...prepared.activeTools] } : {}),
2367
- 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() })),
2368
- workingFileAttachments: spec.compaction?.attachWorkingFiles !== false && prepared.readTaskFile
2369
- ? {
2370
- readFile: prepared.readTaskFile,
2371
- ...(prepared.recentlyReadFiles ? { recentlyReadFiles: prepared.recentlyReadFiles } : {}),
2372
- ...(prepared.normalizeAttachmentPath ? { normalizePath: prepared.normalizeAttachmentPath } : {}),
2373
- ...(prepared.isDedupStubResult ? { isDedupStubResult: prepared.isDedupStubResult } : {}),
2374
- ...(prepared.instructionSources
2375
- ? { instructionSourcePaths: prepared.instructionSources.filter((s) => s.contentHash !== null).map((s) => s.path) }
2376
- : {}),
2377
- ...(typeof spec.compaction?.attachWorkingFiles === "object" ? spec.compaction.attachWorkingFiles : undefined),
2378
- }
2379
- : undefined,
2339
+ onInputTruncated: emitInputTruncated(rs.telemetry.tracer, rs.telemetry.taskId),
2340
+ workingFileAttachments: buildWorkingFileAttachments(spec, prepared),
2380
2341
  ...(prepared.onCompactionApplied ? { onApplied: prepared.onCompactionApplied } : {}),
2381
2342
  ...this.seamCCompactionOptions(prepared),
2382
2343
  ...windowSafetyOptions(prepared.harness.getModel()),
@@ -2498,6 +2459,8 @@ export class Runner {
2498
2459
  resume.pendingActionStarted = true;
2499
2460
  }
2500
2461
  });
2462
+ if (resume.outcome.gate === "policy_ask")
2463
+ resume.decisionDelivered = true;
2501
2464
  }
2502
2465
  if (walltimeExhaustedResume) {
2503
2466
  timeout.fired = true;
@@ -2529,6 +2492,8 @@ export class Runner {
2529
2492
  if (steer.trusted)
2530
2493
  engineSegments.push({ start, end: continuation.length });
2531
2494
  }
2495
+ if (resume !== undefined)
2496
+ resume.decisionDelivered = true;
2532
2497
  final = await withBrainSinks(() => prepared.harness.prompt(continuation, { engineSegments }));
2533
2498
  }
2534
2499
  }
@@ -2639,17 +2604,8 @@ export class Runner {
2639
2604
  }
2640
2605
  catch (err) {
2641
2606
  if (errorCodeOf(err) === "resume.tool_unavailable") {
2642
- try {
2643
- await prepared.mcp.dispose();
2644
- }
2645
- catch {
2646
- }
2647
- try {
2648
- if (prepared.ownedEnv && hasDestroy(prepared.ownedEnv))
2649
- await prepared.ownedEnv.destroy();
2650
- }
2651
- catch {
2652
- }
2607
+ await settleTeardownLeg(() => prepared.mcp.dispose(), "mcp.dispose (tool_unavailable leg)", (e) => this.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
2608
+ 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 }));
2653
2609
  throw err;
2654
2610
  }
2655
2611
  threw = err;
@@ -2738,20 +2694,20 @@ export class Runner {
2738
2694
  tokens: prepared.nestedStats.tokens,
2739
2695
  turns: prepared.nestedStats.turns,
2740
2696
  tasks: prepared.nestedStats.tasks,
2741
- costMicroUsd: prepared.nestedStats.costMicroUsd || undefined,
2697
+ ...(prepared.nestedStats.anyUnpriced ? {} : { costMicroUsd: prepared.nestedStats.costMicroUsd }),
2742
2698
  };
2743
2699
  }
2744
- if (stats.promptTokens > 0) {
2745
- const rawHit = stats.cachedTokens / stats.promptTokens;
2746
- if (stats.cachedTokens > stats.promptTokens) {
2747
- this.deps.onError?.(new Error(`prompt-cache: usage/api mismatch — cachedTokens (${stats.cachedTokens}) > promptTokens (${stats.promptTokens}); the cache family is likely wrong. Does model.api match the brain serving it? Override with model.params.promptCacheFamily = "input-includes-cached" | "input-excludes-cached".`), { phase: "prompt-cache", sessionId: prepared.sessionId });
2700
+ if (stats.totalInputTokens > 0) {
2701
+ const rawHit = stats.cachedTokens / stats.totalInputTokens;
2702
+ if (stats.cachedTokens > stats.totalInputTokens) {
2703
+ this.deps.onError?.(new Error(`prompt-cache: usage/api mismatch — cachedTokens (${stats.cachedTokens}) > totalInputTokens (${stats.totalInputTokens}); the cache family is likely wrong. Does model.api match the brain serving it? Override with model.params.promptCacheFamily = "input-includes-cached" | "input-excludes-cached".`), { phase: "prompt-cache", sessionId: prepared.sessionId });
2748
2704
  }
2749
2705
  stats.cacheHitRate = Math.min(1, rawHit);
2750
- if (!rs.telemetry.cacheBreakReported && stats.turns >= 2 && stats.promptTokens >= 8000 && stats.cacheHitRate < 0.15) {
2706
+ if (!rs.telemetry.cacheBreakReported && stats.turns >= 2 && stats.totalInputTokens >= 8000 && stats.cacheHitRate < 0.15) {
2751
2707
  const cause = rs.degrade.degraded
2752
2708
  ? `a mid-task model switch (${rs.degrade.degraded.from} → ${rs.degrade.degraded.to}, degradation) reset the prefix cache — this is the likely cause`
2753
2709
  : `the cacheable prefix looks unstable. Keep volatile content (memory/timestamps/ids) out of the prompt prefix and tool order stable, or it's a server-side/TTL miss`;
2754
- this.deps.onError?.(new Error(`prompt-cache: low prefix-cache hit rate ${(stats.cacheHitRate * 100).toFixed(0)}% over ${stats.turns} turns (${stats.promptTokens} prompt tokens) — ${cause}. See design/09.`), { phase: "prompt-cache", sessionId: prepared.sessionId });
2710
+ this.deps.onError?.(new Error(`prompt-cache: low prefix-cache hit rate ${(stats.cacheHitRate * 100).toFixed(0)}% over ${stats.turns} turns (${stats.totalInputTokens} prompt tokens) — ${cause}. See design/09.`), { phase: "prompt-cache", sessionId: prepared.sessionId });
2755
2711
  }
2756
2712
  }
2757
2713
  if (prepared.resourceLedger &&
@@ -2823,6 +2779,8 @@ export class Runner {
2823
2779
  resume.pendingActionStarted !== true &&
2824
2780
  result.status !== "completed" &&
2825
2781
  result.status !== "suspended" &&
2782
+ result.status !== "needs_review" &&
2783
+ !(resumeDecisionWasNegative(resume) && resume.decisionDelivered === true) &&
2826
2784
  resume.onEnvRestoreFailed !== undefined) {
2827
2785
  try {
2828
2786
  await resume.onEnvRestoreFailed("env_failed");
@@ -2843,15 +2801,11 @@ export class Runner {
2843
2801
  const curWt = prepared.worktreeSessionRef?.current;
2844
2802
  const changed = curCwd !== wss.restoredHandsCwd || curWt?.worktreeDir !== wss.restoredWorktreeDir;
2845
2803
  if (curCwd !== undefined || curWt !== undefined || changed || wss.baselineUnknown === true) {
2846
- try {
2847
- await prepared.session.appendWorkspaceState({
2848
- taskRoot: wss.rootCanonical,
2849
- ...(curCwd !== undefined ? { handsCwd: curCwd } : {}),
2850
- ...(curWt !== undefined ? { activeWorktree: { ...curWt } } : {}),
2851
- });
2852
- }
2853
- catch {
2854
- }
2804
+ await settleTeardownLeg(() => prepared.session.appendWorkspaceState({
2805
+ taskRoot: wss.rootCanonical,
2806
+ ...(curCwd !== undefined ? { handsCwd: curCwd } : {}),
2807
+ ...(curWt !== undefined ? { activeWorktree: { ...curWt } } : {}),
2808
+ }), "session.appendWorkspaceState (settle leg)", (e) => this.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
2855
2809
  }
2856
2810
  }
2857
2811
  }
@@ -2859,8 +2813,8 @@ export class Runner {
2859
2813
  if (stopFailureHook !== undefined &&
2860
2814
  result.status === "failed" &&
2861
2815
  final?.stopReason === "error" &&
2862
- final.errorMessage !== DEGENERATE_MESSAGE &&
2863
- final.errorMessage !== WALLTIME_CUTOFF_MESSAGE &&
2816
+ !isDegenerateCutMessage(final) &&
2817
+ !isWalltimeCutMessage(final) &&
2864
2818
  threw === undefined &&
2865
2819
  rs.limits.budgetHit === undefined &&
2866
2820
  !rs.degrade.outputInvalid &&
@@ -2901,19 +2855,12 @@ export class Runner {
2901
2855
  }
2902
2856
  }
2903
2857
  if (resume && committedToken === undefined) {
2904
- try {
2905
- await this.sessions.unpin?.(resume.cp.sessionId);
2906
- }
2907
- catch (err) {
2908
- try {
2909
- this.deps.onError?.(err, { phase: "config", sessionId: prepared.sessionId });
2910
- }
2911
- catch {
2912
- }
2913
- }
2858
+ await settleTeardownLeg(() => this.sessions.unpin?.(resume.cp.sessionId), "sessions.unpin (resumed-terminal leg)", (e) => this.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
2859
+ }
2860
+ if (prepared.memoryEngineSession) {
2861
+ const mes = prepared.memoryEngineSession;
2862
+ await settleTeardownLeg(() => mes.harvest(), "memoryEngineSession.harvest (settle leg)", (e) => this.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
2914
2863
  }
2915
- if (prepared.memoryEngineSession)
2916
- await prepared.memoryEngineSession.harvest();
2917
2864
  emitTrace(rs.telemetry.tracer, () => ({
2918
2865
  kind: "task.end",
2919
2866
  version: 1,
@@ -2967,28 +2914,19 @@ export class Runner {
2967
2914
  isIsolated(prepared.ownedEnv) &&
2968
2915
  prepared.suspendRef.token === undefined &&
2969
2916
  prepared.reviewRef.token === undefined;
2970
- try {
2971
- await defaultTaskRegistry.settleKilledForOwner({ owner: spec.taskId ?? prepared.sessionId, scope: spec.principal ?? "default", sessionId: prepared.sessionId }, {
2972
- source: abortedLive &&
2973
- !timeout.fired &&
2974
- !rs.limits.turnsExceeded &&
2975
- rs.limits.budgetHit === undefined &&
2976
- prepared.suspendRef.token === undefined &&
2977
- prepared.reviewRef.token === undefined
2978
- ? "user"
2979
- : "parent",
2980
- skipSessionScoped: true,
2981
- envDying: ownedEnvDying,
2982
- retainProcesses: spec.retainBackgroundProcesses === true,
2983
- });
2984
- }
2985
- catch (e) {
2986
- try {
2987
- this.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId });
2988
- }
2989
- catch {
2990
- }
2991
- }
2917
+ await settleTeardownLeg(() => defaultTaskRegistry.settleKilledForOwner({ owner: spec.taskId ?? prepared.sessionId, scope: spec.principal ?? "default", sessionId: prepared.sessionId }, {
2918
+ source: abortedLive &&
2919
+ !timeout.fired &&
2920
+ !rs.limits.turnsExceeded &&
2921
+ rs.limits.budgetHit === undefined &&
2922
+ prepared.suspendRef.token === undefined &&
2923
+ prepared.reviewRef.token === undefined
2924
+ ? "user"
2925
+ : "parent",
2926
+ skipSessionScoped: true,
2927
+ envDying: ownedEnvDying,
2928
+ retainProcesses: spec.retainBackgroundProcesses === true,
2929
+ }), "taskRegistry.settleKilledForOwner", (e) => this.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
2992
2930
  const bgEnv = prepared.ownedEnv ?? this.deps.executionEnv;
2993
2931
  const retainBgBySpec = spec.retainBackgroundProcesses === true && !ownedEnvDying;
2994
2932
  if (bgEnv && hasBackgroundShell(bgEnv) && !retainBgBySpec) {
@@ -3000,7 +2938,7 @@ export class Runner {
3000
2938
  ...defaultTaskRegistry.timeoutResidentShellIds(bgEnv),
3001
2939
  ...defaultTaskRegistry.retainedShellIds(bgEnv),
3002
2940
  ];
3003
- await sweepBackgroundShells(bgEnv, defaultTaskRegistry, keepAlive.length > 0 ? { except: keepAlive } : undefined);
2941
+ await settleTeardownLeg(() => sweepBackgroundShells(bgEnv, defaultTaskRegistry, keepAlive.length > 0 ? { except: keepAlive } : undefined), "sweepBackgroundShells", (e) => this.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
3004
2942
  }
3005
2943
  catch (e) {
3006
2944
  this.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId });
@@ -3015,14 +2953,9 @@ export class Runner {
3015
2953
  scope: spec.principal ?? "default",
3016
2954
  }, { skipSessionScoped: true });
3017
2955
  if (prepared.subagentRetain) {
3018
- try {
3019
- await prepared.subagentRetain.disposeAll();
3020
- }
3021
- catch (e) {
3022
- this.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId });
3023
- }
2956
+ await settleTeardownLeg(() => prepared.subagentRetain.disposeAll(), "subagentRetain.disposeAll", (e) => this.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
3024
2957
  }
3025
- await this.teardownOwnedEnv(prepared);
2958
+ await settleTeardownLeg(() => this.teardownOwnedEnv(prepared), "teardownOwnedEnv", (e) => this.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
3026
2959
  }
3027
2960
  }
3028
2961
  async suggestNextPrompts(spec, prepared, result) {
@@ -3035,7 +2968,6 @@ export class Runner {
3035
2968
  ac.abort();
3036
2969
  resolve([]);
3037
2970
  }, SUGGESTIONS_TIMEOUT_MS);
3038
- timer.unref?.();
3039
2971
  });
3040
2972
  const work = (async () => {
3041
2973
  let model = prepared.model;
@@ -3107,7 +3039,6 @@ export class Runner {
3107
3039
  ac.abort();
3108
3040
  resolve({ ok: false, error: { code: "aborted", message: "snapshot timed out" } });
3109
3041
  }, SNAPSHOT_TIMEOUT_MS);
3110
- timer.unref?.();
3111
3042
  });
3112
3043
  const work = store.snapshot(prepared.sessionId, leafId, env, root, ac.signal);
3113
3044
  work.catch(() => { });
@@ -3526,10 +3457,7 @@ export class Runner {
3526
3457
  const finishComp = await maybeCompact({
3527
3458
  session: prepared.session,
3528
3459
  epochDeclaredSections: prepared.epochDeclaredSections,
3529
- ...(() => {
3530
- const ca = prepared.centerCompactionCandidate?.();
3531
- return ca !== undefined ? { centerAdoption: ca } : {};
3532
- })(),
3460
+ ...centerAdoptionOption(prepared),
3533
3461
  model: prepared.model,
3534
3462
  compactionModel: prepared.compModel,
3535
3463
  brain: opts?.brain ?? this.deps.brain,
@@ -3540,28 +3468,9 @@ export class Runner {
3540
3468
  minTokens: opts?.minTokens,
3541
3469
  overheadTokens: prepared.promptOverheadTokens,
3542
3470
  ...(prepared.activeTools.size > 0 ? { activeTools: [...prepared.activeTools] } : {}),
3543
- onInputTruncated: (info) => emitTrace(spec.tracer ?? this.deps.tracer, () => ({
3544
- kind: "compaction.input_truncated",
3545
- version: 1,
3546
- taskId: spec.taskId ?? prepared.sessionId,
3547
- label: info.label,
3548
- droppedChars: info.droppedChars,
3549
- keptChars: info.keptChars,
3550
- ts: Date.now(),
3551
- })),
3471
+ onInputTruncated: emitInputTruncated(spec.tracer ?? this.deps.tracer, spec.taskId ?? prepared.sessionId),
3552
3472
  ...(opts?.windowSafety ?? {}),
3553
- workingFileAttachments: spec.compaction?.attachWorkingFiles !== false && prepared.readTaskFile
3554
- ? {
3555
- readFile: prepared.readTaskFile,
3556
- ...(prepared.recentlyReadFiles ? { recentlyReadFiles: prepared.recentlyReadFiles } : {}),
3557
- ...(prepared.normalizeAttachmentPath ? { normalizePath: prepared.normalizeAttachmentPath } : {}),
3558
- ...(prepared.isDedupStubResult ? { isDedupStubResult: prepared.isDedupStubResult } : {}),
3559
- ...(prepared.instructionSources
3560
- ? { instructionSourcePaths: prepared.instructionSources.filter((s) => s.contentHash !== null).map((s) => s.path) }
3561
- : {}),
3562
- ...(typeof spec.compaction?.attachWorkingFiles === "object" ? spec.compaction.attachWorkingFiles : undefined),
3563
- }
3564
- : undefined,
3473
+ workingFileAttachments: buildWorkingFileAttachments(spec, prepared),
3565
3474
  ...(prepared.onCompactionApplied ? { onApplied: prepared.onCompactionApplied } : {}),
3566
3475
  ...this.seamCCompactionOptions(prepared),
3567
3476
  ...this.compactionHookOptions(spec, prepared.sessionId, "auto"),
@@ -3630,7 +3539,6 @@ function startTimeout(harness, abortController, remainingMs, softSuspendable = f
3630
3539
  abortController.abort();
3631
3540
  void harness.abort();
3632
3541
  }, hardMs);
3633
- timer.unref?.();
3634
3542
  state.clear = () => clearTimeout(timer);
3635
3543
  }
3636
3544
  return state;
@@ -1,10 +1,13 @@
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
4
  import { canonicalToolName } from "../tool-name-aliases.js";
4
5
  export const PATH_WRITE_TOOLS = new Set(["Write", "Edit", "MultiEdit"]);
5
6
  export function isWithin(root, p) {
7
+ if (!root)
8
+ return false;
6
9
  const norm = (x) => {
7
- let n = x.replace(/\\/g, "/");
10
+ let n = isWinFormPath(x) ? x.replace(/\\/g, "/") : x;
8
11
  if (/^[A-Za-z]:(\/|$)/.test(n))
9
12
  n = n[0].toLowerCase() + n.slice(1);
10
13
  return n;
@@ -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>;