@sema-agent/core 5.64.0 → 6.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 (165) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/dist/agents/subagent.d.ts +2 -2
  3. package/dist/agents/subagent.js +11 -0
  4. package/dist/agents/verify.d.ts +1 -1
  5. package/dist/brain/anthropic.js +1 -1
  6. package/dist/brain/errors.d.ts +29 -0
  7. package/dist/brain/errors.js +20 -0
  8. package/dist/brain/open-responses.js +2 -2
  9. package/dist/brain/route-adjudicator.d.ts +8 -1
  10. package/dist/brain/route-adjudicator.js +1 -0
  11. package/dist/brain/status-sink.js +12 -1
  12. package/dist/brain/stream-engine.js +17 -6
  13. package/dist/core/auto-compaction.d.ts +26 -0
  14. package/dist/core/auto-compaction.js +7 -2
  15. package/dist/core/auto-mode-arming.d.ts +138 -0
  16. package/dist/core/auto-mode-arming.js +181 -0
  17. package/dist/core/auto-mode-defaults.d.ts +13 -0
  18. package/dist/core/auto-mode-defaults.js +5 -0
  19. package/dist/core/auto-mode-prompt.d.ts +14 -3
  20. package/dist/core/auto-mode-prompt.js +10 -7
  21. package/dist/core/auto-mode-rebuild.d.ts +75 -0
  22. package/dist/core/auto-mode-rebuild.js +41 -0
  23. package/dist/core/auto-mode.d.ts +15 -0
  24. package/dist/core/auto-mode.js +4 -2
  25. package/dist/core/checkpoint-store.d.ts +113 -4
  26. package/dist/core/context-edit.d.ts +47 -5
  27. package/dist/core/context-guard.d.ts +1 -1
  28. package/dist/core/file-history-retention.d.ts +106 -0
  29. package/dist/core/file-history-retention.js +36 -0
  30. package/dist/core/file-history-store.d.ts +768 -0
  31. package/dist/core/file-history-store.js +880 -0
  32. package/dist/core/governance-codes.d.ts +2 -1
  33. package/dist/core/governance-codes.js +14 -0
  34. package/dist/core/hooks.d.ts +48 -8
  35. package/dist/core/hooks.js +39 -22
  36. package/dist/core/lsp.d.ts +2 -2
  37. package/dist/core/mcp.d.ts +29 -7
  38. package/dist/core/memory-engine/consolidation-driver.d.ts +11 -0
  39. package/dist/core/memory-engine/consolidation-driver.js +71 -4
  40. package/dist/core/memory-engine/consolidation.d.ts +25 -2
  41. package/dist/core/memory-engine/consolidation.js +4 -1
  42. package/dist/core/memory-engine/distiller.d.ts +84 -1
  43. package/dist/core/memory-engine/distiller.js +68 -0
  44. package/dist/core/memory-engine/dual-root.js +6 -0
  45. package/dist/core/memory-engine/engine.d.ts +329 -15
  46. package/dist/core/memory-engine/engine.js +364 -34
  47. package/dist/core/memory-engine/file-backend.d.ts +30 -0
  48. package/dist/core/memory-engine/file-backend.js +14 -13
  49. package/dist/core/memory-engine/frontmatter.d.ts +22 -1
  50. package/dist/core/memory-engine/frontmatter.js +3 -0
  51. package/dist/core/memory-engine/header-hints.d.ts +5 -0
  52. package/dist/core/memory-engine/index.d.ts +5 -4
  53. package/dist/core/memory-engine/index.js +5 -4
  54. package/dist/core/memory-engine/layout.d.ts +88 -2
  55. package/dist/core/memory-engine/layout.js +112 -3
  56. package/dist/core/memory-engine/provenance-wording.d.ts +7 -0
  57. package/dist/core/memory-engine/provenance-wording.js +3 -0
  58. package/dist/core/memory-engine/tools.d.ts +89 -8
  59. package/dist/core/memory-engine/tools.js +263 -22
  60. package/dist/core/memory-engine/types.d.ts +80 -1
  61. package/dist/core/memory-recall.d.ts +6 -0
  62. package/dist/core/memory.d.ts +27 -1
  63. package/dist/core/memory.js +16 -2
  64. package/dist/core/permission-rule-consent.d.ts +20 -0
  65. package/dist/core/permission-rule-consent.js +12 -3
  66. package/dist/core/permission-rule-model.d.ts +67 -7
  67. package/dist/core/permission-rule-model.js +53 -7
  68. package/dist/core/permission-rule-store.js +15 -10
  69. package/dist/core/permission-rule-sync.js +15 -11
  70. package/dist/core/remote-env.d.ts +3 -3
  71. package/dist/core/retention-policy.d.ts +9 -0
  72. package/dist/core/retention-policy.js +5 -2
  73. package/dist/core/retention.d.ts +13 -2
  74. package/dist/core/runner/assemble-result.d.ts +19 -1
  75. package/dist/core/runner/assemble-result.js +17 -2
  76. package/dist/core/runner/compaction-call-options.d.ts +93 -0
  77. package/dist/core/runner/compaction-call-options.js +3 -0
  78. package/dist/core/runner/memory-capture-optout.d.ts +80 -0
  79. package/dist/core/runner/memory-capture-optout.js +53 -0
  80. package/dist/core/runner/prepare-config-doors.d.ts +5 -0
  81. package/dist/core/runner/prepare-config-doors.js +16 -0
  82. package/dist/core/runner/prepare-hands-readface.d.ts +119 -5
  83. package/dist/core/runner/prepare-hands-readface.js +103 -8
  84. package/dist/core/runner/prepare-memory.d.ts +88 -0
  85. package/dist/core/runner/prepare-memory.js +306 -25
  86. package/dist/core/runner/prepare-task.d.ts +156 -5
  87. package/dist/core/runner/prepare-task.js +488 -98
  88. package/dist/core/runner/runtask.d.ts +27 -20
  89. package/dist/core/runner/runtask.js +283 -99
  90. package/dist/core/runner/session-file-state-replay.d.ts +18 -10
  91. package/dist/core/runner/session-file-state-replay.js +52 -1
  92. package/dist/core/runner/tool-disclosure.js +2 -1
  93. package/dist/core/runner/turn-attachments.d.ts +22 -12
  94. package/dist/core/session-store.d.ts +1 -1
  95. package/dist/core/session-store.js +6 -1
  96. package/dist/core/session.d.ts +34 -1
  97. package/dist/core/store-contracts/file-history-store-contract.d.ts +3 -0
  98. package/dist/core/store-contracts/file-history-store-contract.js +720 -0
  99. package/dist/core/stub-env.d.ts +4 -0
  100. package/dist/core/stub-env.js +1 -0
  101. package/dist/core/task-registry-shared.js +30 -2
  102. package/dist/core/tool-errors.js +1 -0
  103. package/dist/core/tool-policy.d.ts +172 -1
  104. package/dist/core/tool-policy.js +32 -1
  105. package/dist/core/tool-result-store.js +2 -1
  106. package/dist/core/trace.d.ts +24 -0
  107. package/dist/core/types.d.ts +875 -97
  108. package/dist/core/types.js +4 -3
  109. package/dist/core/untrusted-text.d.ts +1 -1
  110. package/dist/core/untrusted-text.js +8 -0
  111. package/dist/core/workflow-run-store-contract.js +17 -0
  112. package/dist/core/workflow-run-store.d.ts +20 -0
  113. package/dist/core/workflow-run-store.js +1 -0
  114. package/dist/engine/compaction/compaction.d.ts +88 -10
  115. package/dist/engine/compaction/compaction.js +109 -30
  116. package/dist/engine/execution-env/node-execution-env.d.ts +9 -1
  117. package/dist/engine/execution-env/node-execution-env.js +28 -0
  118. package/dist/engine/harness/agent-harness.d.ts +52 -1
  119. package/dist/engine/harness/agent-harness.js +36 -1
  120. package/dist/engine/harness/types.d.ts +44 -1
  121. package/dist/engine/llm/types.d.ts +50 -4
  122. package/dist/engine/loop/agent-loop.d.ts +5 -1
  123. package/dist/engine/loop/agent-loop.js +25 -0
  124. package/dist/engine/loop/types.d.ts +19 -0
  125. package/dist/engine/lsp/node-lsp-manager.d.ts +1 -1
  126. package/dist/engine/session/session.js +1 -1
  127. package/dist/index.d.ts +18 -8
  128. package/dist/index.js +14 -6
  129. package/dist/orchestration/run-workflow-tool.d.ts +20 -2
  130. package/dist/orchestration/run-workflow-tool.js +22 -3
  131. package/dist/orchestration/workflow-governance.d.ts +59 -1
  132. package/dist/orchestration/workflow-governance.js +61 -8
  133. package/dist/orchestration/workflow-meta.d.ts +4 -2
  134. package/dist/orchestration/workflow-primitives.js +56 -13
  135. package/dist/orchestration/workflow-types.d.ts +112 -1
  136. package/dist/orchestration/workflow-types.js +2 -2
  137. package/dist/orchestration/workflow.d.ts +20 -0
  138. package/dist/orchestration/workflow.js +182 -14
  139. package/dist/prompt-assembly/event-registry.js +1 -1
  140. package/dist/prompts/default.d.ts +15 -7
  141. package/dist/prompts/default.js +3 -0
  142. package/dist/stores/file/file-history-store.d.ts +368 -0
  143. package/dist/stores/file/file-history-store.js +1248 -0
  144. package/dist/stores/file/index.d.ts +22 -13
  145. package/dist/stores/file/index.js +4 -4
  146. package/dist/stores/file/permission-rule-store.js +1 -0
  147. package/dist/stores/file/strategy-store.d.ts +3 -3
  148. package/dist/tools/fs/bash-readonly-classifier.d.ts +87 -3
  149. package/dist/tools/fs/bash-readonly-classifier.js +106 -4
  150. package/dist/tools/fs/fs-bash.js +9 -5
  151. package/dist/tools/fs/fs-shared.d.ts +52 -1
  152. package/dist/tools/fs/fs-shared.js +14 -0
  153. package/dist/tools/fs/fs-write.d.ts +5 -5
  154. package/dist/tools/fs/fs-write.js +71 -14
  155. package/dist/tools/fs/index.d.ts +6 -1
  156. package/dist/tools/fs/index.js +1 -1
  157. package/dist/tools/web.js +2 -1
  158. package/package.json +5 -1
  159. package/test/export-surface.snapshot.json +159 -23
  160. package/dist/core/file-snapshot-store.d.ts +0 -165
  161. package/dist/core/file-snapshot-store.js +0 -259
  162. package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +0 -13
  163. package/dist/core/store-contracts/file-snapshot-store-contract.js +0 -134
  164. package/dist/stores/file/file-snapshot-store.d.ts +0 -58
  165. package/dist/stores/file/file-snapshot-store.js +0 -353
@@ -34,21 +34,22 @@ import { toImageContent } from "./image.js";
34
34
  import { OUTPUT_TOOL_NAME, SKILLS_LISTING_PROBE_HEADER, resolveOutputRetries } from "./synthetic-tools.js";
35
35
  import { cacheFamilyOf, usageCostMicroUsd } from "./usage-accounting.js";
36
36
  import { assembleResult, errorCodeOf } from "./assemble-result.js";
37
- import { ATTACHMENT_BYTE_CAP, CHANGED_FILES_MAX, AGENT_LISTING_REMOVED_HEADER, SKILLS_LISTING_DELTA_HEADER, SKILLS_LISTING_REMOVED_HEADER, advanceCadenceClock, agentListingDeltaHeader, attachmentEnvelopeTags, 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";
38
- import { buildWorkingFileAttachments, centerAdoptionOption, emitInputTruncated, forkContextOption } from "./compaction-call-options.js";
39
- import { effectiveDelegationFacts, gatedCallIdOf, prepareTask, resolveCheckpointStore } from "./prepare-task.js";
37
+ import { ATTACHMENT_BYTE_CAP, CHANGED_FILES_MAX, TOTAL_TOKENS_REMINDER_DEFAULT_MODE, AGENT_LISTING_REMOVED_HEADER, SKILLS_LISTING_DELTA_HEADER, SKILLS_LISTING_REMOVED_HEADER, advanceCadenceClock, agentListingDeltaHeader, attachmentEnvelopeTags, 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";
38
+ import { buildWorkingFileAttachments, centerAdoptionOption, contextInstructionFilesOption, emitInputTruncated, forkContextOption } from "./compaction-call-options.js";
39
+ import { effectiveDelegationFacts, gatedCallIdOf, placementValueOrAbsent, prepareTask, resolveCheckpointStore } from "./prepare-task.js";
40
+ import { SessionReadFileStates } from "./prepare-hands-readface.js";
40
41
  import { settleTeardownLeg } from "./teardown-bounded.js";
41
42
  import { TOOL_SEARCH_NAME } from "./tool-disclosure.js";
42
43
  import { hasVerifiableStructureSignal } from "./grounding-signal.js";
43
44
  import { hasDestroy, isIsolated } from "../remote-env.js";
44
45
  import { hasBackgroundShell, sweepBackgroundShells } from "../background-shell.js";
45
- import { cloneObserverInput, formatHookFeedback, hookSeatExpiredError, mintHookInvocationIdentity, resolveHookTimeoutMs, runHookSeat } from "../hooks.js";
46
+ import { cloneObserverInput, formatHookFeedback, hookSeatExpiredError, MAX_HOOK_TIMEOUT_MS, mintHookInvocationIdentity, resolveHookTimeoutMs, runHookSeat } from "../hooks.js";
46
47
  import { buildHumanInputEvent, frameMidTurnUserInput, projectHumanInput } from "../human-input-projection.js";
47
48
  import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY, sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "../untrusted-text.js";
48
49
  import { appendInterruptionMarker, reconcileInterruptedSession } from "../session-reconcile.js";
49
50
  import { RunnerSharedToolResultStore } from "../tool-result-store.js";
50
51
  import { formatDiagnosticsBlock } from "../lsp-diagnostics.js";
51
- import { checkToolPolicyProjection, constraintChainDigest, constraintChainEntryOf, isApprovalSettledBy, refuseOutOfContractDecision, screenApproverAttribution, toolPolicyNameSets } from "../tool-policy.js";
52
+ import { checkToolPolicyProjection, constraintChainDigest, constraintChainEntryOfSuppliedLayer, isApprovalSettledBy, refuseOutOfContractDecision, screenApproverAttribution, toolPolicyNameSets } from "../tool-policy.js";
52
53
  import { defaultTaskRegistry } from "../task-registry.js";
53
54
  import { discloseDroppedPending, isDelegatedAgentTerminal, isSystemInjectionPriority, isTerminalTaskNotification, PendingSessionNotifications, renderTaskNotificationXml, SYSTEM_INJECTION_PRIORITIES, SystemInjectionQueue, taskNotificationDedupKey } from "../task-notification.js";
54
55
  import { ToolDetachHub } from "../tool-detach.js";
@@ -57,7 +58,7 @@ import { workflowSizeGuidelineChangeNotice } from "../../orchestration/workflow-
57
58
  import { structuredFrom, toolOutputFrom } from "./tool-output-projection.js";
58
59
  function createRunState() {
59
60
  return {
60
- telemetry: { cacheFamily: "input-excludes-cached", pricing: { inputPer1M: 0, outputPer1M: 0 }, pricingConfigured: true, unpricedSpend: false, tracer: undefined, taskId: "", taskStart: 0, taskStartMonotonic: 0, cacheBreakReported: false },
61
+ telemetry: { cacheFamily: "input-excludes-cached", pricing: { inputPer1M: 0, outputPer1M: 0 }, pricingConfigured: true, unpricedSpend: false, tracer: undefined, taskId: "", runId: "", taskStart: 0, taskStartMonotonic: 0, cacheBreakReported: false },
61
62
  degrade: { degradeToModel: undefined, degraded: undefined, outputErrorStreak: 0, outputInvalid: false, recordDegraded: () => { } },
62
63
  limits: { turnsExceeded: false, budgetHit: undefined, budgetAxis: undefined, platformTerminal: undefined, outputRetryCap: 0, effectiveMaxTurns: undefined },
63
64
  budget: { remainingMicroUsd: undefined, maxCostMicroUsd: undefined, remainingTokens: undefined, maxTokensWindow: undefined, overBudget: () => undefined, streamCancel: false, callOutputChars: 0, lastStreamBudgetCheck: 0, projectedOverBudget: () => undefined },
@@ -84,6 +85,7 @@ function sameAcceptedSteerInput(a, b) {
84
85
  a.actor?.issuer === b.actor?.issuer);
85
86
  }
86
87
  const MAX_CONSECUTIVE_COMPACTION_FAILURES = 3;
88
+ const RESUME_PREFLIGHT_DEFAULT_TIMEOUT_MS = 10_000;
87
89
  const STOP_HOOK_BLOCK_CAP = 8;
88
90
  const COMPACTION_REGROWTH_FACTOR = 1.5;
89
91
  const COMPACTION_FREED_EPSILON = 256;
@@ -123,8 +125,6 @@ const SUGGESTIONS_DEFAULT_COUNT = 3;
123
125
  const SUGGESTIONS_MAX_COUNT = 8;
124
126
  const SUGGESTIONS_TIMEOUT_MS = 30_000;
125
127
  const SUGGESTIONS_TRANSCRIPT_MESSAGES = 12;
126
- const SNAPSHOT_TIMEOUT_MS = 30_000;
127
- const SNAPSHOT_TOO_LARGE_TTL_MS = 30 * 60_000;
128
128
  const LIMIT_APPROACH_DEFAULT_THRESHOLDS = [0.8, 0.95];
129
129
  const DEFAULT_PRECALL_OUTPUT_TOKENS = 4096;
130
130
  function limitApproachFrames(index, axis, percent, mark) {
@@ -606,7 +606,13 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
606
606
  const mcpRemovals = mcpInstructionsOn && mcpDelta.pendingRemovals.length > 0 ? [...mcpDelta.pendingRemovals] : undefined;
607
607
  const mcpDropped = mcpInstructionsOn && prepared.mcp.droppedTools.length > 0 ? selectMcpDroppedBatch(prepared.mcp.droppedTools) : undefined;
608
608
  const budgetUsdOn = rs.attach.attachmentsCfg?.budgetUsd === true && rs.budget.maxCostMicroUsd !== undefined && rs.turn.lastTurnHadToolCalls;
609
- const totalTokensOn = rs.attach.attachmentsCfg?.totalTokensReminder === true && rs.turn.lastTurnHadToolCalls;
609
+ const totalTokensDeclared = rs.attach.attachmentsCfg?.totalTokensReminder;
610
+ const totalTokensMode = rs.attach.attachmentsCfg?.totalTokensReminderMode ?? TOTAL_TOKENS_REMINDER_DEFAULT_MODE;
611
+ const totalTokensArmedByDefault = totalTokensDeclared === undefined &&
612
+ eventDefaultOn("total_tokens_reminder") &&
613
+ totalTokensMode !== "fixed" &&
614
+ totalTokensMode !== "infinite";
615
+ const totalTokensOn = (totalTokensDeclared === true || totalTokensArmedByDefault) && rs.turn.lastTurnHadToolCalls;
610
616
  const toolSearchReminderOn = rs.attach.attachmentsCfg?.toolSearchReminder === true && prepared.deferredToolNames !== undefined;
611
617
  const undiscoveredTools = toolSearchReminderOn
612
618
  ? [...prepared.deferredToolNames].filter((n) => !prepared.activeTools.has(n)).sort()
@@ -896,6 +902,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
896
902
  overheadTokens: prepared.promptOverheadTokens,
897
903
  ...(prepared.activeTools.size > 0 ? { activeTools: [...prepared.activeTools] } : {}),
898
904
  workingFileAttachments: buildWorkingFileAttachments(spec, prepared),
905
+ ...contextInstructionFilesOption(prepared),
899
906
  ...(prepared.onCompactionApplied ? { onApplied: prepared.onCompactionApplied } : {}),
900
907
  ...runnerHooks.seamCCompactionOptions(prepared),
901
908
  ...gitRestateOption(prepared),
@@ -1164,6 +1171,8 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
1164
1171
  if (event.entryId !== undefined && (m.role === "user" || m.role === "assistant" || m.role === "toolResult")) {
1165
1172
  emitCommitted(event.entryId, m.role, m.role === "toolResult" ? m.toolCallId : undefined);
1166
1173
  }
1174
+ if (event.entryId !== undefined && m.role === "user")
1175
+ prepared.fileHistoryBoundary?.begin(event.entryId);
1167
1176
  if (event.entryId !== undefined && m.role === "user" && prepared.gitStatusRef.pendingReceipt !== undefined) {
1168
1177
  const pr = prepared.gitStatusRef.pendingReceipt;
1169
1178
  const c = m.content;
@@ -1452,9 +1461,11 @@ export class Runner {
1452
1461
  deps;
1453
1462
  sessions;
1454
1463
  sessionLocks = new Map();
1455
- snapshotTooLargeRoots = new Map();
1464
+ sessionReadStates = new SessionReadFileStates();
1456
1465
  pendingSessionNotifications = new PendingSessionNotifications();
1457
1466
  parentConstraintRegistry = new Map();
1467
+ suspendedEnvReaps = new Map();
1468
+ locallyClaimedTokens = new Set();
1458
1469
  static PARENT_CONSTRAINT_REGISTRY_CAP = 1024;
1459
1470
  constructor(deps) {
1460
1471
  this.deps = deps;
@@ -1602,7 +1613,7 @@ export class Runner {
1602
1613
  deliverEngineNotice(this.deps.onNotice, {
1603
1614
  code: "config.models_swapped",
1604
1615
  message: `model catalog swapped: ${Object.keys(next.models).length} model(s), ${tiers ? Object.keys(tiers).length : 0} tier binding(s); in-flight tasks finish on their resolved models, new tasks resolve against the new catalog`,
1605
- detail: { models: Object.keys(next.models).length, tiers: tiers ? Object.keys(tiers).length : 0 },
1616
+ detail: { models: Object.keys(next.models).length, tiers: tiers ? Object.keys(tiers).length : 0, occurrenceId: uuidv7() },
1606
1617
  });
1607
1618
  if (movedEntries.length > 0) {
1608
1619
  const RENDER_CAP = 8;
@@ -1613,7 +1624,7 @@ export class Runner {
1613
1624
  rendered.map((e) => `"${e.modelId}" ${e.from || "(config root)"} → ${e.to || "(config root)"}`).join("; ") +
1614
1625
  (movedEntries.length > rendered.length ? `; +${movedEntries.length - rendered.length} more` : "") +
1615
1626
  " — if the provider changed (not just its domain), update the credential reference in the same step",
1616
- detail: { entries: rendered, total: movedEntries.length },
1627
+ detail: { entries: rendered, total: movedEntries.length, occurrenceId: uuidv7() },
1617
1628
  });
1618
1629
  }
1619
1630
  }
@@ -1626,6 +1637,7 @@ export class Runner {
1626
1637
  "(Note the parameter order: runTaskStream(spec, resume?, internals?) — internals is the THIRD parameter.)");
1627
1638
  }
1628
1639
  const entryActor = spec.actor === undefined ? undefined : snapshotActorAssertion(spec.actor);
1640
+ const entryTracer = { tracer: spec.tracer ?? this.deps.tracer };
1629
1641
  const queue = new PushQueue();
1630
1642
  const detachHub = new ToolDetachHub();
1631
1643
  let resultValue;
@@ -1647,6 +1659,7 @@ export class Runner {
1647
1659
  let steerChain = Promise.resolve();
1648
1660
  const acceptedSteerInputs = new Map();
1649
1661
  const notifyRef = {};
1662
+ const captureOptOutRef = {};
1650
1663
  const manualCompactRef = { requested: false, waiters: [] };
1651
1664
  const drainManualCompactWaiters = (outcome) => {
1652
1665
  manualCompactRef.requested = false;
@@ -1663,8 +1676,11 @@ export class Runner {
1663
1676
  await this.runLocked(spec, queue, (r) => {
1664
1677
  resultValue = r;
1665
1678
  const pcs = internals?.inheritedGate?.parentConstraints;
1679
+ if (resume !== undefined)
1680
+ this.locallyClaimedTokens.delete(resume.cp.token);
1666
1681
  if (resume !== undefined && r.checkpointToken !== resume.cp.token) {
1667
1682
  this.parentConstraintRegistry.delete(resume.cp.token);
1683
+ this.suspendedEnvReaps.delete(resume.cp.token);
1668
1684
  }
1669
1685
  if (r.status === "suspended" && r.checkpointToken !== undefined && pcs !== undefined && pcs.length > 0) {
1670
1686
  if (this.parentConstraintRegistry.size >= Runner.PARENT_CONSTRAINT_REGISTRY_CAP) {
@@ -1681,7 +1697,8 @@ export class Runner {
1681
1697
  publishReady(h);
1682
1698
  }, (s) => {
1683
1699
  reapHandle = s;
1684
- }, manualCompactRef, taskIdRef, resume, { ...(internals ?? {}), detachHub }, notifyRef, entryActor);
1700
+ this.suspendedEnvReaps.set(s.token, s);
1701
+ }, manualCompactRef, taskIdRef, resume, { ...(internals ?? {}), detachHub }, notifyRef, captureOptOutRef, entryActor, entryTracer);
1685
1702
  }
1686
1703
  finally {
1687
1704
  releaseLock?.();
@@ -1698,6 +1715,7 @@ export class Runner {
1698
1715
  const reopenReason = code === "resume.tool_unavailable" ? "tool_unavailable" : "env_failed";
1699
1716
  const reopenable = resume !== undefined && resume.pendingActionStarted !== true && reopenReason !== undefined && !(resumeDecisionWasNegative(resume) && resume.decisionDelivered === true);
1700
1717
  let reopenCommitted = false;
1718
+ let reopenStateUnknown = false;
1701
1719
  if (reopenable && resume?.onEnvRestoreFailed) {
1702
1720
  try {
1703
1721
  await resume.onEnvRestoreFailed(reopenReason);
@@ -1708,15 +1726,26 @@ export class Runner {
1708
1726
  const original = err instanceof Error ? err : new Error(String(err));
1709
1727
  const reopenMsg = reopenErr instanceof Error ? reopenErr.message : String(reopenErr);
1710
1728
  const definitive = errorCodeOf(reopenErr) === "checkpoint.reopen_failed";
1729
+ reopenStateUnknown = !definitive;
1711
1730
  err = Object.assign(new Error(definitive
1712
1731
  ? `${reopenMsg} (original resume failure: ${original.message})`
1713
1732
  : `checkpoint reopen threw mid-flight — state UNKNOWN, confirm via the checkpoint store before retrying: ${reopenMsg} (original resume failure: ${original.message})`, { cause: original }), { code: "checkpoint.reopen_failed" });
1714
1733
  code = "checkpoint.reopen_failed";
1715
1734
  }
1716
1735
  }
1736
+ if (resume)
1737
+ this.locallyClaimedTokens.delete(resume.cp.token);
1717
1738
  if (resume && !reopenCommitted) {
1718
1739
  this.parentConstraintRegistry.delete(resume.cp.token);
1719
1740
  await settleTeardownLeg(() => this.sessions.unpin?.(resume.cp.sessionId), "sessions.unpin (resume-failure leg)", (e) => this.deps.onError?.(e, { phase: "config", sessionId: resume.cp.sessionId }));
1741
+ if (!reopenStateUnknown) {
1742
+ const failedReap = this.suspendedEnvReaps.get(resume.cp.token);
1743
+ this.suspendedEnvReaps.delete(resume.cp.token);
1744
+ if (failedReap?.env !== undefined && hasDestroy(failedReap.env)) {
1745
+ const failedEnv = failedReap.env;
1746
+ await settleTeardownLeg(() => failedEnv.destroy(), "terminal resume-failure env destroy", (e) => this.deps.onError?.(e, { phase: "config", sessionId: resume.cp.sessionId }));
1747
+ }
1748
+ }
1720
1749
  }
1721
1750
  if (resultValue !== undefined) {
1722
1751
  try {
@@ -1740,6 +1769,7 @@ export class Runner {
1740
1769
  resultValue = {
1741
1770
  taskId: taskIdRef.current ?? "unknown",
1742
1771
  sessionId: taskIdRef.sessionId ?? "unknown",
1772
+ ...(taskIdRef.runId !== undefined ? { runId: taskIdRef.runId } : {}),
1743
1773
  status: "failed",
1744
1774
  result: "",
1745
1775
  errorMessage: err instanceof Error ? err.message : String(err),
@@ -1755,10 +1785,11 @@ export class Runner {
1755
1785
  })(),
1756
1786
  stats: { turns: 0, tokens: 0, toolCalls: 0, cachedTokens: 0, costMicroUsd: 0 },
1757
1787
  };
1758
- emitTrace(spec.tracer ?? this.deps.tracer, () => ({
1788
+ emitTrace(entryTracer.tracer, () => ({
1759
1789
  kind: "task.end",
1760
1790
  version: 1,
1761
1791
  taskId: taskIdRef.current ?? "unknown",
1792
+ ...(taskIdRef.runId !== undefined ? { runId: taskIdRef.runId } : {}),
1762
1793
  status: "failed",
1763
1794
  errorCode: code,
1764
1795
  turns: 0,
@@ -1802,9 +1833,13 @@ export class Runner {
1802
1833
  reportErr(err);
1803
1834
  return;
1804
1835
  }
1805
- if (!won)
1836
+ if (!won) {
1837
+ if (!this.locallyClaimedTokens.has(rh.token))
1838
+ this.suspendedEnvReaps.delete(rh.token);
1806
1839
  return;
1840
+ }
1807
1841
  this.parentConstraintRegistry.delete(rh.token);
1842
+ this.suspendedEnvReaps.delete(rh.token);
1808
1843
  if (rh.env && hasDestroy(rh.env)) {
1809
1844
  try {
1810
1845
  await rh.env.destroy();
@@ -1962,6 +1997,7 @@ export class Runner {
1962
1997
  detail: {
1963
1998
  inputId: effectiveInputId,
1964
1999
  sessionId: h.sessionId,
2000
+ runId: h.runId,
1965
2001
  ...(actor?.id !== undefined ? { actorId: actor.id } : {}),
1966
2002
  ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}),
1967
2003
  },
@@ -2042,6 +2078,26 @@ export class Runner {
2042
2078
  }
2043
2079
  notifyRef.inject(payload, priority !== undefined ? { priority } : undefined);
2044
2080
  },
2081
+ optOutMemoryCapture: async (options) => {
2082
+ const reasonIn = options?.reason;
2083
+ if (reasonIn !== undefined && typeof reasonIn !== "string") {
2084
+ const e = new Error(`cannot opt out of memory capture: options.reason must be a string when present`);
2085
+ e.code = "steering.invalid_content";
2086
+ throw e;
2087
+ }
2088
+ const reason = reasonIn;
2089
+ if (resultValue)
2090
+ throw steeringError("the task has already finished");
2091
+ const h = handle ?? (await orTimeout(ready));
2092
+ if (!h)
2093
+ throw steeringError("the task is not running");
2094
+ if (captureOptOutRef.flip === undefined) {
2095
+ const e = new Error(`cannot opt out of memory capture: this run mounted no memory session (no memory spec / no backend / prepare degraded memory-less) — there is nothing to opt out of`);
2096
+ e.code = "memory.capture_optout_unavailable";
2097
+ throw e;
2098
+ }
2099
+ return captureOptOutRef.flip(reason);
2100
+ },
2045
2101
  compact: async (opts) => {
2046
2102
  if (resultValue)
2047
2103
  throw steeringError("the task has already finished");
@@ -2095,6 +2151,60 @@ export class Runner {
2095
2151
  h.abortController.abort();
2096
2152
  void h.harness.abort();
2097
2153
  },
2154
+ halt: async () => {
2155
+ const haltRefused = (msg) => {
2156
+ const e = new Error(`cannot halt: ${msg}`);
2157
+ e.code = "steering.not_running";
2158
+ return e;
2159
+ };
2160
+ if (resultValue)
2161
+ throw haltRefused("the task has already finished");
2162
+ const h = handle ?? (await orTimeout(ready));
2163
+ if (!h)
2164
+ throw haltRefused("the task is not running");
2165
+ const apply = () => {
2166
+ const abortOwnedBeforeHalt = h.abortController.signal.aborted;
2167
+ const receipt = h.harness.halt();
2168
+ if (receipt.accepted && !abortOwnedBeforeHalt)
2169
+ h.loop.userHalted = true;
2170
+ if (receipt.turnCut) {
2171
+ deliverEngineNotice(this.deps.onNotice, {
2172
+ code: "task.turn_interrupted",
2173
+ message: "a bare user halt cut the running turn: in-flight work was cut at a manufactured boundary " +
2174
+ "(finished tool calls keep their real results; never-started ones settle as interrupted) and " +
2175
+ "the run is collecting to a clean, resumable stop — no further model turn will start.",
2176
+ detail: {
2177
+ cause: "user_halt",
2178
+ sessionId: h.sessionId,
2179
+ runId: h.runId,
2180
+ ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}),
2181
+ },
2182
+ });
2183
+ }
2184
+ return { turnCut: receipt.turnCut };
2185
+ };
2186
+ try {
2187
+ if (resultValue !== undefined || h.loop.ended)
2188
+ throw haltRefused("the task is no longer running");
2189
+ return apply();
2190
+ }
2191
+ catch (e) {
2192
+ if (!(e instanceof Error && e.code === "invalid_state"))
2193
+ throw e;
2194
+ }
2195
+ const birthDeadline = Date.now() + READY_TIMEOUT_MS;
2196
+ while (resultValue === undefined && !h.loop.ended && Date.now() < birthDeadline) {
2197
+ try {
2198
+ return apply();
2199
+ }
2200
+ catch (e2) {
2201
+ if (!(e2 instanceof Error && e2.code === "invalid_state"))
2202
+ throw e2;
2203
+ }
2204
+ await new Promise((r) => setTimeout(r, 10));
2205
+ }
2206
+ throw haltRefused("the task is no longer running");
2207
+ },
2098
2208
  destroy: () => (destroyOnce ??= (async () => {
2099
2209
  const h = handle ?? (await orTimeout(ready));
2100
2210
  if (h) {
@@ -2106,7 +2216,7 @@ export class Runner {
2106
2216
  })()),
2107
2217
  };
2108
2218
  }
2109
- async runLocked(spec, queue, setResult, onSuggestions, onReady, onSuspend, manualCompactRef, taskIdRef, resume, internals, notifyRef, entryActor) {
2219
+ async runLocked(spec, queue, setResult, onSuggestions, onReady, onSuspend, manualCompactRef, taskIdRef, resume, internals, notifyRef, captureOptOutRef, entryActor, entryTracer) {
2110
2220
  const prepareResume = resume
2111
2221
  ? {
2112
2222
  leafId: resume.cp.leafId,
@@ -2210,6 +2320,7 @@ export class Runner {
2210
2320
  peerSelfRef,
2211
2321
  peerInboundChainRef,
2212
2322
  detachHub,
2323
+ sessionReadStates: this.sessionReadStates,
2213
2324
  onTaskNotification: (notification, opts) => {
2214
2325
  try {
2215
2326
  upstreamTaskNotification?.(notification, opts);
@@ -2218,7 +2329,7 @@ export class Runner {
2218
2329
  }
2219
2330
  injectTaskNotification(notification, opts);
2220
2331
  },
2221
- }, this);
2332
+ }, this, taskIdRef);
2222
2333
  notificationHarness = prepared.harness;
2223
2334
  notificationSessionId = prepared.sessionId;
2224
2335
  if (taskIdRef)
@@ -2258,7 +2369,7 @@ export class Runner {
2258
2369
  undrainedUserAtEnd = counts;
2259
2370
  return;
2260
2371
  }
2261
- for (const notice of undrainedUserInputNotices(counts, spec.taskId, prepared.sessionId)) {
2372
+ for (const notice of undrainedUserInputNotices(counts, spec.taskId ?? prepared.sessionId, prepared.sessionId, prepared.runId)) {
2262
2373
  deliverEngineNotice(this.deps.onNotice, notice);
2263
2374
  }
2264
2375
  };
@@ -2269,6 +2380,9 @@ export class Runner {
2269
2380
  };
2270
2381
  if (notifyRef)
2271
2382
  notifyRef.inject = injectTaskNotification;
2383
+ if (captureOptOutRef && prepared.memoryEngineSession?.captureOptOut !== undefined) {
2384
+ captureOptOutRef.flip = prepared.memoryEngineSession.captureOptOut.flip;
2385
+ }
2272
2386
  try {
2273
2387
  internals?.onNotifyInjectorReady?.(injectTaskNotification);
2274
2388
  }
@@ -2287,8 +2401,8 @@ export class Runner {
2287
2401
  }
2288
2402
  }
2289
2403
  }
2290
- const loopLatch = { ended: false, userInterrupted: false };
2291
- onReady({ harness: prepared.harness, abortController: prepared.abortController, loop: loopLatch, reminderMark: prepared.reminderMark, sessionId: prepared.sessionId, hookTimeoutMs: prepared.hookTimeoutMs, hookIdentity: prepared.hookIdentity });
2404
+ const loopLatch = { ended: false, userInterrupted: false, userHalted: false };
2405
+ onReady({ harness: prepared.harness, abortController: prepared.abortController, loop: loopLatch, reminderMark: prepared.reminderMark, sessionId: prepared.sessionId, runId: prepared.runId, hookTimeoutMs: prepared.hookTimeoutMs, hookIdentity: prepared.hookIdentity });
2292
2406
  const stats = { turns: 0, tokens: 0, toolCalls: 0, promptTokens: 0, totalInputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, cacheWriteTokensLong: 0, outputTokens: 0, costMicroUsd: 0 };
2293
2407
  prepared.liveSpendRef.get = () => ({ costMicroUsd: stats.costMicroUsd, tokens: stats.tokens, turns: stats.turns, walltimeMs: Math.round(performance.now() - rs.telemetry.taskStartMonotonic) });
2294
2408
  if (resume &&
@@ -2329,8 +2443,9 @@ export class Runner {
2329
2443
  rs.telemetry.cacheBreakReported = false;
2330
2444
  rs.limits.outputRetryCap = resolveOutputRetries(spec.outputRetries);
2331
2445
  rs.limits.effectiveMaxTurns = resolveMaxTurns(spec.limits);
2332
- rs.telemetry.tracer = spec.tracer ?? this.deps.tracer;
2446
+ rs.telemetry.tracer = entryTracer !== undefined ? entryTracer.tracer : (spec.tracer ?? this.deps.tracer);
2333
2447
  rs.telemetry.taskId = runSourceTaskId;
2448
+ rs.telemetry.runId = prepared.runId;
2334
2449
  if (taskIdRef) {
2335
2450
  taskIdRef.current = rs.telemetry.taskId;
2336
2451
  taskIdRef.sessionId = prepared.sessionId;
@@ -2345,7 +2460,7 @@ export class Runner {
2345
2460
  }
2346
2461
  };
2347
2462
  try {
2348
- void Promise.resolve(this.sessions.noteTaskRun?.(prepared.sessionId, rs.telemetry.taskId)).catch(discloseNoteTaskRunFailure);
2463
+ void Promise.resolve(this.sessions.noteTaskRun?.(prepared.sessionId, rs.telemetry.taskId, rs.telemetry.runId)).catch(discloseNoteTaskRunFailure);
2349
2464
  }
2350
2465
  catch (err) {
2351
2466
  discloseNoteTaskRunFailure(err);
@@ -2631,7 +2746,7 @@ export class Runner {
2631
2746
  const writeFamilyOf = (name) => writeFamilyByName.get(name) ?? writeFamilyOfCanonical(name);
2632
2747
  const toolStartAt = new Map();
2633
2748
  const startedToolCallIds = new Set();
2634
- emitTrace(rs.telemetry.tracer, () => ({ kind: "task.start", version: 1, taskId: rs.telemetry.taskId, model: prepared.model.id, engineVersion: engineVersion(), ts: rs.telemetry.taskStart }));
2749
+ emitTrace(rs.telemetry.tracer, () => ({ kind: "task.start", version: 1, taskId: rs.telemetry.taskId, runId: rs.telemetry.runId, model: prepared.model.id, engineVersion: engineVersion(), ts: rs.telemetry.taskStart }));
2635
2750
  emitTrace(rs.telemetry.tracer, () => ({
2636
2751
  kind: "prompt.assembled",
2637
2752
  version: 1,
@@ -2723,7 +2838,9 @@ export class Runner {
2723
2838
  }
2724
2839
  }
2725
2840
  };
2841
+ const ownCommittedTailRef = { current: undefined };
2726
2842
  const emitCommitted = (entryId, role, toolCallId) => {
2843
+ ownCommittedTailRef.current = entryId;
2727
2844
  if (role === "toolResult" && toolCallId !== undefined)
2728
2845
  startedToolCallIds.delete(toolCallId);
2729
2846
  queue.push({ type: "message_committed", entryId, role, ...(toolCallId !== undefined ? { toolCallId } : {}), ...ident() });
@@ -2739,6 +2856,7 @@ export class Runner {
2739
2856
  ...(s.detail !== undefined ? { detail: s.detail } : {}),
2740
2857
  ...(s.retryInSec !== undefined ? { retryInSec: s.retryInSec } : {}),
2741
2858
  ...(s.retryInMs !== undefined ? { retryInMs: s.retryInMs } : {}),
2859
+ ...(s.retryAtMs !== undefined ? { retryAtMs: s.retryAtMs } : {}),
2742
2860
  ...(s.attempt !== undefined ? { attempt: s.attempt } : {}),
2743
2861
  ...(s.maxRetries !== undefined ? { maxRetries: s.maxRetries } : {}),
2744
2862
  ...(s.errClass !== undefined ? { errClass: s.errClass } : {}),
@@ -3056,6 +3174,7 @@ export class Runner {
3056
3174
  ...(prepared.activeTools.size > 0 ? { activeTools: [...prepared.activeTools] } : {}),
3057
3175
  onInputTruncated: emitInputTruncated(rs.telemetry.tracer, rs.telemetry.taskId),
3058
3176
  workingFileAttachments: buildWorkingFileAttachments(spec, prepared),
3177
+ ...contextInstructionFilesOption(prepared),
3059
3178
  ...(prepared.onCompactionApplied ? { onApplied: prepared.onCompactionApplied } : {}),
3060
3179
  ...this.seamCCompactionOptions(prepared),
3061
3180
  ...gitRestateOption(prepared),
@@ -3456,6 +3575,7 @@ export class Runner {
3456
3575
  detail: {
3457
3576
  ...(steer.inputId !== undefined ? { inputId: steer.inputId } : {}),
3458
3577
  sessionId: prepared.sessionId,
3578
+ runId: prepared.runId,
3459
3579
  ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}),
3460
3580
  },
3461
3581
  });
@@ -3836,7 +3956,7 @@ export class Runner {
3836
3956
  steer: Math.max(0, undrainedUserAtEnd.steer - migratedParked.steer),
3837
3957
  followUp: Math.max(0, undrainedUserAtEnd.followUp - migratedParked.followUp),
3838
3958
  };
3839
- for (const notice of undrainedUserInputNotices(remaining, spec.taskId, prepared.sessionId)) {
3959
+ for (const notice of undrainedUserInputNotices(remaining, spec.taskId ?? prepared.sessionId, prepared.sessionId, prepared.runId)) {
3840
3960
  deliverEngineNotice(this.deps.onNotice, notice);
3841
3961
  }
3842
3962
  }
@@ -3844,6 +3964,7 @@ export class Runner {
3844
3964
  emitTrace(rs.telemetry.tracer, () => ({ kind: "compaction.announce_dropped", version: 1, taskId: rs.telemetry.taskId, ts: Date.now() }));
3845
3965
  }
3846
3966
  const comp = await this.finish(spec, prepared, {
3967
+ telemetry: { tracer: rs.telemetry.tracer, taskId: rs.telemetry.taskId },
3847
3968
  skipCompaction: durablyPaused || compactionBreaker.failures >= MAX_CONSECUTIVE_COMPACTION_FAILURES,
3848
3969
  minTokens: rs.counters.compactionFloor,
3849
3970
  brain: compactionBrain,
@@ -3928,10 +4049,12 @@ export class Runner {
3928
4049
  }
3929
4050
  const result = assembleResult(spec, prepared.sessionId, final, stats, {
3930
4051
  threw,
4052
+ runId: prepared.runId,
3931
4053
  model: prepared.model.id,
3932
4054
  unpricedSpend: rs.telemetry.unpricedSpend,
3933
4055
  rewindNotes: prepared.rewindNotes,
3934
4056
  haltedOnUserRejection: prepared.batchHaltRef.current !== undefined,
4057
+ userHalted: loopLatch.userHalted,
3935
4058
  strandedHumanAnswers,
3936
4059
  remoteEnvFailures: prepared.remoteEnvFailures,
3937
4060
  effectiveReadFace: prepared.effectiveReadFace,
@@ -3985,6 +4108,7 @@ export class Runner {
3985
4108
  r.errorMessage = `${r.errorMessage ?? "the resume leg did not complete"} — the approved action never started, and reopening the checkpoint FAILED: its state is unprovable from here. Re-read it before deciding; do NOT issue a fresh approval on the assumption the old one is dead`;
3986
4109
  }
3987
4110
  }
4111
+ prepared.sealReadStateSeat?.(ownCommittedTailRef.current);
3988
4112
  {
3989
4113
  const wss = prepared.workspaceStateSettle;
3990
4114
  if (wss !== undefined && result.status !== "suspended" && result.status !== "needs_review") {
@@ -4073,6 +4197,7 @@ export class Runner {
4073
4197
  kind: "task.end",
4074
4198
  version: 1,
4075
4199
  taskId: rs.telemetry.taskId,
4200
+ runId: rs.telemetry.runId,
4076
4201
  status: result.status,
4077
4202
  errorCode: result.errorCode,
4078
4203
  turns: stats.turns,
@@ -4116,9 +4241,7 @@ export class Runner {
4116
4241
  manualCompactRef.emitMooted = undefined;
4117
4242
  queue.push({ type: "done", result });
4118
4243
  queue.close();
4119
- if (spec.rewindFiles && result.status === "completed") {
4120
- await this.snapshotTurn(prepared);
4121
- }
4244
+ await prepared.fileHistoryBoundary?.settle();
4122
4245
  if (spec.suggestNextPrompts && result.status === "completed") {
4123
4246
  onSuggestions(this.suggestNextPrompts(spec, prepared, result));
4124
4247
  }
@@ -4197,7 +4320,7 @@ export class Runner {
4197
4320
  if (!sameRouteIdentity(model, prepared.model)) {
4198
4321
  const verdict = await adjudicateDerivedRoute({ brain: this.deps.brain, model, getApiKeyAndHeaders: spec.getApiKeyAndHeaders });
4199
4322
  if (verdict !== undefined && !verdict.ok) {
4200
- deliverEngineNotice(this.deps.onNotice, fallbackToPrimaryNotice({ seat: "prompt-suggestions", from: model.id, to: prepared.model.id, verdict, sessionId: prepared.sessionId }));
4323
+ deliverEngineNotice(this.deps.onNotice, fallbackToPrimaryNotice({ seat: "prompt-suggestions", from: model.id, to: prepared.model.id, verdict, sessionId: prepared.sessionId, runId: prepared.runId }));
4201
4324
  model = prepared.model;
4202
4325
  thinking = prepared.thinking;
4203
4326
  }
@@ -4230,71 +4353,6 @@ export class Runner {
4230
4353
  clearTimeout(timer);
4231
4354
  }
4232
4355
  }
4233
- async snapshotTurn(prepared) {
4234
- const store = this.deps.fileSnapshotStore;
4235
- const env = prepared.ownedEnv ?? this.deps.executionEnv;
4236
- if (!store || !env)
4237
- return;
4238
- try {
4239
- const leafId = await prepared.session.getLeafId();
4240
- if (!leafId)
4241
- return;
4242
- const root = prepared.taskRootPath;
4243
- const refusal = this.snapshotTooLargeRoots.get(root);
4244
- if (refusal !== undefined) {
4245
- if (Date.now() - refusal.refusedAt < SNAPSHOT_TOO_LARGE_TTL_MS) {
4246
- if (!refusal.skipAnnounced) {
4247
- refusal.skipAnnounced = true;
4248
- try {
4249
- this.deps.onError?.(new Error(`rewind-files snapshot skipped for this working-tree root: a too_large refusal is still inside its ${SNAPSHOT_TOO_LARGE_TTL_MS / 60_000}-minute cooldown (re-probed at ${new Date(refusal.refusedAt + SNAPSHOT_TOO_LARGE_TTL_MS).toISOString()}) — turns completed during the cooldown capture no snapshot and cannot be rewound to. Announced once per cooldown window.`), { phase: "rewind", sessionId: prepared.sessionId, classification: "too_large" });
4250
- }
4251
- catch {
4252
- }
4253
- }
4254
- return;
4255
- }
4256
- this.snapshotTooLargeRoots.delete(root);
4257
- }
4258
- const ac = new AbortController();
4259
- let timer;
4260
- const timeout = new Promise((resolve) => {
4261
- timer = setTimeout(() => {
4262
- ac.abort();
4263
- resolve({ ok: false, error: { code: "aborted", message: "snapshot timed out" } });
4264
- }, SNAPSHOT_TIMEOUT_MS);
4265
- });
4266
- const work = store.snapshot(prepared.sessionId, leafId, env, root, ac.signal);
4267
- work.catch(() => { });
4268
- let r;
4269
- try {
4270
- r = await Promise.race([work, timeout]);
4271
- }
4272
- finally {
4273
- clearTimeout(timer);
4274
- }
4275
- if (!r.ok) {
4276
- const tooLarge = r.error.code === "too_large";
4277
- const refusedAt = Date.now();
4278
- if (tooLarge && !this.snapshotTooLargeRoots.has(root))
4279
- this.snapshotTooLargeRoots.set(root, { refusedAt, skipAnnounced: false });
4280
- try {
4281
- this.deps.onError?.(new Error(`rewind-files snapshot failed (${r.error.code}): ${r.error.message}` +
4282
- (tooLarge
4283
- ? ` — skipping further snapshots for this root (this process) during a ${SNAPSHOT_TOO_LARGE_TTL_MS / 60_000}-minute cooldown, until ${new Date(refusedAt + SNAPSHOT_TOO_LARGE_TTL_MS).toISOString()}, when the tree is re-probed automatically. Raise the store's snapshotBounds to accept a tree this size; shrinking the tree also re-enables rewind, but only at that re-probe — not immediately`
4284
- : "")), { phase: "rewind", sessionId: prepared.sessionId, ...(tooLarge ? { classification: "too_large" } : {}) });
4285
- }
4286
- catch {
4287
- }
4288
- }
4289
- }
4290
- catch (err) {
4291
- try {
4292
- this.deps.onError?.(err, { phase: "rewind", sessionId: prepared.sessionId });
4293
- }
4294
- catch {
4295
- }
4296
- }
4297
- }
4298
4356
  async runTask(spec, internals) {
4299
4357
  const stream = this.runTaskStream(spec, undefined, internals);
4300
4358
  for await (const _ of stream) {
@@ -4314,6 +4372,7 @@ export class Runner {
4314
4372
  throw new CheckpointError("checkpoint.invalid_outcome", `resume outcome must be an object naming its gate (got ${outcome === null ? "null" : typeof outcome})`);
4315
4373
  }
4316
4374
  const config = typeof taskConfig === "object" && taskConfig !== null ? { ...taskConfig } : taskConfig;
4375
+ internals = internals !== undefined ? { ...internals } : undefined;
4317
4376
  const store = resolveCheckpointStore(config, this.deps);
4318
4377
  if (!store) {
4319
4378
  throw new CheckpointError("checkpoint.not_found", "no CheckpointStore wired — cannot resume (set RunnerDeps.checkpointStore or taskConfig.checkpointStore)");
@@ -4663,12 +4722,13 @@ export class Runner {
4663
4722
  "(corrupt / inconsistently-minted row) — the frozen deny data the resume would execute is not the data the " +
4664
4723
  "digest authenticates; rejected pre-CAS (the checkpoint stays pending)");
4665
4724
  }
4666
- const suppliedDigest = constraintChainDigest((internals?.inheritedGate?.parentConstraints ?? []).map((pc) => constraintChainEntryOf(pc.policy, { ...(pc.autoMode !== undefined ? { autoModeArmed: true } : {}), ...(pc.durableMandate === true ? { durableMandate: true } : {}), ...(pc.contentMandate === true ? { contentMandate: true } : {}) })));
4725
+ const suppliedDigest = constraintChainDigest((internals?.inheritedGate?.parentConstraints ?? []).map((pc, i) => constraintChainEntryOfSuppliedLayer(pc, persistedChain[i])));
4667
4726
  if (suppliedDigest !== expectedDigest) {
4668
4727
  throw new CheckpointError("resume.parent_constraint_mismatch", "the re-supplied parent-constraint chain's projection content does not match what the checkpoint recorded " +
4669
4728
  `(recorded ${expectedDigest}, re-supplied ${suppliedDigest}) — a same-shape chain with different frozen ` +
4670
4729
  "deny data would run the resumed leg under a different ancestor gate than it suspended with; " +
4671
- "rejected pre-CAS (the checkpoint stays pending) — re-resume with the original chain's policies");
4730
+ "rejected pre-CAS (the checkpoint stays pending) — re-resume with the original chain's policies " +
4731
+ "(including, when the row recorded one, the ancestor's auto-mode arming recipe verbatim)");
4672
4732
  }
4673
4733
  }
4674
4734
  }
@@ -4694,6 +4754,15 @@ export class Runner {
4694
4754
  "running the resumed leg under a different identity would move its usage-ledger bucket, scope derivation and attribution; " +
4695
4755
  "refused pre-CAS (the checkpoint stays pending): re-resume with the original principal, or omit the field to inherit the recorded one");
4696
4756
  }
4757
+ {
4758
+ const recordedPlacementRoot = placementValueOrAbsent(cp.state.placementRootSessionId);
4759
+ const suppliedPlacementRoot = placementValueOrAbsent(internals?.placementRoot);
4760
+ if (recordedPlacementRoot !== undefined && suppliedPlacementRoot !== undefined && suppliedPlacementRoot !== recordedPlacementRoot) {
4761
+ throw new CheckpointError("resume.placement_mismatch", `the resume supplied explicit placement root ${JSON.stringify(suppliedPlacementRoot)} but this checkpoint recorded placement root ${JSON.stringify(recordedPlacementRoot)} at suspend — ` +
4762
+ "resuming under a different placement fixed point would re-place the leg (and its descendants) on another target; " +
4763
+ "refused pre-CAS (the checkpoint stays pending): re-resume with the recorded root, or omit internals.placementRoot to inherit it");
4764
+ }
4765
+ }
4697
4766
  const owesDelivery = outcomeGate !== "resource_limit" ||
4698
4767
  readPendingSteerQueue(cp.state).length > 0 ||
4699
4768
  (cp.state.runningBackgroundTasks?.length ?? 0) > 0;
@@ -4869,7 +4938,121 @@ export class Runner {
4869
4938
  if (outcomeForStore === undefined) {
4870
4939
  throw new CheckpointError("checkpoint.invalid_outcome", `resume outcome (gate "${describeSuppliedValue(outcomeGate)}") matched the checkpoint gate but no lane captured it — refusing pre-CAS rather than passing the caller's live object to the store and the resumed run`);
4871
4940
  }
4941
+ if (this.deps.resumePreflight !== undefined) {
4942
+ const preflight = this.deps.resumePreflight;
4943
+ const rawDeadline = this.deps.resumePreflightTimeoutMs;
4944
+ let preflightTimeoutMs = RESUME_PREFLIGHT_DEFAULT_TIMEOUT_MS;
4945
+ if (rawDeadline !== undefined) {
4946
+ if (typeof rawDeadline === "number" && Number.isFinite(rawDeadline) && rawDeadline > 0 && rawDeadline <= MAX_HOOK_TIMEOUT_MS) {
4947
+ preflightTimeoutMs = rawDeadline;
4948
+ }
4949
+ else {
4950
+ try {
4951
+ this.deps.onError?.(new Error(`RunnerDeps.resumePreflightTimeoutMs is not a positive finite number within the seat ceiling of ${MAX_HOOK_TIMEOUT_MS}ms (got ${describeSuppliedValue(rawDeadline)}) — the resume preflight falls back to its own ${RESUME_PREFLIGHT_DEFAULT_TIMEOUT_MS}ms default`), { phase: "config", sessionId: cp.sessionId });
4952
+ }
4953
+ catch {
4954
+ }
4955
+ }
4956
+ }
4957
+ const info = {
4958
+ token,
4959
+ sessionId: cp.sessionId,
4960
+ ...(cp.state.placementRootSessionId !== undefined ? { placementRootSessionId: cp.state.placementRootSessionId } : {}),
4961
+ ...(cp.principal ? { principal: cp.principal } : {}),
4962
+ ...(cp.state.workspaceHandle !== undefined ? { workspaceHandle: structuredClone(cp.state.workspaceHandle) } : {}),
4963
+ gateKind: cp.gate.kind,
4964
+ };
4965
+ const refusalTail = "; the checkpoint stays pending and the same token is redeemable once the obstacle clears";
4966
+ let verdict;
4967
+ try {
4968
+ const seat = await runHookSeat("resumePreflight", {
4969
+ timeoutMs: preflightTimeoutMs,
4970
+ ...(resumeSignal !== undefined ? { signal: resumeSignal } : {}),
4971
+ abortEnds: true,
4972
+ onBadTimeout: (badErr) => {
4973
+ try {
4974
+ this.deps.onError?.(badErr instanceof Error ? badErr : new Error(String(badErr)), { phase: "hook", sessionId: cp.sessionId });
4975
+ }
4976
+ catch {
4977
+ }
4978
+ },
4979
+ owner: this.deps,
4980
+ }, (sig) => preflight(info, sig));
4981
+ if (seat.expired) {
4982
+ throw new CheckpointError("resume.preflight_rejected", seat.cause === "timeout"
4983
+ ? `the deployment's resumePreflight did not answer within its ${preflightTimeoutMs}ms deadline while screening this resume — refused fail-closed (retry-later arm)${refusalTail}`
4984
+ : `the resume was cancelled while the deployment's resumePreflight was still screening it — refused (retry-later arm)${refusalTail}`);
4985
+ }
4986
+ verdict = seat.value;
4987
+ }
4988
+ catch (preflightErr) {
4989
+ if (preflightErr instanceof CheckpointError)
4990
+ throw preflightErr;
4991
+ const err = preflightErr instanceof Error ? preflightErr : new Error(String(preflightErr));
4992
+ try {
4993
+ this.deps.onError?.(err, { phase: "hook", sessionId: cp.sessionId });
4994
+ }
4995
+ catch {
4996
+ }
4997
+ throw new CheckpointError("resume.preflight_rejected", `the deployment's resumePreflight crashed while screening this resume (${inlineUntrusted(err.message)}) — refused fail-closed (retry-later arm)${refusalTail}`);
4998
+ }
4999
+ const verdictOk = verdict !== null && typeof verdict === "object" ? verdict.ok : undefined;
5000
+ if (verdictOk !== true) {
5001
+ const bag = verdict !== null && typeof verdict === "object" ? verdict : undefined;
5002
+ const disposition = bag?.disposition;
5003
+ const suppliedMessage = bag?.message;
5004
+ const retryAfterMs = bag?.retryAfterMs;
5005
+ const readableMessage = typeof suppliedMessage === "string" && suppliedMessage !== "" ? suppliedMessage : undefined;
5006
+ const said = readableMessage !== undefined ? `: ${inlineUntrusted(readableMessage)}` : " (no readable verdict — fail-closed)";
5007
+ if (disposition === "terminal" && readableMessage !== undefined) {
5008
+ this.locallyClaimedTokens.add(token);
5009
+ let settled;
5010
+ try {
5011
+ settled = await store.expire(token, cp.scope);
5012
+ }
5013
+ catch (expireErr) {
5014
+ this.locallyClaimedTokens.delete(token);
5015
+ throw expireErr;
5016
+ }
5017
+ if (!settled) {
5018
+ this.locallyClaimedTokens.delete(token);
5019
+ const live = await store.get(token);
5020
+ if (live?.status === "pending") {
5021
+ throw new CheckpointError("resume.preflight_rejected", `the deployment's resumePreflight ruled this resume PERMANENTLY blocked${said}, but the row moved under this refusal (a concurrent resolve/reopen cycle) — nothing was settled here; the row is pending again, re-resume against the current state (the preflight screens every attempt)`);
5022
+ }
5023
+ throw new CheckpointError("resume.preflight_rejected", `the deployment's resumePreflight ruled this resume PERMANENTLY blocked${said} — a concurrent actor had already settled the row (${describeSuppliedValue(live?.status ?? "consumed")}); nothing was settled by this refusal`);
5024
+ }
5025
+ this.parentConstraintRegistry.delete(token);
5026
+ const terminalReap = this.suspendedEnvReaps.get(token);
5027
+ this.suspendedEnvReaps.delete(token);
5028
+ this.locallyClaimedTokens.delete(token);
5029
+ if (terminalReap?.env !== undefined && hasDestroy(terminalReap.env)) {
5030
+ const terminalEnv = terminalReap.env;
5031
+ await settleTeardownLeg(() => terminalEnv.destroy(), "terminal-preflight env destroy", (envErr) => {
5032
+ try {
5033
+ this.deps.onError?.(envErr, { phase: "config", sessionId: terminalReap.sessionId });
5034
+ }
5035
+ catch {
5036
+ }
5037
+ });
5038
+ }
5039
+ try {
5040
+ this.deps.onError?.(new Error(`resumePreflight ruled this row PERMANENTLY blocked${said} — the row was settled terminally (expired) by this refusal's single-shot CAS (token no longer redeemable)`), { phase: "hook", sessionId: cp.sessionId });
5041
+ }
5042
+ catch {
5043
+ }
5044
+ throw new CheckpointError("resume.preflight_rejected", `the deployment's resumePreflight ruled this resume PERMANENTLY blocked${said} — the row was settled terminally (expired by this refusal's single-shot CAS); the token is not redeemable`);
5045
+ }
5046
+ const waitHint = typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs >= 0 ? retryAfterMs : undefined;
5047
+ throw new CheckpointError("resume.preflight_rejected", `the deployment's resumePreflight refused this resume${said}${refusalTail}${waitHint !== undefined ? ` (deployment wait hint: ${String(waitHint)}ms)` : ""}`, waitHint !== undefined ? { retryAfterMs: waitHint } : undefined);
5048
+ }
5049
+ if (recheckGovernanceWindow !== undefined)
5050
+ await recheckGovernanceWindow();
5051
+ }
5052
+ this.locallyClaimedTokens.add(token);
4872
5053
  const won = await store.resolve(token, cp.scope, outcomeForStore, { rev: cp.rev ?? 0 });
5054
+ if (!won)
5055
+ this.locallyClaimedTokens.delete(token);
4873
5056
  if (!won) {
4874
5057
  const live = await store.get(token);
4875
5058
  if (live?.status === "pending") {
@@ -5223,9 +5406,10 @@ export class Runner {
5223
5406
  minTokens: opts?.minTokens,
5224
5407
  overheadTokens: prepared.promptOverheadTokens,
5225
5408
  ...(prepared.activeTools.size > 0 ? { activeTools: [...prepared.activeTools] } : {}),
5226
- onInputTruncated: emitInputTruncated(spec.tracer ?? this.deps.tracer, spec.taskId ?? prepared.sessionId),
5409
+ onInputTruncated: emitInputTruncated(opts?.telemetry?.tracer ?? spec.tracer ?? this.deps.tracer, opts?.telemetry?.taskId ?? spec.taskId ?? prepared.sessionId),
5227
5410
  ...(opts?.windowSafety ?? {}),
5228
5411
  workingFileAttachments: buildWorkingFileAttachments(spec, prepared),
5412
+ ...contextInstructionFilesOption(prepared),
5229
5413
  ...(prepared.onCompactionApplied ? { onApplied: prepared.onCompactionApplied } : {}),
5230
5414
  ...this.seamCCompactionOptions(prepared),
5231
5415
  ...gitRestateOption(prepared),
@@ -5236,17 +5420,17 @@ export class Runner {
5236
5420
  });
5237
5421
  this.recordCompactionReuse(prepared, finishComp);
5238
5422
  if (finishComp.unevaluableWindow) {
5239
- discloseUnevaluableWindow(finishComp.unevaluableWindow, prepared, spec.tracer ?? this.deps.tracer, spec.taskId ?? prepared.sessionId, this.deps.onError);
5423
+ discloseUnevaluableWindow(finishComp.unevaluableWindow, prepared, opts?.telemetry?.tracer ?? spec.tracer ?? this.deps.tracer, opts?.telemetry?.taskId ?? spec.taskId ?? prepared.sessionId, this.deps.onError);
5240
5424
  }
5241
5425
  comp = finishComp;
5242
5426
  }
5243
5427
  catch (err) {
5244
5428
  const failMsg = String(err instanceof Error ? err.message : err);
5245
5429
  const failReason = failMsg.length > 512 ? `${failMsg.slice(0, 512)}…` : failMsg;
5246
- emitTrace(spec.tracer ?? this.deps.tracer, () => ({
5430
+ emitTrace(opts?.telemetry?.tracer ?? spec.tracer ?? this.deps.tracer, () => ({
5247
5431
  kind: "compaction.failed",
5248
5432
  version: 1,
5249
- taskId: spec.taskId ?? prepared.sessionId,
5433
+ taskId: opts?.telemetry?.taskId ?? spec.taskId ?? prepared.sessionId,
5250
5434
  trigger: "auto",
5251
5435
  reason: failReason,
5252
5436
  ts: Date.now(),