@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
@@ -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) {
@@ -504,16 +495,17 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
504
495
  const batch = rs.turn.toolBatch;
505
496
  rs.turn.toolBatch = [];
506
497
  batchArgs?.clear();
507
- 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) {
508
500
  try {
509
- const r = await postToolBatchHook(batch);
510
- if (r?.additionalContext) {
501
+ const r = await postToolBatchHook(batch, injectedThisTurn !== undefined ? { injectedThisTurn } : undefined);
502
+ if (r?.additionalContext && injectedThisTurn === undefined) {
511
503
  const body = sanitizeUntrustedText(r.additionalContext);
512
504
  const budget = ATTACHMENT_BYTE_CAP - boundaryAttachmentBytes;
513
505
  if (budget >= BATCH_CONTEXT_MIN_KEEP_BYTES) {
514
- const clipped = clipToByteBudget(body, budget) === body
506
+ const clipped = clipToBytes(body, budget) === body
515
507
  ? body
516
- : `${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}`;
517
509
  batchContextBlock = `<system-reminder>\n${clipped}\n</system-reminder>`;
518
510
  }
519
511
  }
@@ -603,10 +595,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
603
595
  const comp = await maybeCompact({
604
596
  session: prepared.session,
605
597
  epochDeclaredSections: prepared.epochDeclaredSections,
606
- ...(() => {
607
- const ca = prepared.centerCompactionCandidate?.();
608
- return ca !== undefined ? { centerAdoption: ca } : {};
609
- })(),
598
+ ...centerAdoptionOption(prepared),
610
599
  model: event.model,
611
600
  compactionModel: prepared.compModel,
612
601
  brain: compactionBrain,
@@ -618,22 +607,11 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
618
607
  ...(manualCancelSignal !== undefined ? { manualCancelSignal } : {}),
619
608
  minTokens: forceManual ? 0 : rs.counters.compactionFloor,
620
609
  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() })),
610
+ onInputTruncated: emitInputTruncated(rs.telemetry.tracer, rs.telemetry.taskId),
622
611
  forceUnderThreshold: trimPressure,
623
612
  overheadTokens: prepared.promptOverheadTokens,
624
613
  ...(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,
614
+ workingFileAttachments: buildWorkingFileAttachments(spec, prepared),
637
615
  ...(prepared.onCompactionApplied ? { onApplied: prepared.onCompactionApplied } : {}),
638
616
  ...runnerHooks.seamCCompactionOptions(prepared),
639
617
  ...windowSafetyOptions(event.model),
@@ -891,7 +869,7 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
891
869
  if (m.role === "assistant" && typeof m.stopReason === "string") {
892
870
  rs.turn.turnStopReason = m.stopReason;
893
871
  }
894
- if (m.role === "assistant" && m.errorMessage === WALLTIME_CUTOFF_MESSAGE) {
872
+ if (m.role === "assistant" && isWalltimeCutMessage(m)) {
895
873
  rs.counters.callCutoffs += 1;
896
874
  if (prepared.callCapRef)
897
875
  prepared.callCapRef.finalizeMode = true;
@@ -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 {
@@ -2324,9 +2298,9 @@ export class Runner {
2324
2298
  truncatedOutput: {},
2325
2299
  malformedToolUse: {},
2326
2300
  thinkingOnly: {},
2327
- degenerateOutput: { detect: (m) => m.errorMessage === DEGENERATE_MESSAGE },
2301
+ degenerateOutput: { detect: (m) => isDegenerateCutMessage(m) },
2328
2302
  walltimeCutoff: {
2329
- detect: (m) => m.errorMessage === WALLTIME_CUTOFF_MESSAGE,
2303
+ detect: (m) => isWalltimeCutMessage(m),
2330
2304
  hasLiveWriteoutWindow: () => {
2331
2305
  const ref = prepared.callCapRef;
2332
2306
  if (ref === undefined)
@@ -2349,10 +2323,7 @@ export class Runner {
2349
2323
  const comp = await maybeCompact({
2350
2324
  session: prepared.session,
2351
2325
  epochDeclaredSections: prepared.epochDeclaredSections,
2352
- ...(() => {
2353
- const ca = prepared.centerCompactionCandidate?.();
2354
- return ca !== undefined ? { centerAdoption: ca } : {};
2355
- })(),
2326
+ ...centerAdoptionOption(prepared),
2356
2327
  model: prepared.harness.getModel(),
2357
2328
  compactionModel: prepared.compModel,
2358
2329
  brain: compactionBrain,
@@ -2365,19 +2336,8 @@ export class Runner {
2365
2336
  force: true,
2366
2337
  overheadTokens: prepared.promptOverheadTokens,
2367
2338
  ...(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,
2339
+ onInputTruncated: emitInputTruncated(rs.telemetry.tracer, rs.telemetry.taskId),
2340
+ workingFileAttachments: buildWorkingFileAttachments(spec, prepared),
2381
2341
  ...(prepared.onCompactionApplied ? { onApplied: prepared.onCompactionApplied } : {}),
2382
2342
  ...this.seamCCompactionOptions(prepared),
2383
2343
  ...windowSafetyOptions(prepared.harness.getModel()),
@@ -2499,6 +2459,8 @@ export class Runner {
2499
2459
  resume.pendingActionStarted = true;
2500
2460
  }
2501
2461
  });
2462
+ if (resume.outcome.gate === "policy_ask")
2463
+ resume.decisionDelivered = true;
2502
2464
  }
2503
2465
  if (walltimeExhaustedResume) {
2504
2466
  timeout.fired = true;
@@ -2530,6 +2492,8 @@ export class Runner {
2530
2492
  if (steer.trusted)
2531
2493
  engineSegments.push({ start, end: continuation.length });
2532
2494
  }
2495
+ if (resume !== undefined)
2496
+ resume.decisionDelivered = true;
2533
2497
  final = await withBrainSinks(() => prepared.harness.prompt(continuation, { engineSegments }));
2534
2498
  }
2535
2499
  }
@@ -2640,17 +2604,8 @@ export class Runner {
2640
2604
  }
2641
2605
  catch (err) {
2642
2606
  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
- }
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 }));
2654
2609
  throw err;
2655
2610
  }
2656
2611
  threw = err;
@@ -2739,7 +2694,7 @@ export class Runner {
2739
2694
  tokens: prepared.nestedStats.tokens,
2740
2695
  turns: prepared.nestedStats.turns,
2741
2696
  tasks: prepared.nestedStats.tasks,
2742
- costMicroUsd: prepared.nestedStats.costMicroUsd || undefined,
2697
+ ...(prepared.nestedStats.anyUnpriced ? {} : { costMicroUsd: prepared.nestedStats.costMicroUsd }),
2743
2698
  };
2744
2699
  }
2745
2700
  if (stats.totalInputTokens > 0) {
@@ -2824,6 +2779,8 @@ export class Runner {
2824
2779
  resume.pendingActionStarted !== true &&
2825
2780
  result.status !== "completed" &&
2826
2781
  result.status !== "suspended" &&
2782
+ result.status !== "needs_review" &&
2783
+ !(resumeDecisionWasNegative(resume) && resume.decisionDelivered === true) &&
2827
2784
  resume.onEnvRestoreFailed !== undefined) {
2828
2785
  try {
2829
2786
  await resume.onEnvRestoreFailed("env_failed");
@@ -2844,15 +2801,11 @@ export class Runner {
2844
2801
  const curWt = prepared.worktreeSessionRef?.current;
2845
2802
  const changed = curCwd !== wss.restoredHandsCwd || curWt?.worktreeDir !== wss.restoredWorktreeDir;
2846
2803
  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
- }
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 }));
2856
2809
  }
2857
2810
  }
2858
2811
  }
@@ -2860,8 +2813,8 @@ export class Runner {
2860
2813
  if (stopFailureHook !== undefined &&
2861
2814
  result.status === "failed" &&
2862
2815
  final?.stopReason === "error" &&
2863
- final.errorMessage !== DEGENERATE_MESSAGE &&
2864
- final.errorMessage !== WALLTIME_CUTOFF_MESSAGE &&
2816
+ !isDegenerateCutMessage(final) &&
2817
+ !isWalltimeCutMessage(final) &&
2865
2818
  threw === undefined &&
2866
2819
  rs.limits.budgetHit === undefined &&
2867
2820
  !rs.degrade.outputInvalid &&
@@ -2902,19 +2855,12 @@ export class Runner {
2902
2855
  }
2903
2856
  }
2904
2857
  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
- }
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 }));
2915
2863
  }
2916
- if (prepared.memoryEngineSession)
2917
- await prepared.memoryEngineSession.harvest();
2918
2864
  emitTrace(rs.telemetry.tracer, () => ({
2919
2865
  kind: "task.end",
2920
2866
  version: 1,
@@ -2968,28 +2914,19 @@ export class Runner {
2968
2914
  isIsolated(prepared.ownedEnv) &&
2969
2915
  prepared.suspendRef.token === undefined &&
2970
2916
  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
- }
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 }));
2993
2930
  const bgEnv = prepared.ownedEnv ?? this.deps.executionEnv;
2994
2931
  const retainBgBySpec = spec.retainBackgroundProcesses === true && !ownedEnvDying;
2995
2932
  if (bgEnv && hasBackgroundShell(bgEnv) && !retainBgBySpec) {
@@ -3001,7 +2938,7 @@ export class Runner {
3001
2938
  ...defaultTaskRegistry.timeoutResidentShellIds(bgEnv),
3002
2939
  ...defaultTaskRegistry.retainedShellIds(bgEnv),
3003
2940
  ];
3004
- 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 }));
3005
2942
  }
3006
2943
  catch (e) {
3007
2944
  this.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId });
@@ -3016,14 +2953,9 @@ export class Runner {
3016
2953
  scope: spec.principal ?? "default",
3017
2954
  }, { skipSessionScoped: true });
3018
2955
  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
- }
2956
+ await settleTeardownLeg(() => prepared.subagentRetain.disposeAll(), "subagentRetain.disposeAll", (e) => this.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
3025
2957
  }
3026
- await this.teardownOwnedEnv(prepared);
2958
+ await settleTeardownLeg(() => this.teardownOwnedEnv(prepared), "teardownOwnedEnv", (e) => this.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
3027
2959
  }
3028
2960
  }
3029
2961
  async suggestNextPrompts(spec, prepared, result) {
@@ -3525,10 +3457,7 @@ export class Runner {
3525
3457
  const finishComp = await maybeCompact({
3526
3458
  session: prepared.session,
3527
3459
  epochDeclaredSections: prepared.epochDeclaredSections,
3528
- ...(() => {
3529
- const ca = prepared.centerCompactionCandidate?.();
3530
- return ca !== undefined ? { centerAdoption: ca } : {};
3531
- })(),
3460
+ ...centerAdoptionOption(prepared),
3532
3461
  model: prepared.model,
3533
3462
  compactionModel: prepared.compModel,
3534
3463
  brain: opts?.brain ?? this.deps.brain,
@@ -3539,28 +3468,9 @@ export class Runner {
3539
3468
  minTokens: opts?.minTokens,
3540
3469
  overheadTokens: prepared.promptOverheadTokens,
3541
3470
  ...(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
- })),
3471
+ onInputTruncated: emitInputTruncated(spec.tracer ?? this.deps.tracer, spec.taskId ?? prepared.sessionId),
3551
3472
  ...(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,
3473
+ workingFileAttachments: buildWorkingFileAttachments(spec, prepared),
3564
3474
  ...(prepared.onCompactionApplied ? { onApplied: prepared.onCompactionApplied } : {}),
3565
3475
  ...this.seamCCompactionOptions(prepared),
3566
3476
  ...this.compactionHookOptions(spec, prepared.sessionId, "auto"),
@@ -3629,7 +3539,6 @@ function startTimeout(harness, abortController, remainingMs, softSuspendable = f
3629
3539
  abortController.abort();
3630
3540
  void harness.abort();
3631
3541
  }, hardMs);
3632
- timer.unref?.();
3633
3542
  state.clear = () => clearTimeout(timer);
3634
3543
  }
3635
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>;
@@ -1,3 +1,4 @@
1
+ import type { McpDroppedTool } from "../mcp.js";
1
2
  import { type SkillListingEntry } from "./synthetic-tools.js";
2
3
  export declare const TODO_REMINDER_CONFIG: {
3
4
  readonly TURNS_SINCE_WRITE: 10;
@@ -112,11 +113,7 @@ export interface AttachmentInputs {
112
113
  }>;
113
114
  removed: readonly string[];
114
115
  };
115
- mcpDroppedTools?: ReadonlyArray<{
116
- server: string;
117
- tool: string;
118
- reason: string;
119
- }>;
116
+ mcpDroppedTools?: ReadonlyArray<McpDroppedTool>;
120
117
  }
121
118
  export declare function collectDueAttachments(state: AttachmentState, inp: AttachmentInputs): readonly TurnAttachment[];
122
119
  export declare function renderToolSearchUsageReminder(undiscovered: readonly string[]): string;
@@ -178,14 +175,7 @@ export declare function renderMcpInstructionsDelta(added: ReadonlyArray<{
178
175
  server: string;
179
176
  text: string;
180
177
  }>, removed: readonly string[]): string | undefined;
181
- export declare function renderMcpDroppedTools(entries: ReadonlyArray<{
182
- server: string;
183
- tool: string;
184
- reason: string;
185
- }>): string;
178
+ export declare function renderMcpDroppedTools(entries: ReadonlyArray<McpDroppedTool>): string;
186
179
  export declare const MCP_DROPPED_BATCH_MAX_BYTES = 4096;
187
- export declare function selectMcpDroppedBatch<T extends {
188
- server: string;
189
- tool: string;
190
- reason: string;
191
- }>(entries: ReadonlyArray<T>): T[];
180
+ export declare function selectMcpDroppedBatch<T extends McpDroppedTool>(entries: ReadonlyArray<T>): T[];
181
+ export declare function clipToBytes(s: string, maxBytes: number): string;
@@ -634,7 +634,7 @@ function capAttachments(atts) {
634
634
  }
635
635
  return out;
636
636
  }
637
- function clipToBytes(s, maxBytes) {
637
+ export function clipToBytes(s, maxBytes) {
638
638
  if (Buffer.byteLength(s, "utf8") <= maxBytes)
639
639
  return s;
640
640
  let lo = 0;
@@ -0,0 +1,17 @@
1
+ export interface SafeNotifyFailure {
2
+ readonly site: string;
3
+ readonly error: Error;
4
+ readonly failures: number;
5
+ }
6
+ export interface SafeNotifierOptions {
7
+ readonly onError?: (failure: SafeNotifyFailure) => void;
8
+ }
9
+ export interface SafeNotifier {
10
+ notify(run: () => void, site: string): boolean;
11
+ notifyAsync(run: () => unknown, site: string): Promise<boolean>;
12
+ readonly failures: number;
13
+ failuresAt(site: string): number;
14
+ lastErrorAt(site: string): Error | undefined;
15
+ failedSites(): readonly string[];
16
+ }
17
+ export declare function createSafeNotifier(opts?: SafeNotifierOptions): SafeNotifier;
@@ -0,0 +1,59 @@
1
+ export function createSafeNotifier(opts) {
2
+ const onError = opts?.onError;
3
+ let sites;
4
+ let total = 0;
5
+ const record = (site, thrown) => {
6
+ const error = thrown instanceof Error ? thrown : new Error(String(thrown));
7
+ total += 1;
8
+ if (sites === undefined)
9
+ sites = new Map();
10
+ const state = sites.get(site);
11
+ if (state !== undefined) {
12
+ state.count += 1;
13
+ state.last = error;
14
+ return;
15
+ }
16
+ sites.set(site, { count: 1, last: error });
17
+ if (onError === undefined)
18
+ return;
19
+ try {
20
+ onError({ site, error, failures: 1 });
21
+ }
22
+ catch {
23
+ }
24
+ };
25
+ return {
26
+ notify(run, site) {
27
+ try {
28
+ run();
29
+ return true;
30
+ }
31
+ catch (e) {
32
+ record(site, e);
33
+ return false;
34
+ }
35
+ },
36
+ async notifyAsync(run, site) {
37
+ try {
38
+ await run();
39
+ return true;
40
+ }
41
+ catch (e) {
42
+ record(site, e);
43
+ return false;
44
+ }
45
+ },
46
+ get failures() {
47
+ return total;
48
+ },
49
+ failuresAt(site) {
50
+ return sites?.get(site)?.count ?? 0;
51
+ },
52
+ lastErrorAt(site) {
53
+ return sites?.get(site)?.last;
54
+ },
55
+ failedSites() {
56
+ return sites === undefined ? [] : [...sites.keys()];
57
+ },
58
+ };
59
+ }
@@ -28,8 +28,9 @@ export const RECOMMENDED_SENSITIVE_PATTERNS = [
28
28
  ];
29
29
  function segmentGlobToRegExp(segment) {
30
30
  const escaped = segment.replace(/[.+^${}()|[\]\\?]/g, "\\$&").replace(/\*/g, "[^/]*");
31
- return new RegExp(`^${escaped}$`);
31
+ return new RegExp(`^${escaped}$`, CASE_INSENSITIVE_FS ? "i" : "");
32
32
  }
33
+ const CASE_INSENSITIVE_FS = process.platform === "darwin" || process.platform === "win32";
33
34
  function compilePatterns(patterns) {
34
35
  const out = [];
35
36
  for (const raw of patterns) {
@@ -20,7 +20,7 @@ export function createCheckpointFixture(over = {}) {
20
20
  },
21
21
  state: {
22
22
  activeTools: [],
23
- nestedStats: { tokens: 0, turns: 0, tasks: 0, costMicroUsd: 0 },
23
+ nestedStats: { tokens: 0, turns: 0, tasks: 0, costMicroUsd: 0, anyUnpriced: false },
24
24
  },
25
25
  status: "pending",
26
26
  createdAt: 1_700_000_000_000,