@sema-agent/core 7.7.0 → 7.9.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 (186) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/dist/agents/agent-transcript-tool.js +2 -2
  3. package/dist/agents/list-agents-tool.js +2 -3
  4. package/dist/agents/observer.js +2 -2
  5. package/dist/agents/send-message-tool.js +2 -1
  6. package/dist/agents/subagent-steps.js +5 -4
  7. package/dist/agents/subagent.d.ts +1 -1
  8. package/dist/agents/subagent.js +2 -3
  9. package/dist/core/ask-origin.d.ts +12 -1
  10. package/dist/core/ask-origin.js +5 -1
  11. package/dist/core/ask-question.js +2 -2
  12. package/dist/core/checkpoint-store.d.ts +64 -24
  13. package/dist/core/context-edit.d.ts +0 -22
  14. package/dist/core/context-edit.js +2 -11
  15. package/dist/core/engine-notice.d.ts +6 -0
  16. package/dist/core/fs-write-gate-policy.d.ts +7 -1
  17. package/dist/core/fs-write-gate-policy.js +7 -7
  18. package/dist/core/gate-fold.js +2 -2
  19. package/dist/core/gate-lanes.js +53 -26
  20. package/dist/core/gate-outcome.d.ts +8 -4
  21. package/dist/core/gate-outcome.js +2 -1
  22. package/dist/core/governance-codes.d.ts +1 -1
  23. package/dist/core/governance-codes.js +6 -0
  24. package/dist/core/hooks.d.ts +64 -33
  25. package/dist/core/hooks.js +1 -1
  26. package/dist/core/lsp.js +2 -2
  27. package/dist/core/mcp-server-spec.d.ts +17 -0
  28. package/dist/core/mcp.js +3 -1
  29. package/dist/core/memory-engine/tools.js +4 -12
  30. package/dist/core/permission-rule-consent.d.ts +29 -36
  31. package/dist/core/permission-rule-consent.js +110 -60
  32. package/dist/core/permission-rule-model.d.ts +254 -55
  33. package/dist/core/permission-rule-model.js +323 -43
  34. package/dist/core/permission-rule-org.d.ts +9 -8
  35. package/dist/core/permission-rule-org.js +6 -15
  36. package/dist/core/permission-rule-provider.d.ts +10 -6
  37. package/dist/core/permission-rule-provider.js +12 -8
  38. package/dist/core/permission-rule-session.d.ts +7 -6
  39. package/dist/core/permission-rule-session.js +29 -10
  40. package/dist/core/permission-rule-store.d.ts +40 -18
  41. package/dist/core/permission-rule-store.js +68 -42
  42. package/dist/core/permission-rule-sync.d.ts +9 -3
  43. package/dist/core/permission-rule-sync.js +29 -25
  44. package/dist/core/permission-rule-syntax.d.ts +30 -0
  45. package/dist/core/permission-rule-syntax.js +44 -0
  46. package/dist/core/permission-rules.d.ts +85 -33
  47. package/dist/core/permission-rules.js +104 -88
  48. package/dist/core/persisted-rule-arms.d.ts +56 -0
  49. package/dist/core/persisted-rule-arms.js +48 -0
  50. package/dist/core/present-plan-tool.js +3 -4
  51. package/dist/core/runner/active-skill-scope.js +7 -4
  52. package/dist/core/runner/compaction-call-options.d.ts +1 -1
  53. package/dist/core/runner/contracts.d.ts +46 -3
  54. package/dist/core/runner/permission-rule-lanes.d.ts +33 -26
  55. package/dist/core/runner/permission-rule-lanes.js +27 -21
  56. package/dist/core/runner/prepare-ask-lane.d.ts +1 -1
  57. package/dist/core/runner/prepare-caps-and-workflow.d.ts +5 -4
  58. package/dist/core/runner/prepare-caps-and-workflow.js +16 -11
  59. package/dist/core/runner/prepare-defer-classify.d.ts +5 -26
  60. package/dist/core/runner/prepare-defer-classify.js +10 -10
  61. package/dist/core/runner/prepare-delegation-surface.d.ts +5 -4
  62. package/dist/core/runner/prepare-delegation-surface.js +15 -17
  63. package/dist/core/runner/prepare-gate-stations.d.ts +4 -1
  64. package/dist/core/runner/prepare-gate-stations.js +4 -2
  65. package/dist/core/runner/prepare-hands-readface.d.ts +3 -2
  66. package/dist/core/runner/prepare-hands-readface.js +3 -2
  67. package/dist/core/runner/prepare-inherited-gate.d.ts +1 -1
  68. package/dist/core/runner/prepare-inherited-gate.js +1 -0
  69. package/dist/core/runner/prepare-lsp.d.ts +4 -3
  70. package/dist/core/runner/prepare-lsp.js +3 -2
  71. package/dist/core/runner/prepare-memory-engine-session.d.ts +4 -4
  72. package/dist/core/runner/prepare-memory-engine-session.js +4 -2
  73. package/dist/core/runner/prepare-offload-wrappers.js +7 -5
  74. package/dist/core/runner/prepare-park-ask.d.ts +4 -0
  75. package/dist/core/runner/prepare-park-ask.js +4 -1
  76. package/dist/core/runner/prepare-project-context.d.ts +5 -23
  77. package/dist/core/runner/prepare-project-context.js +7 -5
  78. package/dist/core/runner/prepare-prompt-assembly.d.ts +1 -1
  79. package/dist/core/runner/prepare-protocol-tools.d.ts +5 -1
  80. package/dist/core/runner/prepare-protocol-tools.js +38 -21
  81. package/dist/core/runner/prepare-question-face.d.ts +4 -3
  82. package/dist/core/runner/prepare-question-face.js +5 -3
  83. package/dist/core/runner/prepare-run-refs.d.ts +16 -0
  84. package/dist/core/runner/prepare-run-refs.js +6 -0
  85. package/dist/core/runner/prepare-safety-scan.js +48 -2
  86. package/dist/core/runner/prepare-task.js +37 -19
  87. package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +5 -2
  88. package/dist/core/runner/prepare-tool-disclosure-mount.js +3 -1
  89. package/dist/core/runner/prepare-wiring-manifest.d.ts +4 -0
  90. package/dist/core/runner/prepare-wiring-manifest.js +3 -8
  91. package/dist/core/runner/resume-admission.d.ts +53 -0
  92. package/dist/core/runner/resume-admission.js +83 -0
  93. package/dist/core/runner/resume-apply.d.ts +50 -0
  94. package/dist/core/runner/resume-apply.js +184 -0
  95. package/dist/core/runner/resume-checkpoint-screen.d.ts +18 -0
  96. package/dist/core/runner/resume-checkpoint-screen.js +108 -0
  97. package/dist/core/runner/resume-claim.d.ts +32 -0
  98. package/dist/core/runner/resume-claim.js +27 -0
  99. package/dist/core/runner/resume-internals-and-config.d.ts +33 -0
  100. package/dist/core/runner/resume-internals-and-config.js +50 -0
  101. package/dist/core/runner/resume-policy-outcome.d.ts +31 -0
  102. package/dist/core/runner/resume-policy-outcome.js +127 -0
  103. package/dist/core/runner/resume-preflight.d.ts +40 -0
  104. package/dist/core/runner/resume-preflight.js +122 -0
  105. package/dist/core/runner/resume-review-outcome.d.ts +30 -0
  106. package/dist/core/runner/resume-review-outcome.js +88 -0
  107. package/dist/core/runner/run-harness-handlers.d.ts +46 -0
  108. package/dist/core/runner/run-harness-handlers.js +345 -0
  109. package/dist/core/runner/run-turn-boundary.d.ts +69 -0
  110. package/dist/core/runner/run-turn-boundary.js +694 -0
  111. package/dist/core/runner/runtask.d.ts +20 -0
  112. package/dist/core/runner/runtask.js +129 -1774
  113. package/dist/core/runner/session-rule-policy.js +9 -4
  114. package/dist/core/runner/synthetic-tools.js +5 -8
  115. package/dist/core/runner/tool-disclosure.d.ts +4 -0
  116. package/dist/core/runner/tool-disclosure.js +9 -5
  117. package/dist/core/runner/tool-end-body.d.ts +12 -5
  118. package/dist/core/runner/tool-end-body.js +8 -7
  119. package/dist/core/runner/tool-face-overlay.d.ts +40 -0
  120. package/dist/core/runner/tool-face-overlay.js +125 -0
  121. package/dist/core/runner/tool-output-projection.d.ts +14 -1
  122. package/dist/core/runner/tool-output-projection.js +24 -18
  123. package/dist/core/runner/turn-attachments.d.ts +2 -2
  124. package/dist/core/sensitive-path-policy.d.ts +3 -0
  125. package/dist/core/sensitive-path-policy.js +6 -5
  126. package/dist/core/shared-memory/tools.js +3 -6
  127. package/dist/core/store-contracts/checkpoint-store-contract.js +23 -0
  128. package/dist/core/store-contracts/permission-rule-sync-contract.js +29 -10
  129. package/dist/core/task-event.d.ts +10 -0
  130. package/dist/core/task-registry.js +4 -5
  131. package/dist/core/task-spec.d.ts +8 -0
  132. package/dist/core/tool-catalog-entries.d.ts +48 -0
  133. package/dist/core/tool-catalog-entries.js +174 -0
  134. package/dist/core/tool-conformance.d.ts +22 -0
  135. package/dist/core/tool-conformance.js +37 -0
  136. package/dist/core/tool-face.d.ts +171 -0
  137. package/dist/core/tool-face.js +25 -0
  138. package/dist/core/tool-policy.d.ts +24 -0
  139. package/dist/core/tool-policy.js +4 -2
  140. package/dist/core/tool-registry.d.ts +114 -0
  141. package/dist/core/tool-registry.js +114 -0
  142. package/dist/core/tool-result-budget.d.ts +0 -3
  143. package/dist/core/tool-result-budget.js +4 -3
  144. package/dist/core/tool-result-store.d.ts +0 -18
  145. package/dist/core/tool-result-store.js +2 -18
  146. package/dist/core/tool-roster.d.ts +459 -0
  147. package/dist/core/tool-roster.js +507 -0
  148. package/dist/core/tool-spec.d.ts +54 -3
  149. package/dist/core/tools.js +7 -0
  150. package/dist/core/wiring-manifest.d.ts +13 -0
  151. package/dist/core/wiring-manifest.js +2 -1
  152. package/dist/core/write-protect.d.ts +2 -1
  153. package/dist/core/write-protect.js +5 -4
  154. package/dist/engine/compaction/utils.d.ts +6 -0
  155. package/dist/engine/compaction/utils.js +19 -19
  156. package/dist/engine/loop/types.d.ts +6 -0
  157. package/dist/index.d.ts +17 -6
  158. package/dist/index.js +17 -6
  159. package/dist/orchestration/goal.js +2 -1
  160. package/dist/orchestration/run-spec.js +3 -5
  161. package/dist/orchestration/run-workflow-tool.js +2 -2
  162. package/dist/prompt-assembly/event-registry.js +3 -3
  163. package/dist/prompt-assembly/tool-catalog.d.ts +2 -2
  164. package/dist/stores/file/checkpoint-store.js +1 -1
  165. package/dist/stores/file/permission-rule-store.d.ts +28 -23
  166. package/dist/stores/file/permission-rule-store.js +64 -16
  167. package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -1
  168. package/dist/tools/fs/fs-bash.d.ts +1 -1
  169. package/dist/tools/fs/fs-bash.js +7 -11
  170. package/dist/tools/fs/fs-pdf.d.ts +1 -1
  171. package/dist/tools/fs/fs-read.js +2 -2
  172. package/dist/tools/fs/fs-search-tools.d.ts +0 -7
  173. package/dist/tools/fs/fs-search-tools.js +3 -16
  174. package/dist/tools/fs/fs-write.js +4 -6
  175. package/dist/tools/fs/notebook.d.ts +1 -1
  176. package/dist/tools/fs/repo-map.js +2 -2
  177. package/dist/tools/fs/safety.d.ts +3 -2
  178. package/dist/tools/fs/safety.js +6 -5
  179. package/dist/tools/monitor.js +2 -2
  180. package/dist/tools/scheduler-tools.js +5 -8
  181. package/dist/tools/task-list.js +5 -8
  182. package/dist/tools/todo.js +2 -2
  183. package/dist/tools/web.js +3 -6
  184. package/dist/tools/worktree.js +3 -4
  185. package/package.json +4 -1
  186. package/test/export-surface.snapshot.json +189 -17
@@ -1,30 +1,26 @@
1
- import { persistedReadDenyEntryProblem } from "../../tools/fs/read-deny.js";
2
1
  import { createSafeNotifier, observeThenableRejection } from "../safe-notify.js";
3
2
  import { mintSystemReminder, openSystemReminder } from "../reminder-mint.js";
4
- import { GLOBAL_USAGE_KEY, resolveUsageWindows, usageRetryAfterMs } from "../usage-window-store.js";
5
3
  import { deliverDelegationLifecycle, deliverEngineNotice, undrainedUserInputNotices } from "../types.js";
6
4
  import { planRejectionClears, resolveTriggerWindow } from "../context-edit.js";
7
5
  import { AgentHarness, DEFAULT_COMPACTION_SETTINGS, isSyntheticApiErrorMessage, uuidv7 } from "../../internal/harness.js";
8
6
  import { snapshotActorAssertion } from "../../internal/llm.js";
9
- import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, F012_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, FACE_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, resolveCheckpointStore, LEGACY_PENDING_STEER_INPUT_ID, MAX_STEER_INPUT_ID_CHARS, validatePendingSteer, winnerFromOutcome, realApprovalOrgFact } from "../checkpoint-store.js";
7
+ import { CheckpointError, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, resolveCheckpointStore, LEGACY_PENDING_STEER_INPUT_ID, MAX_STEER_INPUT_ID_CHARS, realApprovalOrgFact } from "../checkpoint-store.js";
10
8
  import { GIT_STATUS_ECHO_PREVIEW, stripGitStatusUnits } from "./git-status-frame.js";
11
9
  import { settleExecutionRecord } from "./execution-record.js";
12
10
  import { engineVersion } from "../version.js";
13
11
  import { CONFIG_CATALOG_VERSION, declarationReasons, resolveEffectiveConfig } from "../../config/catalog.js";
14
12
  import { eventDefaultOn } from "../../prompt-assembly/event-registry.js";
15
- import { DEFAULT_COMPACTION_INSTRUCTIONS, createRapidRefillState, isCompactionManualCancel, maybeCompact, nextTrimForceBackoff, recordCompactionAndCheckRapidRefill, sanitizeCompactionSettings } from "../auto-compaction.js";
16
- import { ASK_USER_QUESTION_TOOL_NAME, canonicalizeCapturedPlainData, classifyQuestionOutcome, isLiveQuestionFace } from "../ask-question.js";
13
+ import { DEFAULT_COMPACTION_INSTRUCTIONS, createRapidRefillState, maybeCompact, sanitizeCompactionSettings } from "../auto-compaction.js";
14
+ import { ASK_USER_QUESTION_TOOL_NAME } from "../ask-question.js";
17
15
  import { boundInputHashOf } from "../canonical-json.js";
18
16
  import { computeCostMicroUsd, isModelPriced, malformedPricingField, modelCostToPricing } from "../pricing.js";
19
17
  import { emitTrace } from "../trace.js";
20
18
  import { ORG_ADJUDICATION_TIMEOUT_MS, settleOrgVerdictWithin } from "../permission-rule-org.js";
21
19
  import { emitTaskOutcome } from "../task-outcome.js";
22
20
  import { isDegenerateCutMessage } from "../../brain/terminal-cause.js";
23
- import { primaryActivityArg } from "../arg-summary.js";
24
21
  import { redactThenCut } from "../../agents/subagent-steps.js";
25
22
  import { resolveReasoning } from "../../brain/reasoning.js";
26
23
  import { adjudicateDerivedRoute, authCarrierFingerprint, fallbackToPrimaryNotice, normalizeBaseUrl, sameRouteIdentity } from "../../brain/route-adjudicator.js";
27
- import { readDegradation } from "../../brain/degrading.js";
28
24
  import { runWithBrainTelemetry, runWithReasoningWireFacts, runWithStatusSink } from "../../brain/status-sink.js";
29
25
  import { expandTiers, resolveModel, resolveTaskModel } from "../roles.js";
30
26
  import { runSideQuery } from "../side-query.js";
@@ -32,1066 +28,55 @@ import { generatePromptSuggestions } from "./prompt-suggestions.js";
32
28
  import { PushQueue } from "../push-queue.js";
33
29
  import { TtlSessionStore } from "../session-store.js";
34
30
  import { toImageContent } from "./image.js";
35
- import { OUTPUT_TOOL_NAME, SKILLS_LISTING_PROBE_HEADER, resolveOutputRetries } from "./synthetic-tools.js";
31
+ import { SKILLS_LISTING_PROBE_HEADER, resolveOutputRetries } from "./synthetic-tools.js";
36
32
  import { cacheFamilyOf, usageCostMicroUsd } from "./usage-accounting.js";
37
33
  import { assembleResult, errorCodeOf } from "./assemble-result.js";
38
34
  import { amendTerminal, terminalProjection } from "./terminal-projection.js";
39
- 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";
35
+ import { AGENT_LISTING_REMOVED_HEADER, SKILLS_LISTING_DELTA_HEADER, SKILLS_LISTING_REMOVED_HEADER, agentListingDeltaHeader, attachmentEnvelopeTags, agentListingInitialHeader, replayAnnouncedListing, replayAnnouncedModels, commitAgentListing, commitSkillsListing, createAttachmentState, rebaseCadenceWindows, reduceToolEnd, renderAgentListingDelta, renderOrphanedBackgroundTasks, renderSkillsListingDelta, stampWriteAnchor } from "./turn-attachments.js";
40
36
  import { buildWorkingFileAttachments, centerAdoptionOption, contextInstructionFilesOption, emitInputTruncated, forkContextOption } from "./compaction-call-options.js";
41
37
  import { effectiveDelegationFacts, prepareTask } from "./prepare-task.js";
42
- import { gatedCallIdOf } from "./park-commit.js";
43
- import { placementValueOrAbsent } from "./checkpoint-scope.js";
38
+ import { drainForwardedFramesBeforeDone, forwardsSubagentEvents } from "./prepare-run-refs.js";
39
+ import { callFaceSeat, judgeParkedToolIdentity, rosterEntryNamed, toolCallFaceOf } from "../tool-roster.js";
40
+ import { normalizePersistedRuleHit } from "../gate-lanes.js";
41
+ import { applyPersistedTightening, disclosedRuleSet } from "../persisted-rule-arms.js";
44
42
  import { SessionReadFileStates } from "./prepare-hands-readface.js";
45
43
  import { settleTeardownLeg } from "./teardown-bounded.js";
46
- import { hasVerifiableStructureSignal } from "./grounding-signal.js";
47
44
  import { hasDestroy, isIsolated } from "../remote-env.js";
48
45
  import { hasBackgroundShell, sweepBackgroundShells } from "../background-shell.js";
49
- import { cloneObserverInput, formatHookFeedback, hookSeatExpiredError, MAX_HOOK_TIMEOUT_MS, mintHookInvocationIdentity, resolveHookTimeoutMs, runHookSeat } from "../hooks.js";
46
+ import { formatHookFeedback, hookSeatExpiredError, resolveHookTimeoutMs, runHookSeat } from "../hooks.js";
50
47
  import { buildHumanInputEvent, frameMidTurnUserInput, projectHumanInput } from "../human-input-projection.js";
51
- import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY, sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "../untrusted-text.js";
52
- import { droppedToolDigest, withAnnounceDigests } from "./announce-once-ledger.js";
48
+ import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY, sanitizeUntrustedText } from "../untrusted-text.js";
53
49
  import { appendInterruptionMarker, reconcileInterruptedSession } from "../session-reconcile.js";
54
50
  import { RunnerSharedToolResultStore } from "../tool-result-store.js";
55
- import { formatDiagnosticsBlock } from "../lsp-diagnostics.js";
56
- import { isAskOrigin } from "../ask-origin.js";
57
- import { isGateKind, PAUSE_REGISTRY, resumeGateMatches } from "../pause-registry.js";
58
- import { checkToolPolicyProjection, constraintChainDigest, constraintChainEntryOfSuppliedLayer, refuseOutOfContractDecision, screenApproverAttribution, toolPolicyNameSets } from "../tool-policy.js";
51
+ import { PAUSE_REGISTRY } from "../pause-registry.js";
52
+ import { refuseOutOfContractDecision, toolPolicyNameSets } from "../tool-policy.js";
59
53
  import { mintGateOutcome } from "./gate-exit.js";
60
54
  import { defaultTaskRegistry } from "../task-registry.js";
61
55
  import { discloseDroppedPending, isDelegatedAgentTerminal, isSystemInjectionPriority, isTerminalTaskNotification, PendingSessionNotifications, renderTaskNotificationXml, SYSTEM_INJECTION_PRIORITIES, SystemInjectionQueue, taskNotificationDedupKey } from "../task-notification.js";
62
56
  import { ToolDetachHub } from "../tool-detach.js";
63
57
  import { createPeerInboundChainRef, createPeerSelfRef } from "../../agents/peer-admission.js";
64
- import { workflowSizeGuidelineChangeNotice } from "../../orchestration/workflow-size-guideline.js";
65
58
  import { createRunState } from "./initial-run-state.js";
66
59
  import { nextHumanInputSeq, sameAcceptedSteerInput } from "./steer-admission.js";
67
- import { batchResponseDigest, reconciledToolEndBody, toolEndBodyFrom, toolResultMsg, writeFamilyOfCanonical } from "./tool-end-body.js";
68
- import { answerFaceForRedeemedCall, assertOutcomeText, deepJsonEqual, DEFERRED_REISSUE, describeSuppliedValue, pendingContentAskCallId, resumeContinuation, resumeDecisionWasNegative, sameWinner } from "./decide-continuation.js";
69
- import { awaitChargeWithSlowDisclosure, DEFAULT_PRECALL_OUTPUT_TOKENS, discloseUnevaluableWindow, ENV_DUE_GOVERNANCE_READ_BUDGET_MS, GOVERNANCE_READ_STALLED, LIMIT_APPROACH_DEFAULT_THRESHOLDS, limitApproachFrames, platformLimitTerminal, raceUntilDeadline, resolveMaxTurns, startTimeout, TIMER_LATENESS_REPORT_MS } from "./clock-and-limits.js";
60
+ import { reconciledToolEndBody, toolEndBodyFrom, toolResultMsg, writeFamilyOfCanonical } from "./tool-end-body.js";
61
+ import { answerFaceForRedeemedCall, deepJsonEqual, DEFERRED_REISSUE, pendingContentAskCallId, resumeContinuation, resumeDecisionWasNegative } from "./decide-continuation.js";
62
+ import { awaitChargeWithSlowDisclosure, DEFAULT_PRECALL_OUTPUT_TOKENS, discloseUnevaluableWindow, platformLimitTerminal, resolveMaxTurns, startTimeout, TIMER_LATENESS_REPORT_MS } from "./clock-and-limits.js";
70
63
  import { gitRestateOption, resolveGitLegDelivery, wrapGitFrame } from "./git-leg-delivery.js";
71
- const MAX_CONSECUTIVE_COMPACTION_FAILURES = 3;
72
- const RESUME_PREFLIGHT_DEFAULT_TIMEOUT_MS = 10_000;
64
+ import { COMPACTION_FREED_EPSILON, COMPACTION_REGROWTH_FACTOR, MAX_CONSECUTIVE_COMPACTION_FAILURES, createTurnBoundary } from "./run-turn-boundary.js";
65
+ import { createHarnessHandlers } from "./run-harness-handlers.js";
66
+ import { resumeAdmission } from "./resume-admission.js";
67
+ import { resumeReviewOutcome } from "./resume-review-outcome.js";
68
+ import { resumePolicyOutcome } from "./resume-policy-outcome.js";
69
+ import { resumeCheckpointScreen } from "./resume-checkpoint-screen.js";
70
+ import { resumeInternalsAndConfig } from "./resume-internals-and-config.js";
71
+ import { resumeApply } from "./resume-apply.js";
72
+ import { resumePreflight } from "./resume-preflight.js";
73
+ import { resumeClaim } from "./resume-claim.js";
73
74
  const STOP_HOOK_BLOCK_CAP = 8;
74
- const COMPACTION_REGROWTH_FACTOR = 1.5;
75
- const COMPACTION_FREED_EPSILON = 256;
76
75
  const ORG_DISCLOSURE_MAX_CHARS = 600;
77
- const BATCH_CONTEXT_MIN_KEEP_BYTES = 160;
78
- const BATCH_TRUNCATION_MARKER = "\n…[truncated]";
79
76
  const SUGGESTIONS_DEFAULT_COUNT = 3;
80
77
  const SUGGESTIONS_MAX_COUNT = 8;
81
78
  const SUGGESTIONS_TIMEOUT_MS = 30_000;
82
79
  const SUGGESTIONS_TRANSCRIPT_MESSAGES = 12;
83
- function makeTurnBoundary(prepared, stats, rs, deps) {
84
- const { spec, queue, manualCompactRef, todoToolMounted, taskToolsMounted, walltimeMonotonicDeadline, timeout, ident, postToolBatchHook, batchArgs, compactionBrain, withinTaskCompaction, compactionBreaker, windowSafetyOptions, rapidRefill, drainManualCompact, runnerHooks } = deps;
85
- let finalVerifySeenAtLastBoundary = 0;
86
- const onTurnBoundary = async (event) => {
87
- let boundarySteered = false;
88
- const noNextRequest = () => prepared.stopRequestedRef.current || deps.loopLatch.userHalted;
89
- const walltimeHit = walltimeMonotonicDeadline !== undefined && performance.now() >= walltimeMonotonicDeadline;
90
- const preemptHit = spec.preemptSignal?.aborted === true;
91
- if (preemptHit && !prepared.suspendForResource && !rs.counters.preemptIgnoredReported) {
92
- rs.counters.preemptIgnoredReported = true;
93
- emitTrace(rs.telemetry.tracer, () => ({ kind: "preempt.ignored", version: 1, taskId: rs.telemetry.taskId, ts: Date.now() }));
94
- }
95
- let platformCause;
96
- let platformRetryAfterMs;
97
- const preemptWillSuspend = preemptHit && prepared.suspendForResource !== undefined;
98
- if (!preemptWillSuspend && prepared.pausedRef.current === undefined && !prepared.abortController.signal.aborted) {
99
- const ledgerReadDeadline = prepared.envLifetimeSuspendAt !== undefined ? prepared.envLifetimeSuspendAt + ENV_DUE_GOVERNANCE_READ_BUDGET_MS : undefined;
100
- if (prepared.usageGovernance !== undefined) {
101
- const governance = prepared.usageGovernance;
102
- try {
103
- const at = Date.now();
104
- await awaitChargeWithSlowDisclosure(governance.commit(stats.tokens, rs.telemetry.unpricedSpend ? undefined : stats.costMicroUsd, at), () => runnerHooks.onError?.(new Error("the usage ledger CHARGE has not settled after 10s — still waiting (a charge is never abandoned: walking away would double-charge on the end-of-task flush). A wedged ledger store wedges this boundary, now visibly."), { phase: "config", sessionId: prepared.sessionId }));
105
- const windowRead = governance.check(at);
106
- let answer;
107
- if (ledgerReadDeadline !== undefined) {
108
- answer = await raceUntilDeadline(windowRead, ledgerReadDeadline);
109
- if (answer === GOVERNANCE_READ_STALLED) {
110
- void windowRead.catch((lateErr) => {
111
- try {
112
- runnerHooks.onError?.(lateErr instanceof Error ? lateErr : new Error(String(lateErr)), { phase: "config", sessionId: prepared.sessionId });
113
- }
114
- catch {
115
- }
116
- });
117
- try {
118
- runnerHooks.onError?.(new Error("the usage ledger did not answer before the execution environment's suspend deadline — stopping for the environment, the cause this run can still prove. The window state for this boundary is UNKNOWN and was not enforced."), { phase: "config", sessionId: prepared.sessionId });
119
- }
120
- catch {
121
- }
122
- }
123
- }
124
- else {
125
- answer = await windowRead;
126
- }
127
- if (answer !== GOVERNANCE_READ_STALLED) {
128
- platformRetryAfterMs = answer;
129
- if (platformRetryAfterMs !== undefined)
130
- platformCause = "usage_window";
131
- }
132
- }
133
- catch (govErr) {
134
- rs.limits.platformTerminal = govErr instanceof Error ? govErr : new Error(String(govErr));
135
- void prepared.harness.abort();
136
- prepared.abortController.abort();
137
- return undefined;
138
- }
139
- }
140
- if (platformCause === undefined && prepared.envLifetimeSuspendAt !== undefined && Date.now() >= prepared.envLifetimeSuspendAt) {
141
- platformCause = "env_lifetime";
142
- }
143
- }
144
- if (platformCause !== undefined) {
145
- const platformSpend = { costMicroUsd: stats.costMicroUsd, tokens: stats.tokens, turns: stats.turns, walltimeMs: Math.round(performance.now() - rs.telemetry.taskStartMonotonic) };
146
- const committed = prepared.suspendForPlatformLimit !== undefined &&
147
- (await prepared.suspendForPlatformLimit(platformCause, platformSpend, platformRetryAfterMs !== undefined ? { resumeAfterMs: platformRetryAfterMs } : undefined));
148
- if (committed) {
149
- rs.limits.turnsExceeded = false;
150
- rs.limits.budgetHit = undefined;
151
- return undefined;
152
- }
153
- const terminal = platformLimitTerminal(platformCause, platformRetryAfterMs);
154
- rs.limits.platformTerminal = terminal;
155
- try {
156
- runnerHooks.onError?.(terminal, { phase: "config", sessionId: prepared.sessionId });
157
- }
158
- catch {
159
- }
160
- void prepared.harness.abort();
161
- prepared.abortController.abort();
162
- return undefined;
163
- }
164
- if (prepared.suspendForResource &&
165
- (rs.limits.turnsExceeded || rs.limits.budgetHit !== undefined || walltimeHit || preemptHit) &&
166
- prepared.pausedRef.current === undefined) {
167
- const reason = preemptHit
168
- ? "preempt"
169
- : rs.limits.budgetHit !== undefined
170
- ? rs.limits.budgetAxis === "tokens"
171
- ? "budget_tokens"
172
- : "budget_cost"
173
- : rs.limits.turnsExceeded
174
- ? "turns"
175
- : "walltime";
176
- const sliceSpend = { costMicroUsd: stats.costMicroUsd, tokens: stats.tokens, turns: stats.turns, walltimeMs: Math.round(performance.now() - rs.telemetry.taskStartMonotonic) };
177
- if (await prepared.suspendForResource(reason, sliceSpend)) {
178
- rs.limits.turnsExceeded = false;
179
- rs.limits.budgetHit = undefined;
180
- }
181
- else {
182
- void prepared.harness.abort();
183
- prepared.abortController.abort();
184
- }
185
- return undefined;
186
- }
187
- if (walltimeMonotonicDeadline !== undefined &&
188
- prepared.suspendForResource === undefined &&
189
- performance.now() >= walltimeMonotonicDeadline) {
190
- if (!timeout.fired) {
191
- timeout.fired = true;
192
- timeout.clear();
193
- timeout.latenessMs = Math.max(timeout.latenessMs ?? 0, performance.now() - walltimeMonotonicDeadline);
194
- rs.counters.walltimeSyncBackstopFired = true;
195
- void prepared.harness.abort();
196
- prepared.abortController.abort();
197
- }
198
- return undefined;
199
- }
200
- const approachCfg = spec.limits?.approachNotice;
201
- if (approachCfg !== false &&
202
- rs.counters.approachNoticesSent < LIMIT_APPROACH_DEFAULT_THRESHOLDS.length &&
203
- prepared.suspendForResource === undefined &&
204
- rs.turn.lastTurnHadToolCalls &&
205
- prepared.batchHaltRef.current === undefined &&
206
- !boundarySteered &&
207
- !prepared.abortController.signal.aborted) {
208
- const thresholds = approachCfg?.at ?? LIMIT_APPROACH_DEFAULT_THRESHOLDS;
209
- const ratios = [];
210
- if (rs.budget.maxTokensWindow !== undefined && rs.budget.maxTokensWindow > 0) {
211
- ratios.push({ axis: "token budget", ratio: stats.tokens / rs.budget.maxTokensWindow });
212
- }
213
- if (rs.budget.maxCostMicroUsd !== undefined && rs.budget.maxCostMicroUsd > 0) {
214
- ratios.push({ axis: "cost budget", ratio: stats.costMicroUsd / rs.budget.maxCostMicroUsd });
215
- }
216
- const declaredMaxTurns = spec.limits?.maxTurns;
217
- if (declaredMaxTurns !== undefined && declaredMaxTurns > 0) {
218
- ratios.push({ axis: "turn budget", ratio: (stats.turns + 1) / declaredMaxTurns });
219
- }
220
- if (walltimeMonotonicDeadline !== undefined) {
221
- const walltimeWindowMs = walltimeMonotonicDeadline - rs.telemetry.taskStartMonotonic;
222
- if (walltimeWindowMs > 0) {
223
- ratios.push({ axis: "walltime budget", ratio: (performance.now() - rs.telemetry.taskStartMonotonic) / walltimeWindowMs });
224
- }
225
- }
226
- let tightest;
227
- for (const r of ratios)
228
- if (tightest === undefined || r.ratio > tightest.ratio)
229
- tightest = r;
230
- const index = rs.counters.approachNoticesSent;
231
- if (tightest !== undefined && tightest.ratio >= thresholds[index]) {
232
- rs.counters.approachNoticesSent += 1;
233
- const percent = Math.min(100, Math.round(tightest.ratio * 100));
234
- const body = limitApproachFrames(index === 0 ? 0 : 1, tightest.axis, percent, prepared.reminderMark);
235
- void prepared.harness.steer(body, { engineMinted: true }).catch(() => undefined);
236
- queue.push({ type: "steering_injected", source: "limit_approach", preview: body.slice(0, 220), ...ident() });
237
- boundarySteered = true;
238
- }
239
- }
240
- if (prepared.reviewRequestRef.pending !== undefined) {
241
- const { reason } = prepared.reviewRequestRef.pending;
242
- prepared.reviewRequestRef.pending = undefined;
243
- const honorable = prepared.suspendForReview !== undefined &&
244
- !prepared.abortController.signal.aborted &&
245
- prepared.pausedRef.current === undefined;
246
- if (honorable && prepared.suspendForReview) {
247
- await prepared.suspendForReview(reason);
248
- return undefined;
249
- }
250
- emitTrace(rs.telemetry.tracer, () => ({ kind: "review.dropped", version: 1, taskId: rs.telemetry.taskId, ts: Date.now() }));
251
- }
252
- if (prepared.lspDiagnostics &&
253
- !prepared.lspDiagnostics.registry.isEmpty() &&
254
- !prepared.abortController.signal.aborted &&
255
- prepared.batchHaltRef.current === undefined) {
256
- const files = prepared.lspDiagnostics.registry.drain(prepared.lspDiagnostics.runIdent);
257
- if (files.length > 0) {
258
- queue.push({ type: "diagnostics", files, isNew: true, ...ident() });
259
- const block = mintSystemReminder(formatDiagnosticsBlock(files), prepared.reminderMark);
260
- void prepared.harness.steer(block, { provenance: "engine-note" }).catch(() => undefined);
261
- boundarySteered = true;
262
- }
263
- }
264
- let boundaryAttachmentBytes = 0;
265
- let attachmentsPayload;
266
- if ((rs.attach.dateState !== undefined ||
267
- rs.attach.instrState !== undefined ||
268
- rs.attach.sizeGuidelineState !== undefined ||
269
- rs.attach.attachState !== undefined) &&
270
- !boundarySteered &&
271
- prepared.batchHaltRef.current === undefined &&
272
- !noNextRequest() &&
273
- rs.counters.finalVerifyInjections === 0 &&
274
- !prepared.abortController.signal.aborted) {
275
- const due = [];
276
- if (rs.attach.attachState !== undefined) {
277
- const changedFilesOn = Boolean(rs.attach.attachmentsCfg?.changedFiles);
278
- let changed;
279
- if (changedFilesOn && prepared.detectExternalChanges !== undefined) {
280
- const maxFiles = typeof rs.attach.attachmentsCfg?.changedFiles === "object"
281
- ? (rs.attach.attachmentsCfg.changedFiles.maxFiles ?? CHANGED_FILES_MAX)
282
- : CHANGED_FILES_MAX;
283
- const SCAN_CEILING_MS = 2_000;
284
- const scan = await Promise.race([
285
- prepared.detectExternalChanges(maxFiles),
286
- new Promise((resolve) => {
287
- setTimeout(() => resolve({ changed: [], evicted: [] }), SCAN_CEILING_MS);
288
- }),
289
- ]);
290
- for (const p of scan.evicted)
291
- rs.attach.attachState.surfacedMtime.delete(p);
292
- changed = scan.changed;
293
- }
294
- const backgroundTasksOn = (rs.attach.attachmentsCfg?.backgroundTasks ?? eventDefaultOn("background_tasks")) === true;
295
- const bgTasks = backgroundTasksOn && rs.attach.attachState.postCompactPending ? prepared.listBackgroundTasks() : undefined;
296
- const toolsDeltaOn = rs.attach.attachmentsCfg?.toolsDelta === true;
297
- let announcedOnceDirty = false;
298
- const tdRef = toolsDeltaOn ? prepared.toolsDeltaRef : undefined;
299
- const pendingTools = tdRef !== undefined && tdRef.pending.length > 0 ? [...tdRef.pending] : undefined;
300
- const pendingRemovedTools = tdRef !== undefined && tdRef.pendingRemoved.length > 0 ? [...tdRef.pendingRemoved] : undefined;
301
- const pendingReaddedTools = tdRef !== undefined && tdRef.pendingReadded.length > 0 ? [...tdRef.pendingReadded] : undefined;
302
- const pendingFailedMcp = tdRef !== undefined && tdRef.pendingFailed.length > 0 ? tdRef.pendingFailed.map((f) => ({ ...f })) : undefined;
303
- const mcpToolsDelta = pendingRemovedTools !== undefined || pendingReaddedTools !== undefined || pendingFailedMcp !== undefined
304
- ? {
305
- ...(pendingRemovedTools !== undefined ? { removed: pendingRemovedTools } : {}),
306
- ...(pendingReaddedTools !== undefined ? { readded: pendingReaddedTools } : {}),
307
- ...(pendingFailedMcp !== undefined ? { failedServers: pendingFailedMcp } : {}),
308
- }
309
- : undefined;
310
- const mcpInstructionsOn = rs.attach.attachmentsCfg?.mcpInstructions === true;
311
- const mcpDelta = prepared.mcp.instructionsDelta;
312
- const mcpAdds = mcpInstructionsOn && mcpDelta.pendingAdds.length > 0 ? [...mcpDelta.pendingAdds] : undefined;
313
- const mcpRemovals = mcpInstructionsOn && mcpDelta.pendingRemovals.length > 0 ? [...mcpDelta.pendingRemovals] : undefined;
314
- const mcpDropped = mcpInstructionsOn && prepared.mcp.droppedTools.length > 0 ? selectMcpDroppedBatch(prepared.mcp.droppedTools) : undefined;
315
- const budgetUsdOn = rs.attach.attachmentsCfg?.budgetUsd === true && rs.budget.maxCostMicroUsd !== undefined && rs.turn.lastTurnHadToolCalls;
316
- const totalTokensDeclared = rs.attach.attachmentsCfg?.totalTokensReminder;
317
- const totalTokensMode = rs.attach.attachmentsCfg?.totalTokensReminderMode ?? TOTAL_TOKENS_REMINDER_DEFAULT_MODE;
318
- const totalTokensArmedByDefault = totalTokensDeclared === undefined &&
319
- eventDefaultOn("total_tokens_reminder") &&
320
- totalTokensMode !== "fixed" &&
321
- totalTokensMode !== "infinite";
322
- const totalTokensOn = (totalTokensDeclared === true || totalTokensArmedByDefault) && rs.turn.lastTurnHadToolCalls;
323
- const toolSearchReminderOn = rs.attach.attachmentsCfg?.toolSearchReminder === true && prepared.deferredToolNames !== undefined;
324
- const undiscoveredTools = toolSearchReminderOn
325
- ? [...prepared.deferredToolNames].filter((n) => !prepared.activeTools.has(n)).sort()
326
- : undefined;
327
- due.push(...collectDueAttachments(rs.attach.attachState, {
328
- turn: stats.turns,
329
- cadenceTurn: rs.counters.cadenceTurns,
330
- todoMounted: todoToolMounted,
331
- taskListMounted: taskToolsMounted,
332
- planActive: prepared.planModeRef.active,
333
- config: {
334
- todoReminder: rs.attach.attachmentsCfg?.todoReminder === true,
335
- ...(rs.attach.attachmentsCfg?.todoReminderMode !== undefined ? { todoReminderMode: rs.attach.attachmentsCfg.todoReminderMode } : {}),
336
- toolSearchReminder: toolSearchReminderOn,
337
- changedFiles: changedFilesOn,
338
- planModeReminder: rs.attach.attachmentsCfg?.planModeReminder === true,
339
- budgetUsd: budgetUsdOn,
340
- totalTokensReminder: totalTokensOn,
341
- ...(rs.attach.attachmentsCfg?.totalTokensReminderMode !== undefined
342
- ? { totalTokensReminderMode: rs.attach.attachmentsCfg.totalTokensReminderMode }
343
- : {}),
344
- backgroundTasks: backgroundTasksOn,
345
- toolsDelta: toolsDeltaOn,
346
- agentListing: rs.attach.agentListingOn,
347
- skillsListing: rs.attach.skillsListingOn,
348
- mcpInstructions: mcpInstructionsOn,
349
- },
350
- ...(undiscoveredTools !== undefined && undiscoveredTools.length > 0
351
- ? { undiscoveredTools, deferDirectCall: prepared.deferDirectCall }
352
- : {}),
353
- ...(changed !== undefined ? { changedFiles: changed } : {}),
354
- ...(budgetUsdOn && rs.budget.maxCostMicroUsd !== undefined
355
- ? { budgetUsd: { used: stats.costMicroUsd / 1e6, total: rs.budget.maxCostMicroUsd / 1e6 } }
356
- : {}),
357
- ...(totalTokensOn && rs.budget.maxTokensWindow !== undefined
358
- ? { totalTokens: { used: stats.tokens, total: rs.budget.maxTokensWindow } }
359
- : {}),
360
- ...(bgTasks !== undefined ? { backgroundTasks: bgTasks } : {}),
361
- ...(pendingTools !== undefined ? { newTools: pendingTools } : {}),
362
- ...(prepared.toolMaterializeStatic ? { newToolsStaticFace: true } : {}),
363
- ...(prepared.toolMaterializeStatic && pendingTools !== undefined
364
- ? { newToolsSwappedUnderStatic: pendingTools.filter((n) => prepared.staticFaceFor?.(n) !== true) }
365
- : {}),
366
- ...(prepared.toolMaterializeStatic && pendingReaddedTools !== undefined
367
- ? { readdedToolsSwappedUnderStatic: pendingReaddedTools.filter((n) => prepared.staticFaceFor?.(n) !== true) }
368
- : {}),
369
- ...(mcpToolsDelta !== undefined ? { mcpToolsDelta } : {}),
370
- ...(rs.attach.agentListingOn && prepared.agentListing !== undefined
371
- ? {
372
- agentListing: prepared.agentListing.entries,
373
- agentToolName: prepared.agentListing.toolName,
374
- ...(prepared.agentListing.models !== undefined ? { agentModels: prepared.agentListing.models } : {}),
375
- }
376
- : {}),
377
- ...(rs.attach.skillsListingOn && prepared.skillsListing !== undefined ? { skillsListing: prepared.skillsListing.entries } : {}),
378
- ...(mcpAdds !== undefined || mcpRemovals !== undefined
379
- ? { mcpInstructions: { added: mcpAdds ?? [], removed: mcpRemovals ?? [] } }
380
- : {}),
381
- ...(mcpDropped !== undefined ? { mcpDroppedTools: mcpDropped } : {}),
382
- }));
383
- if (pendingTools !== undefined || mcpToolsDelta !== undefined) {
384
- const exact = renderToolsDelta({
385
- ...(pendingTools !== undefined ? { added: pendingTools } : {}),
386
- ...(prepared.toolMaterializeStatic ? { staticFace: true } : {}),
387
- ...(prepared.toolMaterializeStatic && pendingTools !== undefined
388
- ? { swappedUnderStatic: pendingTools.filter((n) => prepared.staticFaceFor?.(n) !== true) }
389
- : {}),
390
- ...(prepared.toolMaterializeStatic && pendingReaddedTools !== undefined
391
- ? { readdedSwappedUnderStatic: pendingReaddedTools.filter((n) => prepared.staticFaceFor?.(n) !== true) }
392
- : {}),
393
- ...(mcpToolsDelta ?? {}),
394
- });
395
- if (exact !== undefined && due.some((a) => a.source === "tools_delta" && a.body === exact)) {
396
- const ref = prepared.toolsDeltaRef;
397
- if (pendingTools !== undefined)
398
- ref.pending.splice(0, pendingTools.length);
399
- if (pendingRemovedTools !== undefined)
400
- ref.pendingRemoved.splice(0, pendingRemovedTools.length);
401
- if (pendingReaddedTools !== undefined)
402
- ref.pendingReadded.splice(0, pendingReaddedTools.length);
403
- if (pendingFailedMcp !== undefined) {
404
- ref.pendingFailed.splice(0, pendingFailedMcp.length);
405
- if (!noNextRequest()) {
406
- prepared.announcedListingsRef.mcpFailed = [...(prepared.announcedListingsRef.mcpFailed ?? []), ...pendingFailedMcp];
407
- announcedOnceDirty = true;
408
- }
409
- }
410
- }
411
- }
412
- if ((mcpAdds !== undefined || mcpRemovals !== undefined) &&
413
- due.some((a) => a.source === "mcp_instructions" && a.body === renderMcpInstructionsDelta(mcpAdds ?? [], mcpRemovals ?? []))) {
414
- if (mcpAdds !== undefined)
415
- mcpDelta.pendingAdds.splice(0, mcpAdds.length);
416
- if (mcpRemovals !== undefined)
417
- mcpDelta.pendingRemovals.splice(0, mcpRemovals.length);
418
- }
419
- if (mcpDropped !== undefined &&
420
- due.some((a) => a.source === "mcp_dropped_tools" && a.body === renderMcpDroppedTools(mcpDropped))) {
421
- prepared.mcp.droppedTools.splice(0, mcpDropped.length);
422
- if (!noNextRequest()) {
423
- prepared.announcedListingsRef.advisories = withAnnounceDigests(prepared.announcedListingsRef.advisories, mcpDropped.map(droppedToolDigest));
424
- announcedOnceDirty = true;
425
- }
426
- }
427
- if (rs.attach.agentListingOn && prepared.agentListing !== undefined && due.some((a) => a.source === "agent_listing")) {
428
- const listing = prepared.agentListing;
429
- const intactBody = renderAgentListingDelta(rs.attach.attachState, listing.entries, listing.toolName, listing.models);
430
- if (intactBody !== undefined && due.some((a) => a.source === "agent_listing" && a.body === intactBody)) {
431
- commitAgentListing(rs.attach.attachState, listing.entries, listing.models);
432
- prepared.announcedListingsRef.agents = listing.entries.map((e) => e.name);
433
- if (listing.models !== undefined)
434
- prepared.announcedListingsRef.models = [...listing.models];
435
- if (prepared.announcedSnapshotRecovered)
436
- await prepared.session.appendAnnouncedListing(prepared.announcedListingsRef).catch(() => undefined);
437
- }
438
- }
439
- if (rs.attach.skillsListingOn && prepared.skillsListing !== undefined && due.some((a) => a.source === "skills_listing")) {
440
- const listing = prepared.skillsListing;
441
- const intactBody = renderSkillsListingDelta(rs.attach.attachState, listing.entries);
442
- if (intactBody !== undefined && due.some((a) => a.source === "skills_listing" && a.body === intactBody)) {
443
- commitSkillsListing(rs.attach.attachState, listing.entries);
444
- prepared.announcedListingsRef.skills = listing.entries.map((e) => e.name);
445
- if (prepared.announcedSnapshotRecovered)
446
- await prepared.session.appendAnnouncedListing(prepared.announcedListingsRef).catch(() => undefined);
447
- }
448
- }
449
- if (announcedOnceDirty && prepared.announcedSnapshotRecovered)
450
- await prepared.session.appendAnnouncedListing(prepared.announcedListingsRef).catch(() => undefined);
451
- }
452
- if (rs.attach.dateState !== undefined && rs.turn.lastTurnHadToolCalls) {
453
- const today = prepared.dateChange.today();
454
- const cand = collectDateChange(rs.attach.dateState, today);
455
- if (cand !== undefined) {
456
- due.unshift(cand);
457
- rs.attach.dateState.announcedDate = today;
458
- }
459
- }
460
- if (rs.attach.sizeGuidelineState !== undefined && rs.turn.lastTurnHadToolCalls) {
461
- const guideline = rs.attach.sizeGuidelineState.current();
462
- if (guideline !== rs.attach.sizeGuidelineState.announcedGuideline) {
463
- due.splice(due.length > 0 && due[0].source === "date_change" ? 1 : 0, 0, {
464
- source: "workflow_size_guideline_change",
465
- body: workflowSizeGuidelineChangeNotice(guideline),
466
- });
467
- rs.attach.sizeGuidelineState.announcedGuideline = guideline;
468
- }
469
- }
470
- if (rs.attach.instrState !== undefined && rs.attach.instrProbe !== undefined && rs.turn.lastTurnHadToolCalls) {
471
- let probed = null;
472
- try {
473
- const PROBE_CEILING_MS = 2_000;
474
- probed = await Promise.race([
475
- rs.attach.instrProbe(prepared.instructionSources),
476
- new Promise((resolve) => {
477
- setTimeout(() => resolve(null), PROBE_CEILING_MS);
478
- }),
479
- ]);
480
- }
481
- catch {
482
- probed = null;
483
- }
484
- const cand = collectInstructionsChange(rs.attach.instrState, probed);
485
- if (cand !== undefined) {
486
- due.splice(due.length > 0 && due[0].source === "date_change" ? 1 : 0, 0, cand.attachment);
487
- commitInstructionsChange(rs.attach.instrState, cand.announced);
488
- }
489
- }
490
- if (due.length > 0) {
491
- attachmentsPayload = due.map((a) => mintSystemReminder(sanitizeUntrustedText(a.body, attachmentEnvelopeTags(a.source)), prepared.reminderMark)).join("\n");
492
- for (const a of due)
493
- boundaryAttachmentBytes += Buffer.byteLength(a.body, "utf8");
494
- rs.attach.attachmentsInjected += due.length;
495
- for (const a of due) {
496
- queue.push({ type: "steering_injected", source: a.source, preview: redactThenCut(a.body, 220), ...ident() });
497
- }
498
- }
499
- }
500
- let batchContextBlock;
501
- const finalVerifyInjectedThisTurn = rs.counters.finalVerifyInjections !== finalVerifySeenAtLastBoundary;
502
- finalVerifySeenAtLastBoundary = rs.counters.finalVerifyInjections;
503
- if (postToolBatchHook !== undefined && rs.turn.toolBatch.length > 0) {
504
- const batch = rs.turn.toolBatch;
505
- rs.turn.toolBatch = [];
506
- batchArgs?.clear();
507
- const injectedThisTurn = finalVerifyInjectedThisTurn ? "final_verification" : undefined;
508
- if (!boundarySteered && !prepared.abortController.signal.aborted) {
509
- try {
510
- const batchSeat = await runHookSeat("postToolBatch", { timeoutMs: prepared.hookTimeoutMs, signal: prepared.abortController.signal }, (sig) => postToolBatchHook(batch, injectedThisTurn !== undefined ? { injectedThisTurn } : undefined, { identity: prepared.hookIdentity, signal: sig }));
511
- if (batchSeat.expired) {
512
- runnerHooks.onError?.(hookSeatExpiredError("postToolBatch", prepared.hookTimeoutMs, batchSeat.cause, "the boundary observation was abandoned and its additionalContext dropped; the turn boundary itself is unchanged"), { phase: "hook", sessionId: prepared.sessionId });
513
- }
514
- const r = batchSeat.expired ? undefined : batchSeat.value;
515
- if (r?.additionalContext && injectedThisTurn === undefined) {
516
- const body = sanitizeUntrustedText(r.additionalContext, SHELLED_BODY_ENVELOPE_TAGS);
517
- const budget = ATTACHMENT_BYTE_CAP - boundaryAttachmentBytes;
518
- if (budget >= BATCH_CONTEXT_MIN_KEEP_BYTES) {
519
- const clipped = clipToBytes(body, budget) === body
520
- ? body
521
- : `${clipToBytes(body, budget - Buffer.byteLength(BATCH_TRUNCATION_MARKER, "utf8"))}${BATCH_TRUNCATION_MARKER}`;
522
- batchContextBlock = mintSystemReminder(clipped, prepared.reminderMark);
523
- }
524
- }
525
- }
526
- catch (err) {
527
- try {
528
- runnerHooks.onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "hook", sessionId: prepared.sessionId });
529
- }
530
- catch {
531
- }
532
- }
533
- }
534
- }
535
- if ((attachmentsPayload !== undefined || batchContextBlock !== undefined) && prepared.batchHaltRef.current === undefined) {
536
- const payload = attachmentsPayload !== undefined && batchContextBlock !== undefined ? `${attachmentsPayload}\n${batchContextBlock}` : (attachmentsPayload ?? batchContextBlock);
537
- void prepared.harness.steer(payload, { engineMinted: true }).catch(() => { });
538
- }
539
- const forceManual = manualCompactRef.requested;
540
- if ((!withinTaskCompaction && !forceManual) ||
541
- prepared.abortController.signal.aborted ||
542
- prepared.pausedRef.current !== undefined) {
543
- if (forceManual) {
544
- emitTrace(rs.telemetry.tracer, () => ({ kind: "compaction.mooted", version: 1, taskId: rs.telemetry.taskId, reason: "task_ending", ts: Date.now() }));
545
- queue.push({ type: "compaction_outcome", outcome: "mooted", trigger: "manual", reason: "task_ending", ...ident() });
546
- drainManualCompact("mooted");
547
- }
548
- return undefined;
549
- }
550
- if (compactionBreaker.failures >= MAX_CONSECUTIVE_COMPACTION_FAILURES) {
551
- if (forceManual) {
552
- emitTrace(rs.telemetry.tracer, () => ({
553
- kind: "compaction.failed",
554
- version: 1,
555
- taskId: rs.telemetry.taskId,
556
- trigger: "manual",
557
- reason: "compaction circuit breaker open (3 consecutive failures this task) — manual request drained without an attempt",
558
- ts: Date.now(),
559
- }));
560
- queue.push({ type: "compaction_outcome", outcome: "failed", trigger: "manual", reason: "compaction circuit breaker open (3 consecutive failures this task) — manual request drained without an attempt", ...ident() });
561
- drainManualCompact("failed");
562
- }
563
- return undefined;
564
- }
565
- const claimedManual = forceManual
566
- ? (() => {
567
- const waiters = manualCompactRef.waiters.splice(0);
568
- let instructions;
569
- for (let i = waiters.length - 1; i >= 0; i--) {
570
- if (waiters[i].instructions !== undefined) {
571
- instructions = waiters[i].instructions;
572
- break;
573
- }
574
- }
575
- return { instructions, waiters };
576
- })()
577
- : undefined;
578
- if (forceManual) {
579
- manualCompactRef.requested = false;
580
- }
581
- const resolveClaimedWaiters = (outcome) => {
582
- if (claimedManual !== undefined)
583
- for (const w of claimedManual.waiters.splice(0))
584
- w.resolve(outcome);
585
- };
586
- const claimedCancelSignals = claimedManual?.waiters.flatMap((w) => (w.signal !== undefined ? [w.signal] : [])) ?? [];
587
- const manualCancelSignal = claimedCancelSignals.length === 0
588
- ? undefined
589
- : claimedCancelSignals.length === 1
590
- ? claimedCancelSignals[0]
591
- : AbortSignal.any(claimedCancelSignals);
592
- const compactionPassSignal = claimedCancelSignals.length > 0
593
- ? AbortSignal.any([prepared.abortController.signal, ...claimedCancelSignals])
594
- : prepared.abortController.signal;
595
- const trimPressureArmed = prepared.trimPressureRef.droppedMessages;
596
- if (trimPressureArmed)
597
- prepared.trimPressureRef.droppedMessages = false;
598
- const trimPressure = trimPressureArmed && !rs.counters.trimForceBackoff;
599
- const passTrigger = trimPressure ? "forced" : forceManual ? "manual" : "auto";
600
- let manualOutcome = "failed";
601
- const manualInstructions = claimedManual?.instructions;
602
- try {
603
- const comp = await maybeCompact({
604
- session: prepared.session,
605
- epochDeclaredSections: prepared.epochDeclaredSections,
606
- ...centerAdoptionOption(prepared),
607
- model: event.model,
608
- compactionModel: prepared.compModel,
609
- ...forkContextOption(prepared, false),
610
- brain: compactionBrain,
611
- getApiKeyAndHeaders: spec.getApiKeyAndHeaders,
612
- thinking: prepared.thinking,
613
- settings: { ...DEFAULT_COMPACTION_SETTINGS, ...spec.compaction },
614
- customInstructions: manualInstructions ?? spec.compaction?.instructions ?? DEFAULT_COMPACTION_INSTRUCTIONS,
615
- signal: compactionPassSignal,
616
- ...(manualCancelSignal !== undefined ? { manualCancelSignal } : {}),
617
- minTokens: forceManual ? 0 : rs.counters.compactionFloor,
618
- force: forceManual,
619
- onInputTruncated: emitInputTruncated(rs.telemetry.tracer, rs.telemetry.taskId),
620
- forceUnderThreshold: trimPressure,
621
- overheadTokens: prepared.promptOverheadTokens,
622
- ...(prepared.activeTools.size > 0 ? { activeTools: [...prepared.activeTools] } : {}),
623
- workingFileAttachments: buildWorkingFileAttachments(spec, prepared),
624
- ...contextInstructionFilesOption(prepared),
625
- ...(prepared.onCompactionApplied ? { onApplied: prepared.onCompactionApplied } : {}),
626
- ...runnerHooks.seamCCompactionOptions(prepared),
627
- ...gitRestateOption(prepared),
628
- ...windowSafetyOptions(event.model),
629
- ...runnerHooks.compactionHookOptions(spec, prepared.sessionId, passTrigger),
630
- });
631
- if (comp.blocked) {
632
- emitTrace(rs.telemetry.tracer, () => ({
633
- kind: "compaction.blocked",
634
- version: 1,
635
- taskId: rs.telemetry.taskId,
636
- trigger: forceManual ? "manual" : "auto",
637
- ts: Date.now(),
638
- }));
639
- }
640
- else {
641
- compactionBreaker.failures = 0;
642
- }
643
- rs.counters.trimForceBackoff = nextTrimForceBackoff(rs.counters.trimForceBackoff, comp, trimPressure);
644
- if (comp.contextUsage) {
645
- queue.push({ type: "context_usage", ...comp.contextUsage, ...ident() });
646
- }
647
- if (comp.unevaluableWindow) {
648
- discloseUnevaluableWindow(comp.unevaluableWindow, prepared, rs.telemetry.tracer, rs.telemetry.taskId, runnerHooks.onError);
649
- }
650
- if (comp.suppressedByFloor) {
651
- const s = comp.suppressedByFloor;
652
- emitTrace(rs.telemetry.tracer, () => ({
653
- kind: "compaction.suppressed",
654
- version: 1,
655
- taskId: rs.telemetry.taskId,
656
- estTokens: s.estTokens,
657
- floor: s.floor,
658
- ts: Date.now(),
659
- }));
660
- }
661
- manualOutcome = comp.compacted ? "compacted" : comp.blocked ? "blocked" : comp.disabled ? "disabled" : "noop";
662
- if (forceManual && !comp.compacted && !comp.blocked) {
663
- if (comp.disabled) {
664
- emitTrace(rs.telemetry.tracer, () => ({ kind: "compaction.disabled", version: 1, taskId: rs.telemetry.taskId, ts: Date.now() }));
665
- }
666
- else {
667
- emitTrace(rs.telemetry.tracer, () => ({ kind: "compaction.noop", version: 1, taskId: rs.telemetry.taskId, ts: Date.now() }));
668
- }
669
- }
670
- if (!comp.compacted) {
671
- const outcome = comp.blocked
672
- ? "blocked"
673
- : comp.disabled
674
- ? (forceManual || trimPressure ? "disabled" : undefined)
675
- : comp.suppressedByFloor
676
- ? "suppressed"
677
- : comp.noop || forceManual
678
- ? "noop"
679
- : undefined;
680
- if (outcome !== undefined) {
681
- queue.push({ type: "compaction_outcome", outcome, trigger: passTrigger, ...ident() });
682
- }
683
- }
684
- runnerHooks.recordCompactionReuse(prepared, comp);
685
- if (comp.compacted) {
686
- queue.push({
687
- type: "compacted",
688
- trigger: passTrigger,
689
- tokensBefore: comp.tokensBefore ?? 0,
690
- ...(comp.postTriggerTokens !== undefined ? { tokensAfter: comp.postTriggerTokens } : {}),
691
- ...(comp.triggerTokens !== undefined ? { triggerTokensBefore: comp.triggerTokens } : {}),
692
- ...(comp.durationMs !== undefined ? { durationMs: comp.durationMs } : {}),
693
- ...(comp.phaseDurations !== undefined ? { phaseDurations: comp.phaseDurations } : {}),
694
- ...(comp.firstKeptEntryId !== undefined ? { preserved_segment: { firstKeptEntryId: comp.firstKeptEntryId } } : {}),
695
- ...(comp.attachedFiles !== undefined ? { attachedFiles: comp.attachedFiles } : {}),
696
- ...(comp.modelFallback ? { modelFallback: true } : {}),
697
- ...(comp.fallbackReason !== undefined ? { fallbackReason: comp.fallbackReason } : {}),
698
- ...(comp.clampedRatio !== undefined ? { clampedRatio: comp.clampedRatio } : {}),
699
- ...(comp.clampReason !== undefined ? { clampReason: comp.clampReason } : {}),
700
- ...ident(),
701
- });
702
- if (!forceManual && recordCompactionAndCheckRapidRefill(rapidRefill, stats.turns)) {
703
- compactionBreaker.failures = MAX_CONSECUTIVE_COMPACTION_FAILURES;
704
- queue.push({ type: "compaction_outcome", outcome: "disabled", trigger: passTrigger, reason: "rapid_refill: compaction disabled for the rest of this task", ...ident() });
705
- runnerHooks.onError?.(new Error("compaction.rapid_refill: the context refilled within <3 turns of compaction 3 times in a row — compaction disabled for the rest of this task (thrash spiral; the transcript is dominated by incompressible content)"), { phase: "compaction", sessionId: prepared.sessionId });
706
- }
707
- if (comp.phaseDurations !== undefined && comp.durationMs !== undefined) {
708
- const pd = comp.phaseDurations;
709
- const pdDur = comp.durationMs;
710
- emitTrace(rs.telemetry.tracer, () => ({ kind: "compaction.phase_timings", version: 1, taskId: rs.telemetry.taskId, ...pd, durationMs: pdDur, ts: Date.now() }));
711
- }
712
- prepared.cacheBreakDetector?.notifyCompaction();
713
- if (rs.attach.attachState !== undefined) {
714
- rs.attach.attachState.postCompactPending = true;
715
- rebaseCadenceWindows(rs.attach.attachState, rs.counters.cadenceTurns);
716
- }
717
- if ((comp.freedTokens ?? 0) >= COMPACTION_FREED_EPSILON) {
718
- const postSize = comp.postTriggerTokens ?? Math.max(0, (comp.triggerTokens ?? comp.tokensBefore ?? 0) - (comp.freedTokens ?? 0));
719
- rs.counters.compactionFloor = Math.ceil(postSize * COMPACTION_REGROWTH_FACTOR);
720
- }
721
- }
722
- }
723
- catch (err) {
724
- const manualCancelAbort = !prepared.abortController.signal.aborted && isCompactionManualCancel(err);
725
- if (manualCancelAbort) {
726
- emitTrace(rs.telemetry.tracer, () => ({ kind: "compaction.mooted", version: 1, taskId: rs.telemetry.taskId, reason: "cancelled", ts: Date.now() }));
727
- queue.push({ type: "compaction_outcome", outcome: "mooted", trigger: passTrigger, reason: "cancelled", ...ident() });
728
- manualOutcome = "mooted";
729
- }
730
- else if (!prepared.abortController.signal.aborted) {
731
- compactionBreaker.failures += 1;
732
- runnerHooks.onError?.(err, { phase: "compaction", sessionId: prepared.sessionId });
733
- const msg = String(err instanceof Error ? err.message : err);
734
- const failReason = msg.length > 512 ? `${msg.slice(0, 512)}…` : msg;
735
- emitTrace(rs.telemetry.tracer, () => ({
736
- kind: "compaction.failed",
737
- version: 1,
738
- taskId: rs.telemetry.taskId,
739
- trigger: passTrigger,
740
- reason: failReason,
741
- ts: Date.now(),
742
- }));
743
- queue.push({ type: "compaction_outcome", outcome: "failed", trigger: passTrigger, reason: failReason, ...ident() });
744
- manualOutcome = "failed";
745
- }
746
- else {
747
- manualOutcome = "mooted";
748
- }
749
- }
750
- if (forceManual)
751
- resolveClaimedWaiters(manualOutcome);
752
- return undefined;
753
- };
754
- return onTurnBoundary;
755
- }
756
- function makeHarnessHandlers(prepared, stats, rs, deps) {
757
- const { spec, queue, internals, ident, parentToolCallId, subagentName, pushContent, emitCommitted, startedToolCallIds, toolStartAt, writeFamilyOf, toolLabels, postToolBatchHook, batchArgs } = deps;
758
- const internalsNotifier = createSafeNotifier({
759
- onError: (f) => console.warn(`[sema-core] ${f.site}: run-internals activity sink threw (contained; further failures counted, not re-disclosed): ${f.error.message}`),
760
- });
761
- let lastWorkspaceCwd = prepared.cwdRef?.current;
762
- const announceWorkspaceMove = () => {
763
- const cwdNow = prepared.cwdRef?.current;
764
- if (cwdNow !== undefined && cwdNow !== lastWorkspaceCwd) {
765
- lastWorkspaceCwd = cwdNow;
766
- queue.push({ type: "workspace_changed", cwd: cwdNow, ...ident() });
767
- }
768
- };
769
- const onMessageUpdate = (event) => {
770
- const ev = event.assistantMessageEvent;
771
- if (rs.turn.callStartAt === undefined)
772
- rs.turn.callStartAt = Date.now();
773
- if (ev.type === "text_delta" && ev.delta) {
774
- if (rs.turn.firstTokenAt === undefined)
775
- rs.turn.firstTokenAt = Date.now();
776
- pushContent({ type: "text_delta", delta: ev.delta, ...ident() });
777
- }
778
- else if (ev.type === "text_end" && typeof ev.content === "string" && ev.content.length !== 0) {
779
- pushContent({ type: "text_end", content: ev.content, ...ident() });
780
- }
781
- else if (ev.type === "thinking_delta" && ev.delta) {
782
- pushContent({ type: "reasoning_delta", delta: ev.delta, ...ident() });
783
- }
784
- if (rs.budget.streamCancel && !rs.limits.budgetHit && (ev.type === "text_delta" || ev.type === "thinking_delta") && ev.delta) {
785
- rs.budget.callOutputChars += ev.delta.length;
786
- if (rs.budget.callOutputChars - rs.budget.lastStreamBudgetCheck >= 256) {
787
- rs.budget.lastStreamBudgetCheck = rs.budget.callOutputChars;
788
- const projectedAxis = rs.budget.projectedOverBudget();
789
- if (projectedAxis !== undefined) {
790
- rs.limits.budgetHit = "exceeded";
791
- rs.limits.budgetAxis = projectedAxis;
792
- void prepared.harness.abort();
793
- prepared.abortController.abort();
794
- }
795
- }
796
- }
797
- };
798
- const onMessageEnd = (event) => {
799
- const m = event.message;
800
- if (event.entryId !== undefined && (m.role === "user" || m.role === "assistant" || m.role === "toolResult")) {
801
- emitCommitted(event.entryId, m.role, m.role === "toolResult" ? m.toolCallId : undefined);
802
- }
803
- if (event.entryId !== undefined && m.role === "user")
804
- prepared.fileHistoryBoundary?.begin(event.entryId);
805
- if (event.entryId !== undefined && m.role === "user" && prepared.gitStatusRef.pendingReceipt !== undefined) {
806
- const pr = prepared.gitStatusRef.pendingReceipt;
807
- const c = m.content;
808
- const text = typeof c === "string"
809
- ? c
810
- : Array.isArray(c) && c.length >= 1 && c[0].type === "text"
811
- ? c[0].text
812
- : undefined;
813
- if (text === pr.text) {
814
- delete prepared.gitStatusRef.pendingReceipt;
815
- pr.commit(event.entryId);
816
- }
817
- }
818
- if (rs.degrade.degraded === undefined && m.role === "assistant") {
819
- const deg = readDegradation(m);
820
- if (deg)
821
- rs.degrade.recordDegraded({ from: deg.from, to: deg.to, reason: deg.reason, atTurn: stats.turns + 1, ...(deg.chain && { chain: deg.chain }) });
822
- }
823
- if (m.role === "assistant" && "usage" in m && m.usage) {
824
- if (m.usageMissing)
825
- rs.turn.turnUsageMissing = true;
826
- const u = m.usage;
827
- stats.tokens += u.totalTokens || 0;
828
- const { totalInputTokens: turnTotal, uncachedInputTokens: turnUncached, costMicroUsd: turnCostMicroUsd, } = usageCostMicroUsd(rs.telemetry.cacheFamily, u, rs.telemetry.pricing);
829
- if (!rs.telemetry.pricingConfigured)
830
- rs.telemetry.unpricedSpend = true;
831
- stats.promptTokens += turnUncached;
832
- stats.totalInputTokens += turnTotal;
833
- stats.cachedTokens += u.cacheRead || 0;
834
- stats.cacheWriteTokens += u.cacheWrite || 0;
835
- stats.outputTokens += u.output || 0;
836
- stats.costMicroUsd += turnCostMicroUsd;
837
- rs.turn.turnUsage ??= { inputTokens: 0, totalInputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, costMicroUsd: 0 };
838
- rs.turn.turnUsage.inputTokens += turnUncached;
839
- rs.turn.turnUsage.totalInputTokens += turnTotal;
840
- rs.turn.turnUsage.outputTokens += u.output || 0;
841
- rs.turn.turnUsage.cacheReadTokens += u.cacheRead || 0;
842
- rs.turn.turnUsage.cacheWriteTokens += u.cacheWrite || 0;
843
- rs.turn.turnUsage.costMicroUsd += turnCostMicroUsd;
844
- const callEndAt = Date.now();
845
- const callIssuedAt = prepared.callIssuedAtRef.current;
846
- prepared.callIssuedAtRef.current = undefined;
847
- emitTrace(rs.telemetry.tracer, () => ({
848
- kind: "brain.call",
849
- version: 1,
850
- taskId: rs.telemetry.taskId,
851
- model: m.model,
852
- provider: m.provider,
853
- turn: stats.turns + 1,
854
- promptTokens: turnUncached,
855
- totalInputTokens: turnTotal,
856
- completionTokens: u.output || 0,
857
- cacheRead: u.cacheRead || 0,
858
- cacheWrite: u.cacheWrite || 0,
859
- latencyMs: rs.turn.callStartAt !== undefined ? callEndAt - rs.turn.callStartAt : 0,
860
- firstTokenMs: rs.turn.callStartAt !== undefined && rs.turn.firstTokenAt !== undefined ? rs.turn.firstTokenAt - rs.turn.callStartAt : undefined,
861
- ...(callIssuedAt !== undefined ? { callStartedAt: callIssuedAt } : {}),
862
- ...(rs.telemetry.pricingConfigured ? { costMicroUsd: turnCostMicroUsd } : {}),
863
- ...(m.role === "assistant" && typeof m.stopReason === "string"
864
- ? { stopReason: m.stopReason }
865
- : {}),
866
- ts: callEndAt,
867
- }));
868
- const cacheRowUnknown = m.usageMissing === true || m.stopReason === "aborted";
869
- if (prepared.cacheBreakDetector && prepared.cacheFingerprint && !cacheRowUnknown) {
870
- const finding = prepared.cacheBreakDetector.observe({
871
- turn: stats.turns + 1,
872
- systemPrompt: prepared.cacheFingerprint.systemPrompt,
873
- tools: prepared.cacheFingerprint.tools,
874
- modelKey: `${m.provider}:${m.model}`,
875
- cacheRead: u.cacheRead || 0,
876
- });
877
- if (finding) {
878
- rs.telemetry.cacheBreakReported = true;
879
- deps.runnerHooks.onError?.(new Error(`prompt-cache: break at turn ${finding.turn} — cache_read ${finding.cacheReadBefore}→${finding.cacheReadAfter}; ${finding.detail}. See design/31.`), { phase: "prompt-cache", sessionId: prepared.sessionId, classification: finding.cause });
880
- }
881
- }
882
- }
883
- if (m.role === "assistant" && typeof m.stopReason === "string") {
884
- rs.turn.turnStopReason = m.stopReason;
885
- }
886
- if (m.role === "assistant") {
887
- const rep = m.repetition;
888
- if (rep !== undefined) {
889
- const observed = [
890
- ...(rep.cut !== undefined ? [{ action: "cut", e: rep.cut }] : []),
891
- ...(rep.spared ?? []).map((e) => ({ action: "spared", e })),
892
- ];
893
- for (const { action, e } of observed) {
894
- if (action === "cut")
895
- rs.counters.repetitionCuts += 1;
896
- else
897
- rs.counters.repetitionSpared += 1;
898
- if (rs.counters.repetitionEvents.length < rs.counters.REPETITION_EVENTS_CAP) {
899
- rs.counters.repetitionEvents.push({ turn: stats.turns + 1, action, rule: e.rule, period: e.period, reps: e.reps, segment: e.segment });
900
- }
901
- emitTrace(rs.telemetry.tracer, () => ({
902
- kind: "repetition.detected",
903
- version: 1,
904
- taskId: rs.telemetry.taskId,
905
- turn: stats.turns + 1,
906
- action,
907
- rule: e.rule,
908
- period: e.period,
909
- reps: e.reps,
910
- segment: e.segment,
911
- ts: Date.now(),
912
- }));
913
- }
914
- }
915
- }
916
- if (m.role === "assistant") {
917
- const content = m.content;
918
- if (rs.attach.attachState !== undefined)
919
- rs.counters.cadenceTurns = advanceCadenceClock(rs.attach.attachState, rs.counters.cadenceTurns, content, writeFamilyOf);
920
- rs.turn.lastTurnHadToolCalls = Array.isArray(content) && content.some((c) => c?.type === "toolCall");
921
- }
922
- rs.turn.callStartAt = undefined;
923
- rs.turn.firstTokenAt = undefined;
924
- rs.budget.callOutputChars = 0;
925
- rs.budget.lastStreamBudgetCheck = 0;
926
- };
927
- const onToolStart = (event) => {
928
- const startAt = Date.now();
929
- toolStartAt.set(event.toolCallId, startAt);
930
- startedToolCallIds.add(event.toolCallId);
931
- batchArgs?.set(event.toolCallId, event.args);
932
- stats.toolCalls += 1;
933
- if (spec.finalVerification === true && !(rs.counters.finalVerifyInjections === 0 ? rs.counters.groundingSignalPreR9 : rs.counters.groundingSignalPostR9) && hasVerifiableStructureSignal(event.args)) {
934
- if (rs.counters.finalVerifyInjections === 0)
935
- rs.counters.groundingSignalPreR9 = true;
936
- else
937
- rs.counters.groundingSignalPostR9 = true;
938
- }
939
- const activityArg = primaryActivityArg(event.args);
940
- internalsNotifier.notify(() => observeThenableRejection(internals?.onActivity?.({ phase: "start", toolCallId: event.toolCallId, toolName: event.toolName, at: startAt, ...(activityArg !== undefined ? { arg: activityArg } : {}) }), internalsNotifier, "runtask.onActivity.start"), "runtask.onActivity.start");
941
- pushContent({
942
- type: "tool_start",
943
- toolCallId: event.toolCallId,
944
- toolName: event.toolName,
945
- ...(toolLabels.get(event.toolName) !== undefined ? { label: toolLabels.get(event.toolName) } : {}),
946
- args: event.args,
947
- ...ident(),
948
- });
949
- };
950
- const onToolEnd = (event) => {
951
- const toolStarted = toolStartAt.get(event.toolCallId);
952
- toolStartAt.delete(event.toolCallId);
953
- const toolNow = Date.now();
954
- if (!rs.counters.wroteThisRun && (prepared.toolEffects.get(event.toolName) ?? "write") !== "read") {
955
- rs.counters.wroteThisRun = true;
956
- }
957
- if (rs.attach.attachState !== undefined && !event.isError) {
958
- const det = event.result?.details;
959
- if (det !== undefined)
960
- reduceToolEnd(rs.attach.attachState, det);
961
- }
962
- if (postToolBatchHook !== undefined) {
963
- if (prepared.blockedToolCalls.delete(event.toolCallId) || event.notExecuted === true) {
964
- batchArgs?.delete(event.toolCallId);
965
- }
966
- else {
967
- const digest = batchResponseDigest(event.result);
968
- rs.turn.toolBatch.push({
969
- toolName: event.toolName,
970
- input: batchArgs?.get(event.toolCallId),
971
- toolCallId: event.toolCallId,
972
- ...(digest !== undefined ? { response: digest } : {}),
973
- isError: event.isError,
974
- });
975
- batchArgs?.delete(event.toolCallId);
976
- }
977
- }
978
- emitTrace(rs.telemetry.tracer, () => ({
979
- kind: "tool.call",
980
- version: 1,
981
- taskId: rs.telemetry.taskId,
982
- name: event.toolName,
983
- toolCallId: event.toolCallId,
984
- turn: stats.turns + 1,
985
- durationMs: toolStarted !== undefined ? toolNow - toolStarted : 0,
986
- ok: !event.isError,
987
- ts: toolNow,
988
- }));
989
- internalsNotifier.notify(() => observeThenableRejection(internals?.onActivity?.({ phase: "end", toolCallId: event.toolCallId, toolName: event.toolName, isError: event.isError, at: toolNow }), internalsNotifier, "runtask.onActivity.end"), "runtask.onActivity.end");
990
- if (prepared.lspDiagnostics && !event.isError) {
991
- const name = event.toolName;
992
- if (name === "Edit" || name === "Write" || name === "NotebookEdit") {
993
- const d = event.result?.details;
994
- const p = typeof d?.filePath === "string" ? d.filePath : typeof d?.notebookPath === "string" ? d.notebookPath : undefined;
995
- if (p)
996
- prepared.lspDiagnostics.nudge(p);
997
- }
998
- }
999
- const gate = prepared.gateOutcomes.get(event.toolCallId);
1000
- if (gate !== undefined)
1001
- prepared.gateOutcomes.delete(event.toolCallId);
1002
- pushContent({
1003
- type: "tool_end",
1004
- toolCallId: event.toolCallId,
1005
- toolName: event.toolName,
1006
- ...(toolLabels.get(event.toolName) !== undefined ? { label: toolLabels.get(event.toolName) } : {}),
1007
- isError: event.isError,
1008
- ...toolEndBodyFrom(event.result, event.isError, gate, prepared.abortController.signal.aborted ? gatedCallIdOf(prepared) : undefined),
1009
- ...ident(),
1010
- });
1011
- announceWorkspaceMove();
1012
- if (spec.outputSchema && event.toolName === OUTPUT_TOOL_NAME) {
1013
- if (event.isError) {
1014
- rs.degrade.outputErrorStreak += 1;
1015
- if (rs.degrade.outputErrorStreak > rs.limits.outputRetryCap) {
1016
- rs.degrade.outputInvalid = true;
1017
- void prepared.harness.abort();
1018
- prepared.abortController.abort();
1019
- }
1020
- }
1021
- else {
1022
- rs.degrade.outputErrorStreak = 0;
1023
- }
1024
- }
1025
- };
1026
- const onTurnEnd = () => {
1027
- stats.turns += 1;
1028
- const stopExtra = rs.turn.turnStopReason !== undefined ? { stopReason: rs.turn.turnStopReason } : {};
1029
- queue.push(rs.turn.turnUsage
1030
- ? { type: "turn_end", usage: rs.turn.turnUsage, ...(rs.turn.turnUsageMissing ? { usageMissing: true } : {}), ...stopExtra, ...ident() }
1031
- : { type: "turn_end", usageMissing: true, ...stopExtra, ...ident() });
1032
- rs.turn.turnUsageMissing = false;
1033
- rs.turn.turnStopReason = undefined;
1034
- if (parentToolCallId !== undefined) {
1035
- const progress = {
1036
- type: "task_progress",
1037
- taskId: rs.telemetry.taskId,
1038
- ...(internals?.delegationTaskType !== undefined ? { taskType: internals.delegationTaskType } : {}),
1039
- ...(internals?.cycleSeq !== undefined ? { seq: internals.cycleSeq } : {}),
1040
- ...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}),
1041
- ...(subagentName ? { name: subagentName } : {}),
1042
- model: prepared.model.id,
1043
- usage: { totalTokens: stats.tokens, toolUses: stats.toolCalls, durationMs: Date.now() - rs.telemetry.taskStart },
1044
- status: "running",
1045
- ...ident(),
1046
- };
1047
- queue.push(progress);
1048
- try {
1049
- internals?.onForwardEvent?.(progress);
1050
- }
1051
- catch {
1052
- }
1053
- }
1054
- emitTrace(rs.telemetry.tracer, () => ({ kind: "turn.end", version: 1, taskId: rs.telemetry.taskId, turn: stats.turns, ts: Date.now() }));
1055
- rs.turn.turnUsage = undefined;
1056
- rs.turn.callStartAt = undefined;
1057
- rs.turn.firstTokenAt = undefined;
1058
- rs.budget.callOutputChars = 0;
1059
- rs.budget.lastStreamBudgetCheck = 0;
1060
- toolStartAt.clear();
1061
- if (rs.limits.effectiveMaxTurns !== undefined && rs.limits.effectiveMaxTurns > 0 && stats.turns >= rs.limits.effectiveMaxTurns) {
1062
- rs.limits.turnsExceeded = true;
1063
- if (prepared.suspendForResource === undefined) {
1064
- void prepared.harness.abort();
1065
- prepared.abortController.abort();
1066
- }
1067
- }
1068
- else {
1069
- if (rs.degrade.degraded === undefined &&
1070
- spec.limits?.degrade &&
1071
- rs.degrade.degradeToModel &&
1072
- rs.budget.maxCostMicroUsd !== undefined &&
1073
- stats.costMicroUsd >= spec.limits.degrade.atCostFraction * rs.budget.maxCostMicroUsd) {
1074
- void prepared.harness.setModel(rs.degrade.degradeToModel);
1075
- rs.degrade.recordDegraded({ from: prepared.model.id, to: rs.degrade.degradeToModel.id, reason: "budget", atTurn: stats.turns }, rs.degrade.degradeToModel);
1076
- }
1077
- const overAxis = rs.limits.budgetHit ? undefined : rs.budget.overBudget();
1078
- if (overAxis !== undefined) {
1079
- rs.limits.budgetHit = "exceeded";
1080
- rs.limits.budgetAxis = overAxis;
1081
- if (prepared.suspendForResource === undefined) {
1082
- void prepared.harness.abort();
1083
- prepared.abortController.abort();
1084
- }
1085
- }
1086
- }
1087
- };
1088
- return { onMessageUpdate, onMessageEnd, onToolStart, onToolEnd, onTurnEnd, announceWorkspaceMove };
1089
- }
1090
- const REFUSED_DECIDE_KEYS = [
1091
- ["gateOutcome", "the settlement record is minted by core from the host's decision facts and the row's origin, never supplied"],
1092
- ["settledBy", 'a retired key — the host supplies decision FACTS as hostDecision{decidedBy: "person" | "sla_timeout", approver?} and core mints the settlement word from them'],
1093
- ["approver", "a retired key — the attribution rides the facts it belongs to, as hostDecision.approver"],
1094
- ];
1095
80
  export class Runner {
1096
81
  deps;
1097
82
  sessions;
@@ -1102,7 +87,10 @@ export class Runner {
1102
87
  suspendedEnvReaps = new Map();
1103
88
  locallyClaimedTokens = new Set();
1104
89
  static PARENT_CONSTRAINT_REGISTRY_CAP = 1024;
90
+ depsSeat;
1105
91
  constructor(deps) {
92
+ const runner = this;
93
+ this.depsSeat = { get deps() { return runner.deps; } };
1106
94
  this.deps = deps;
1107
95
  if (this.deps.tiers) {
1108
96
  this.deps = { ...this.deps, models: expandTiers(this.deps.models, this.deps.tiers) };
@@ -1348,7 +336,7 @@ export class Runner {
1348
336
  })();
1349
337
  const settled = run.catch(async (err) => {
1350
338
  let code = errorCodeOf(err);
1351
- const reopenReason = code === "resume.tool_unavailable" ? "tool_unavailable" : "env_failed";
339
+ const reopenReason = code === "resume.tool_unavailable" || code === "resume.tool_contract_mismatch" ? "tool_unavailable" : "env_failed";
1352
340
  const reopenable = resume !== undefined && resume.pendingActionStarted !== true && reopenReason !== undefined && !(resumeDecisionWasNegative(resume) && resume.decisionDelivered === true);
1353
341
  let reopenCommitted = false;
1354
342
  let reopenStateUnknown = false;
@@ -1442,6 +430,8 @@ export class Runner {
1442
430
  onError: (f) => console.warn(`[sema-core] ${f.site}: delegation-lifecycle observer threw (contained; further failures counted, not re-disclosed): ${f.error.message}`),
1443
431
  }), "runtask.onDelegationLifecycle");
1444
432
  }
433
+ if (forwardsSubagentEvents(spec))
434
+ await drainForwardedFramesBeforeDone();
1445
435
  queue.push({ type: "done", result: resultValue });
1446
436
  queue.close();
1447
437
  });
@@ -1992,6 +982,7 @@ export class Runner {
1992
982
  const ident = () => parentToolCallId !== undefined ? { eventId: uuidv7(), parentToolCallId, sourceTaskId: runSourceTaskId } : { eventId: uuidv7() };
1993
983
  notificationIdent = ident;
1994
984
  queue.push({ type: "wiring_manifest", manifest: prepared.wiringManifest, ...ident() });
985
+ prepared.toolRosterDeltas.subscribe((delta) => queue.push({ type: "tool_roster_delta", delta, ...ident() }));
1995
986
  const delegationLifecycleNotifier = createSafeNotifier({
1996
987
  onError: (f) => console.warn(`[sema-core] ${f.site}: delegation-lifecycle observer threw (contained; further failures counted, not re-disclosed): ${f.error.message}`),
1997
988
  });
@@ -2553,17 +1544,20 @@ export class Runner {
2553
1544
  const withBrainSinks = (fn) => runWithStatusSink(statusEmit, () => runWithBrainTelemetry(telemetryEmit, () => runWithReasoningWireFacts(observeReasoningWireFacts, fn)));
2554
1545
  const toolLabels = new Map(prepared.tools.flatMap((t) => (t.label !== undefined && t.label !== t.name ? [[t.name, t.label]] : [])));
2555
1546
  for (const orphan of prepared.wakeRecovered) {
2556
- queue.push({ type: "tool_end", toolCallId: orphan.toolCallId, toolName: orphan.toolName, ...(toolLabels.has(orphan.toolName) ? { label: toolLabels.get(orphan.toolName) } : {}), isError: true, ...reconciledToolEndBody(orphan), ...ident() });
1547
+ queue.push({ type: "tool_end", toolCallId: orphan.toolCallId, toolName: orphan.toolName, ...(toolLabels.has(orphan.toolName) ? { label: toolLabels.get(orphan.toolName) } : {}), isError: true, ...reconciledToolEndBody(orphan, prepared.structuredProjector), ...ident() });
2557
1548
  emitCommitted(orphan.entryId, "toolResult", orphan.toolCallId);
2558
1549
  }
2559
1550
  const postToolBatchHook = (spec.hooks ?? this.deps.hooks)?.postToolBatch;
2560
1551
  const batchArgs = postToolBatchHook ? new Map() : undefined;
2561
1552
  rs.turn.toolBatch = [];
2562
- const { onMessageUpdate, onMessageEnd, onToolStart, onToolEnd, onTurnEnd, announceWorkspaceMove } = makeHarnessHandlers(prepared, stats, rs, {
2563
- spec, queue, internals, ident, parentToolCallId, subagentName,
2564
- pushContent, emitCommitted, startedToolCallIds, toolStartAt, writeFamilyOf,
2565
- toolLabels, postToolBatchHook, batchArgs,
2566
- runnerHooks: { onError: (e, info) => this.deps.onError?.(e, info) },
1553
+ const { onMessageUpdate, onMessageEnd, onToolStart, onToolEnd, onTurnEnd, announceWorkspaceMove } = createHarnessHandlers({
1554
+ prepared, stats, rs,
1555
+ deps: {
1556
+ spec, queue, internals, ident, parentToolCallId, subagentName,
1557
+ pushContent, emitCommitted, startedToolCallIds, toolStartAt, writeFamilyOf,
1558
+ toolLabels, postToolBatchHook, batchArgs,
1559
+ runnerHooks: { onError: (e, info) => this.deps.onError?.(e, info) },
1560
+ },
2567
1561
  });
2568
1562
  const recordCompactionUsage = (m, msg) => {
2569
1563
  const u = msg?.usage;
@@ -2977,16 +1971,19 @@ export class Runner {
2977
1971
  for (const w of manualCompactRef.waiters.splice(0))
2978
1972
  w.resolve(outcome);
2979
1973
  };
2980
- const onTurnBoundary = makeTurnBoundary(prepared, stats, rs, {
2981
- spec, queue, loopLatch, manualCompactRef, todoToolMounted, taskToolsMounted,
2982
- walltimeMonotonicDeadline,
2983
- timeout, ident, postToolBatchHook, batchArgs, compactionBrain, withinTaskCompaction,
2984
- compactionBreaker, windowSafetyOptions, rapidRefill, drainManualCompact,
2985
- runnerHooks: {
2986
- onError: this.deps.onError,
2987
- seamCCompactionOptions: (p) => this.seamCCompactionOptions(p),
2988
- compactionHookOptions: (s, sid, trig) => this.compactionHookOptions(s, sid, trig, prepared.hookIdentity, { timeoutMs: prepared.hookTimeoutMs, signal: prepared.abortController.signal }),
2989
- recordCompactionReuse: (p, c) => this.recordCompactionReuse(p, c),
1974
+ const onTurnBoundary = createTurnBoundary({
1975
+ prepared, stats, rs,
1976
+ deps: {
1977
+ spec, queue, loopLatch, manualCompactRef, todoToolMounted, taskToolsMounted,
1978
+ walltimeMonotonicDeadline,
1979
+ timeout, ident, postToolBatchHook, batchArgs, compactionBrain, withinTaskCompaction,
1980
+ compactionBreaker, windowSafetyOptions, rapidRefill, drainManualCompact,
1981
+ runnerHooks: {
1982
+ onError: this.deps.onError,
1983
+ seamCCompactionOptions: (p) => this.seamCCompactionOptions(p),
1984
+ compactionHookOptions: (s, sid, trig) => this.compactionHookOptions(s, sid, trig, prepared.hookIdentity, { timeoutMs: prepared.hookTimeoutMs, signal: prepared.abortController.signal }),
1985
+ recordCompactionReuse: (p, c) => this.recordCompactionReuse(p, c),
1986
+ },
2990
1987
  },
2991
1988
  });
2992
1989
  prepared.gitStatusRef.reassert = async () => {
@@ -3496,7 +2493,7 @@ export class Runner {
3496
2493
  }
3497
2494
  catch (err) {
3498
2495
  loopLatch.ended = true;
3499
- if (errorCodeOf(err) === "resume.tool_unavailable") {
2496
+ if (errorCodeOf(err) === "resume.tool_unavailable" || errorCodeOf(err) === "resume.tool_contract_mismatch") {
3500
2497
  await settleTeardownLeg(() => prepared.mcp.dispose(), "mcp.dispose (tool_unavailable leg)", (e) => this.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
3501
2498
  await settleTeardownLeg(() => prepared.a2a?.dispose(), "a2a.dispose (tool_unavailable leg)", (e) => this.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
3502
2499
  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 }));
@@ -3519,6 +2516,7 @@ export class Runner {
3519
2516
  }
3520
2517
  notificationLaneLive = false;
3521
2518
  unsubscribeTaskNotifications();
2519
+ prepared.toolRosterDeltas.unsubscribe();
3522
2520
  unsubGitRetry();
3523
2521
  unsubBoundary();
3524
2522
  unsub();
@@ -3551,7 +2549,7 @@ export class Runner {
3551
2549
  const report = await reconcileInterruptedSession(prepared.session, prepared.toolEffects, undefined, startedToolCallIds);
3552
2550
  orphansClosed = report.recovered.length;
3553
2551
  for (const orphan of report.recovered) {
3554
- queue.push({ type: "tool_end", toolCallId: orphan.toolCallId, toolName: orphan.toolName, ...(toolLabels.has(orphan.toolName) ? { label: toolLabels.get(orphan.toolName) } : {}), isError: true, ...reconciledToolEndBody(orphan), ...ident() });
2552
+ queue.push({ type: "tool_end", toolCallId: orphan.toolCallId, toolName: orphan.toolName, ...(toolLabels.has(orphan.toolName) ? { label: toolLabels.get(orphan.toolName) } : {}), isError: true, ...reconciledToolEndBody(orphan, prepared.structuredProjector), ...ident() });
3555
2553
  emitCommitted(orphan.entryId, "toolResult", orphan.toolCallId);
3556
2554
  }
3557
2555
  reconcileComplete = true;
@@ -3881,6 +2879,8 @@ export class Runner {
3881
2879
  drainManualCompact("mooted");
3882
2880
  }
3883
2881
  manualCompactRef.emitMooted = undefined;
2882
+ if (forwardsSubagentEvents(spec))
2883
+ await drainForwardedFramesBeforeDone();
3884
2884
  queue.push({ type: "done", result });
3885
2885
  queue.close();
3886
2886
  await prepared.fileHistoryBoundary?.settle();
@@ -4022,718 +3022,41 @@ export class Runner {
4022
3022
  return stream.result();
4023
3023
  }
4024
3024
  async resumeStream(token, outcome, taskConfig, internals) {
4025
- if (outcome === null || typeof outcome !== "object") {
4026
- throw new CheckpointError("checkpoint.invalid_outcome", `resume outcome must be an object naming its gate (got ${outcome === null ? "null" : typeof outcome})`);
4027
- }
4028
- const config = typeof taskConfig === "object" && taskConfig !== null ? { ...taskConfig } : taskConfig;
4029
- internals = internals !== undefined ? { ...internals } : undefined;
4030
- const store = resolveCheckpointStore(config, this.deps);
4031
- if (!store) {
4032
- throw new CheckpointError("checkpoint.not_found", "no CheckpointStore wired — cannot resume (set RunnerDeps.checkpointStore or taskConfig.checkpointStore)");
4033
- }
4034
- let cp = await store.get(token);
4035
- if (!cp) {
4036
- throw new CheckpointError("checkpoint.not_found", "no checkpoint found for the supplied token");
4037
- }
4038
- if (cp.status !== "pending") {
4039
- const live = await store.get(token);
4040
- if (live?.status === "pending") {
4041
- throw new CheckpointError("checkpoint.reopened_concurrently", "checkpoint changed concurrently (a resolve/reopen cycle landed between this resume's read of the row and its state check) — nothing was validated or executed; re-resume against the current state");
4042
- }
4043
- throw new CheckpointError("checkpoint.already_resolved", `checkpoint is ${describeSuppliedValue(live?.status ?? cp.status)} — the token is consumed, so this resume was neither validated nor executed (idempotent); a different outcome cannot redeem it`);
4044
- }
4045
- if ((internals?.inheritedGate?.parentConstraints?.length ?? 0) === 0) {
4046
- const parked = this.parentConstraintRegistry.get(token);
4047
- if (parked !== undefined) {
4048
- internals = {
4049
- ...(internals ?? {}),
4050
- inheritedGate: { ...(internals?.inheritedGate ?? {}), parentConstraints: parked },
4051
- };
4052
- }
4053
- }
4054
- let wakeMessage;
4055
- let wakeHookContext;
4056
- const outcomeGate = outcome.gate;
4057
- const resumeSignal = config.signal;
4058
- let suppliedMessage;
4059
- if (outcomeGate === "wake") {
4060
- suppliedMessage = outcome.message;
4061
- const gk = cp.gate.kind;
4062
- const decideEntry = gk === "human" || gk === "irreversible_ask"
4063
- ? 'a `policy_ask` outcome (decision allow/deny bound to the pending tool call)'
4064
- : gk === "needs_review"
4065
- ? 'a `dry_run_review` outcome (decision approve/reject)'
4066
- : gk === "plan_review"
4067
- ? 'a `plan_review` outcome (decision approve/edit/reject)'
4068
- : gk === "resource_limit"
4069
- ? 'a `resource_limit` outcome (decision "continue")'
4070
- : undefined;
4071
- if (gk !== "task_done") {
4072
- throw new CheckpointError("wake.gate_pending", `cannot wake: this checkpoint awaits a pending gate decision (gate "${gk}") — wake is not a decision ` +
4073
- `channel; resume it with ${decideEntry ?? "its own gate outcome"} instead`);
4074
- }
4075
- const pa = cp.pendingAction;
4076
- switch (pa.kind) {
4077
- case "task_done":
4078
- break;
4079
- case "tool_approval":
4080
- case "resource_limit":
4081
- case "review":
4082
- case "plan_review": {
4083
- throw new CheckpointError("wake.gate_pending", `cannot wake: this checkpoint still carries a pending gate-typed action (pendingAction "${pa.kind}" ` +
4084
- `on a "${gk}" gate — inconsistent row) — wake is not a decision channel; resolve it through its ` +
4085
- `own decide entry instead`);
4086
- }
4087
- default: {
4088
- const _exhaustive = pa;
4089
- void _exhaustive;
4090
- throw new CheckpointError("wake.gate_pending", `cannot wake: this checkpoint carries an unrecognized pendingAction kind "${pa.kind}" ` +
4091
- `(newer-version row?) — fail-closed: wake only serves a pure park (pendingAction "task_done")`);
4092
- }
4093
- }
4094
- if (suppliedMessage !== undefined) {
4095
- wakeMessage = validatePendingSteer(suppliedMessage);
4096
- }
4097
- else if (readPendingSteerQueue(cp.state).length === 0) {
4098
- throw new CheckpointError("wake.nothing_to_deliver", "cannot wake: no message was supplied and the checkpoint holds no parked pendingSteer — an empty " +
4099
- "wake would burn the checkpoint on a blank continuation; supply `message` or park a steer first");
4100
- }
4101
- }
4102
- const gateMatch = isGateKind(cp.gate.kind) && resumeGateMatches(cp.gate.kind, outcomeGate);
4103
- if (!gateMatch) {
4104
- throw new CheckpointError("checkpoint.gate_mismatch", `resume outcome (gate "${describeSuppliedValue(outcomeGate)}") does not match checkpoint gate "${cp.gate.kind}" — resume serves ${Object.entries(PAUSE_REGISTRY)
4105
- .filter(([, row]) => !row.terminal)
4106
- .map(([k, row]) => `${k}/${row.resume}`)
4107
- .join(", ")}`);
4108
- }
4109
- let plainReviewOutcome;
4110
- if (cp.gate.kind === "plan_review") {
4111
- const decide = outcome;
4112
- const decision = decide.decision;
4113
- const editedPlan = decide.editedPlan;
4114
- const reason = decide.reason;
4115
- if (decision !== "approve" && decision !== "edit" && decision !== "reject") {
4116
- throw new CheckpointError("checkpoint.invalid_outcome", `resume decision "${describeSuppliedValue(decision)}" is outside the plan_review domain — a plan review is exactly "approve", "edit" or "reject"; refusing pre-CAS, the checkpoint stays pending`);
4117
- }
4118
- assertOutcomeText(editedPlan, "editedPlan");
4119
- assertOutcomeText(reason, "reason");
4120
- plainReviewOutcome = {
4121
- gate: "plan_review",
4122
- decision,
4123
- ...(editedPlan !== undefined ? { editedPlan } : {}),
4124
- ...(reason !== undefined ? { reason } : {}),
4125
- };
4126
- }
4127
- else if (cp.gate.kind === "needs_review") {
4128
- const decide = outcome;
4129
- const decision = decide.decision;
4130
- const reason = decide.reason;
4131
- if (decision !== "approve" && decision !== "reject") {
4132
- throw new CheckpointError("checkpoint.invalid_outcome", `resume decision "${describeSuppliedValue(decision)}" is outside the dry_run_review domain — a dry-run review is exactly "approve" or "reject"; refusing pre-CAS, the checkpoint stays pending`);
4133
- }
4134
- assertOutcomeText(reason, "reason");
4135
- plainReviewOutcome = {
4136
- gate: "dry_run_review",
4137
- decision,
4138
- ...(reason !== undefined ? { reason } : {}),
4139
- };
4140
- }
4141
- if (plainReviewOutcome !== undefined) {
4142
- const reason = plainReviewOutcome.reason;
4143
- if (plainReviewOutcome.decision === "reject" && reason && sanitizeUntrustedText(reason) !== reason) {
4144
- throw new CheckpointError("checkpoint.invalid_outcome", "resume deny/reject reason must not contain a </system-reminder> tag");
4145
- }
4146
- const capturedPlan = plainReviewOutcome.gate === "plan_review" ? plainReviewOutcome.editedPlan : undefined;
4147
- if (plainReviewOutcome.decision === "edit" &&
4148
- capturedPlan &&
4149
- sanitizeUntrustedText(capturedPlan) !== capturedPlan) {
4150
- throw new CheckpointError("checkpoint.invalid_outcome", "resume editedPlan must not contain a </system-reminder> tag");
4151
- }
4152
- if (cp.reopenReason === "env_failed") {
4153
- const winner = cp.resolvedOutcome;
4154
- if (winner === undefined) {
4155
- throw new CheckpointError("checkpoint.reopen_revote", "review checkpoint was reopened after an env-restore failure (env_failed) but carries no persisted winner to replay — refusing to resume (inconsistent row, fail-closed)");
4156
- }
4157
- if (winner.decision !== plainReviewOutcome.decision) {
4158
- throw new CheckpointError("checkpoint.reopen_revote", `an env_failed reopen replays the ALREADY-RECORDED review decision ("${winner.decision}") — refusing a re-vote with a different decision ("${plainReviewOutcome.decision}")`);
4159
- }
4160
- if (winner.updatedInput !== capturedPlan) {
4161
- throw new CheckpointError("checkpoint.reopen_revote", "an env_failed reopen replays the ALREADY-RECORDED review decision — refusing a re-vote whose edited plan differs from the recorded one");
4162
- }
4163
- if (winner.reason !== undefined && winner.reason !== plainReviewOutcome.reason) {
4164
- throw new CheckpointError("checkpoint.reopen_revote", "an env_failed reopen replays the ALREADY-RECORDED review decision — refusing a re-vote whose reviewer note differs from the recorded one");
4165
- }
4166
- }
4167
- }
4168
- let plainParkOutcome;
4169
- if (outcomeGate === "wake") {
4170
- plainParkOutcome = {
4171
- gate: "wake",
4172
- ...(wakeMessage !== undefined ? { message: wakeMessage } : {}),
4173
- };
4174
- }
4175
- else if (outcomeGate === "resource_limit") {
4176
- const decision = outcome.decision;
4177
- if (decision !== "continue") {
4178
- throw new CheckpointError("checkpoint.invalid_outcome", `resume decision "${describeSuppliedValue(decision)}" is outside the resource_limit domain — a slice resume is exactly "continue"; refusing pre-CAS, the checkpoint stays pending`);
4179
- }
4180
- plainParkOutcome = { gate: "resource_limit", decision };
4181
- }
4182
- let suppliedAnswer;
4183
- let redeemedAnswer;
4184
- let plainPolicyOutcome;
4185
- if (cp.gate.kind === "human" || cp.gate.kind === "irreversible_ask") {
4186
- const decide = outcome;
4187
- suppliedAnswer = decide.answer;
4188
- if (suppliedAnswer !== undefined) {
4189
- const reading = classifyQuestionOutcome(suppliedAnswer);
4190
- if (reading.shape !== "answered") {
4191
- throw new CheckpointError("checkpoint.invalid_outcome", "resume carried a content-ask `answer` that does not read as an answer (malformed, sparse, or not capturable as plain data) — " +
4192
- "refusing pre-CAS rather than fabricating an empty human response; the checkpoint stays pending", { field: "answer" });
4193
- }
4194
- redeemedAnswer = reading.answer;
4195
- }
4196
- const updatedInputRaw = decide.updatedInput;
4197
- let updatedInput;
4198
- try {
4199
- updatedInput = updatedInputRaw === undefined ? undefined : structuredClone(updatedInputRaw);
4200
- }
4201
- catch {
4202
- throw new CheckpointError("checkpoint.invalid_outcome", "resume carried an `updatedInput` that could not be captured as plain data — refusing pre-CAS; the checkpoint stays pending");
4203
- }
4204
- if (Object.is(updatedInput, -0))
4205
- updatedInput = 0;
4206
- if (updatedInput !== undefined && !canonicalizeCapturedPlainData(updatedInput)) {
4207
- throw new CheckpointError("checkpoint.invalid_outcome", "resume carried an `updatedInput` outside the plain-JSON value domain (a Map/Set/Date/buffer, a sparse or expando-carrying array, a non-finite number, or a cycle) — " +
4208
- "such a value reads differently on the two sides of the durable store; refusing pre-CAS, the checkpoint stays pending");
4209
- }
4210
- const decision = decide.decision;
4211
- if (decision !== "allow" && decision !== "deny") {
4212
- throw new CheckpointError("checkpoint.invalid_outcome", `resume decision "${describeSuppliedValue(decision)}" is outside the policy_ask domain — a decide is exactly "allow" or "deny"; refusing pre-CAS, the checkpoint stays pending`);
4213
- }
4214
- for (const [key, why] of REFUSED_DECIDE_KEYS) {
4215
- if (Reflect.get(decide, key) !== undefined) {
4216
- throw new CheckpointError("checkpoint.invalid_outcome", `resume carries \`${key}\` — ${why}; refusing pre-CAS, the checkpoint stays pending`, { field: "hostDecision" });
4217
- }
4218
- }
4219
- const hostDecisionRaw = decide.hostDecision;
4220
- if (typeof hostDecisionRaw !== "object" || hostDecisionRaw === null) {
4221
- throw new CheckpointError("checkpoint.invalid_outcome", `resume carries no hostDecision — a policy_ask decide names who decided it ({ decidedBy: "person" | "sla_timeout", approver? }); refusing pre-CAS, the checkpoint stays pending`, { field: "hostDecision" });
4222
- }
4223
- const decidedBy = hostDecisionRaw.decidedBy;
4224
- if (decidedBy !== "person" && decidedBy !== "sla_timeout") {
4225
- throw new CheckpointError("checkpoint.invalid_outcome", `resume hostDecision.decidedBy "${describeSuppliedValue(decidedBy)}" is outside the closed set — it is exactly "person" or "sla_timeout"; refusing pre-CAS, the checkpoint stays pending`, { field: "hostDecision" });
4226
- }
4227
- if (decidedBy === "sla_timeout" && decision === "allow") {
4228
- throw new CheckpointError("checkpoint.invalid_outcome", `resume carries decision "allow" decided by "sla_timeout" — an elapsed SLA window cannot be what approved an action that EXECUTES; refusing pre-CAS, the checkpoint stays pending`, { field: "hostDecision" });
4229
- }
4230
- const attribution = screenApproverAttribution(hostDecisionRaw.approver);
4231
- if (attribution.defect !== undefined) {
4232
- throw new CheckpointError("checkpoint.invalid_outcome", `resume carries an approver attribution this engine refuses: ${attribution.defect}; refusing pre-CAS, the checkpoint stays pending`, { field: "approver" });
4233
- }
4234
- const reason = decide.reason;
4235
- assertOutcomeText(reason, "reason");
4236
- const approverCell = attribution.approver !== undefined ? { approver: attribution.approver } : {};
4237
- plainPolicyOutcome = {
4238
- gate: "policy_ask",
4239
- boundCallId: decide.boundCallId,
4240
- boundInputHash: decide.boundInputHash,
4241
- decision,
4242
- ...(updatedInput !== undefined ? { updatedInput } : {}),
4243
- ...(reason !== undefined ? { reason } : {}),
4244
- ...(redeemedAnswer !== undefined ? { answer: redeemedAnswer } : {}),
4245
- hostDecision: { decidedBy, ...approverCell },
4246
- };
4247
- if (plainPolicyOutcome.decision === "deny" && plainPolicyOutcome.reason && sanitizeUntrustedText(plainPolicyOutcome.reason) !== plainPolicyOutcome.reason) {
4248
- throw new CheckpointError("checkpoint.invalid_outcome", "resume deny/reject reason must not contain a </system-reminder> tag");
4249
- }
4250
- }
4251
- if (plainPolicyOutcome !== undefined) {
4252
- const boundTo = cp.pendingAction.kind === "tool_approval" ? cp.pendingAction.toolCallId : undefined;
4253
- if (plainPolicyOutcome.boundCallId !== boundTo) {
4254
- throw new CheckpointError("checkpoint.invalid_outcome", `resume boundCallId "${plainPolicyOutcome.boundCallId}" does not match the checkpoint's pending tool call "${boundTo ?? "(none)"}" — the decision-action binding (design/80 D-1) failed; refusing to apply a decision bound to a different action`, { field: "boundCallId" });
4255
- }
4256
- const boundHash = cp.pendingAction.kind === "tool_approval" ? cp.pendingAction.boundInputHash : undefined;
4257
- if (boundHash !== undefined) {
4258
- if (plainPolicyOutcome.boundInputHash !== boundHash) {
4259
- throw new CheckpointError("checkpoint.invalid_outcome", "resume boundInputHash does not match the checkpoint's pending tool call input — the decision-action input binding (design/80 D-1 §2) failed; refusing to apply an approval bound to a different input (TOCTOU re-mint guard)", { field: "boundInputHash" });
4260
- }
4261
- }
4262
- else if (checkpointVersionOf(cp) >= BINDING_CHECKPOINT_VERSION) {
4263
- throw new CheckpointError("checkpoint.invalid_outcome", `resume of a binding-version (v${checkpointVersionOf(cp)}) checkpoint whose pending tool call has no persisted boundInputHash — refusing to resume an unbound binding-version checkpoint (corruption / downgrade guard)`);
4264
- }
4265
- if (cp.reopenReason === "env_failed") {
4266
- if (cp.resolvedOutcome === undefined) {
4267
- throw new CheckpointError("checkpoint.reopen_revote", "checkpoint was reopened after an env-restore failure (env_failed) but carries no persisted winner to replay — refusing to resume (inconsistent row, fail-closed)");
4268
- }
4269
- const incoming = winnerFromOutcome(plainPolicyOutcome);
4270
- const persistedWinner = (() => {
4271
- const w = cp.resolvedOutcome;
4272
- if (w?.answer === undefined)
4273
- return w;
4274
- const reading = classifyQuestionOutcome(w.answer);
4275
- return reading.shape === "answered" ? { ...w, answer: reading.answer } : w;
4276
- })();
4277
- if (incoming === undefined || !sameWinner(incoming, persistedWinner)) {
4278
- throw new CheckpointError("checkpoint.reopen_revote", `this checkpoint was reopened after an env-restore failure (env_failed) — a re-resume is a system retry of the already-approved action, not a re-vote; the supplied decision must replay the persisted winner (boundCallId "${cp.resolvedOutcome.boundCallId}", decision "${cp.resolvedOutcome.decision}"), refusing a different decision`);
4279
- }
4280
- }
4281
- const contentAskCallId = pendingContentAskCallId(cp);
4282
- if (suppliedAnswer !== undefined && (contentAskCallId === undefined || plainPolicyOutcome.decision !== "allow")) {
4283
- throw new CheckpointError("checkpoint.invalid_outcome", contentAskCallId === undefined
4284
- ? "resume carried a content-ask `answer` but the checkpoint's pending call is not the reserved question tool — an answer has no consumer on a side-effecting tool's approval; refusing rather than dropping it silently"
4285
- : "resume carried a content-ask `answer` on a `deny` — a denial injects a refusal, never an answer; refusing rather than dropping it silently", { field: "answer" });
4286
- }
4287
- if (contentAskCallId !== undefined && plainPolicyOutcome.decision === "allow" && suppliedAnswer === undefined) {
4288
- if (!isLiveQuestionFace(config.onQuestion ?? this.deps.onQuestion)) {
4289
- throw new CheckpointError("checkpoint.invalid_outcome", "resume approved a content-ask (the reserved question tool) without an `answer`, and this resume has no live answering face — " +
4290
- 'executing the question against nothing would hand the model a fabricated "no human is available" default while consuming the approval; ' +
4291
- "re-resume with the operator's answer on the outcome (or deny it), the checkpoint stays pending", { field: "answer" });
4292
- }
4293
- }
4294
- }
4295
- if (checkpointVersionOf(cp) > MAX_SUPPORTED_CHECKPOINT_VERSION) {
4296
- throw new CheckpointError("checkpoint.unsupported_version", `checkpoint version ${checkpointVersionOf(cp)} is newer than this worker supports (max ${MAX_SUPPORTED_CHECKPOINT_VERSION})`, { reason: "version_newer" });
4297
- }
4298
- const preCasGateBit = cp.gate.kind === "irreversible_ask" ? cp.gate.realApproval : undefined;
4299
- const preCasBitWellFormed = preCasGateBit !== undefined && typeof preCasGateBit === "object" && isAskOrigin(preCasGateBit.origin);
4300
- const faceSaysGoverned = (cp.state.readFace?.realApproval) === true;
4301
- const preCasBitViolation = checkpointVersionOf(cp) === REAL_APPROVAL_CHECKPOINT_VERSION
4302
- ? !preCasBitWellFormed
4303
- : checkpointVersionOf(cp) < REAL_APPROVAL_CHECKPOINT_VERSION
4304
- ? preCasGateBit !== undefined
4305
- : faceSaysGoverned
4306
- ? !preCasBitWellFormed
4307
- : preCasGateBit !== undefined && !preCasBitWellFormed;
4308
- if (preCasBitViolation) {
4309
- throw new CheckpointError("checkpoint.invalid_outcome", checkpointVersionOf(cp) >= REAL_APPROVAL_CHECKPOINT_VERSION
4310
- ? `a v${checkpointVersionOf(cp)} checkpoint's realApproval gate bit must be well-formed (origin a member of the closed ask-origin set)${checkpointVersionOf(cp) === REAL_APPROVAL_CHECKPOINT_VERSION ? " and present on an irreversible_ask gate" : ""} — this row's is not; refusing to resume a damaged real-approval row (corruption / downgrade guard), the checkpoint stays pending`
4311
- : `a v${checkpointVersionOf(cp)} checkpoint carries a realApproval gate bit no release of that version ever minted — refusing to honor a fabricated origin (corruption / forgery guard), the checkpoint stays pending`, { reason: checkpointVersionOf(cp) >= REAL_APPROVAL_CHECKPOINT_VERSION ? "real_approval_damaged" : "real_approval_forged" });
4312
- }
4313
- {
4314
- const faceSectionRaw = cp.state.readFace;
4315
- const faceCandidate = faceSectionRaw !== undefined && typeof faceSectionRaw === "object" && faceSectionRaw !== null
4316
- ? faceSectionRaw
4317
- : undefined;
4318
- const faceWellFormed = faceCandidate !== undefined &&
4319
- (faceCandidate.face === "open" || faceCandidate.face === "roots") &&
4320
- (faceCandidate.realApproval === undefined || faceCandidate.realApproval === true) &&
4321
- (faceCandidate.denyEntries === undefined ||
4322
- (Array.isArray(faceCandidate.denyEntries) &&
4323
- faceCandidate.denyEntries.every((e) => persistedReadDenyEntryProblem(e) === null)));
4324
- if (checkpointVersionOf(cp) >= FACE_CHECKPOINT_VERSION ? !faceWellFormed : faceSectionRaw !== undefined) {
4325
- throw new CheckpointError("checkpoint.invalid_outcome", checkpointVersionOf(cp) >= FACE_CHECKPOINT_VERSION
4326
- ? `a v${checkpointVersionOf(cp)} checkpoint must carry a well-formed read-face section (state.readFace) — this row's is missing or malformed; refusing to resume a damaged read-face row (corruption / downgrade guard), the checkpoint stays pending`
4327
- : `a v${checkpointVersionOf(cp)} checkpoint carries a read-face section no release of that version ever minted — refusing to honor a fabricated read-face state (corruption / forgery guard), the checkpoint stays pending`, { reason: checkpointVersionOf(cp) >= FACE_CHECKPOINT_VERSION ? "read_face_damaged" : "read_face_forged" });
4328
- }
4329
- }
4330
- if (realApprovalOrgFact(preCasGateBit) !== undefined && this.deps.permissionRuleStore?.partitions.org !== true) {
4331
- throw new CheckpointError("checkpoint.unsupported_version", `this checkpoint's approval was minted under organization governance (org ${String(realApprovalOrgFact(preCasGateBit))}, routed as ${String(preCasGateBit?.origin)}) and this worker has no org adjudication wiring (its permission-rule store carries no org partition) — a governed approval may only be redeemed where governance can be enforced; the checkpoint stays pending, resume it on an org-wired worker`, { reason: "governed_unwired" });
4332
- }
4333
- const retiredWalltimeTotal = cp.resourceLedger?.totalWalltimeSec;
4334
- if (retiredWalltimeTotal !== undefined) {
4335
- throw new CheckpointError("checkpoint.walltime_axis_retired", "this checkpoint's ledger carries a cross-slice WALL-CLOCK allocation (resourceLedger.totalWalltimeSec), an axis this engine version retired — " +
4336
- "resuming here would run the leg with that ceiling unenforced. Rejected pre-CAS (the checkpoint stays pending): finish it on a worker of the " +
4337
- "previous release, or start a fresh task bounded by resourceSuspend.totalTokens / totalBudgetUsd.");
4338
- }
4339
- if (resumeSignal?.aborted === true && cp.status === "pending") {
4340
- throw new CheckpointError("checkpoint.resume_aborted", "the resume was handed an ALREADY-ABORTED signal — refusing to consume the approval on a leg that cannot run it (the checkpoint stays pending and is resumable with a live signal)");
4341
- }
4342
- if (cp.state.workspaceHandle !== undefined && this.deps.executionEnvFactory === undefined) {
4343
- throw new CheckpointError("checkpoint.unsupported_version", "checkpoint has a remote workspaceHandle but no RunnerDeps.executionEnvFactory is wired to rebuild the env", { reason: "env_factory_missing" });
4344
- }
4345
- if (cp.state.workspaceHandle !== undefined && internals?.requestedCwd !== undefined) {
4346
- throw new CheckpointError("checkpoint.cwd_conflicts_restore", `internals.requestedCwd ("${internals.requestedCwd}") cannot be combined with a checkpoint workspace restore — ` +
4347
- "the restored workspace's own mount path is authoritative for the task root. Drop requestedCwd on resume legs " +
4348
- "(the checkpoint stays pending and is resumable without it).");
4349
- }
4350
- if (cp.state.inheritedGate?.requiresParentConstraint === true) {
4351
- const supplied = internals?.inheritedGate?.parentConstraints?.length ?? 0;
4352
- if (supplied === 0) {
4353
- throw new CheckpointError("resume.parent_constraint_missing", "this checkpoint's task ran under inherited parent-policy constraints (state.inheritedGate.requiresParentConstraint) " +
4354
- "— the live policy/onAsk closures cannot be persisted, so the resume must re-supply them via " +
4355
- "resumeStream(token, outcome, taskConfig, internals) with internals.inheritedGate.parentConstraints; " +
4356
- "rejected pre-CAS (the checkpoint stays pending) rather than silently dropping the ancestors' gate");
4357
- }
4358
- const expected = cp.state.inheritedGate.parentConstraintCount;
4359
- if (expected !== undefined && supplied !== expected) {
4360
- throw new CheckpointError("resume.parent_constraint_mismatch", `re-supplied ${supplied} parent constraint(s) but the checkpoint recorded ${expected} — a partial/mismatched ` +
4361
- "re-supply would run the resumed leg under a different ancestor chain than it suspended with; " +
4362
- "rejected pre-CAS (the checkpoint stays pending) — re-resume with the full original chain");
4363
- }
4364
- if (checkpointVersionOf(cp) >= F012_CHECKPOINT_VERSION &&
4365
- (cp.state.inheritedGate.constraintChain === undefined || cp.state.inheritedGate.constraintDigest === undefined)) {
4366
- throw new CheckpointError("checkpoint.invalid_outcome", `a v${checkpointVersionOf(cp)} checkpoint that requires parent constraints must carry BOTH the frozen constraintChain and its constraintDigest (they are minted in one write with the version stamp) — this row carries neither or only one; refusing to fall back to the count-only contract on a damaged row (corruption / downgrade guard), the checkpoint stays pending`, { reason: "constraint_chain_missing" });
4367
- }
4368
- const expectedDigest = cp.state.inheritedGate.constraintDigest;
4369
- if (expectedDigest !== undefined) {
4370
- const persistedChain = cp.state.inheritedGate.constraintChain;
4371
- if (persistedChain === undefined || constraintChainDigest(persistedChain) !== expectedDigest) {
4372
- throw new CheckpointError("resume.parent_constraint_mismatch", "the checkpoint's OWN recorded constraint chain does not match its recorded digest " +
4373
- "(corrupt / inconsistently-minted row) — the frozen deny data the resume would execute is not the data the " +
4374
- "digest authenticates; rejected pre-CAS (the checkpoint stays pending)");
4375
- }
4376
- const suppliedDigest = constraintChainDigest((internals?.inheritedGate?.parentConstraints ?? []).map((pc, i) => constraintChainEntryOfSuppliedLayer(pc, persistedChain[i])));
4377
- if (suppliedDigest !== expectedDigest) {
4378
- throw new CheckpointError("resume.parent_constraint_mismatch", "the re-supplied parent-constraint chain's projection content does not match what the checkpoint recorded " +
4379
- `(recorded ${expectedDigest}, re-supplied ${suppliedDigest}) — a same-shape chain with different frozen ` +
4380
- "deny data would run the resumed leg under a different ancestor gate than it suspended with; " +
4381
- "rejected pre-CAS (the checkpoint stays pending) — re-resume with the original chain's policies " +
4382
- "(including, when the row recorded one, the ancestor's auto-mode arming recipe verbatim)");
4383
- }
4384
- }
4385
- }
4386
- const resumeInternals = internals?.inheritedGate?.parentConstraints !== undefined
4387
- ? {
4388
- ...internals,
4389
- inheritedGate: {
4390
- ...internals.inheritedGate,
4391
- parentConstraints: Object.freeze(internals.inheritedGate.parentConstraints.map((e) => ({ ...e }))),
4392
- },
4393
- }
4394
- : internals;
4395
- if (this.deps.lockedConfig?.keys?.includes("toolPolicy") === true && config.basePolicyForResumeEdit !== undefined) {
4396
- throw new CheckpointError("checkpoint.invalid_outcome", "TaskSpec.basePolicyForResumeEdit is administratively locked by this deployment (locked key \"toolPolicy\") — a task-supplied " +
4397
- "resume-edit policy is refused pre-CAS (the checkpoint stays pending); remove the field or change the deployment's lock configuration");
4398
- }
4399
- let recheckGovernanceWindow;
4400
- {
4401
- const rowPrincipal = cp.principal || undefined;
4402
- const suppliedPrincipal = config.principal || undefined;
4403
- if (rowPrincipal !== undefined && suppliedPrincipal !== undefined && suppliedPrincipal !== rowPrincipal) {
4404
- throw new CheckpointError("resume.principal_mismatch", `the resume config carries principal ${JSON.stringify(suppliedPrincipal)} but this checkpoint was suspended under principal ${JSON.stringify(rowPrincipal)} — ` +
4405
- "running the resumed leg under a different identity would move its usage-ledger bucket, scope derivation and attribution; " +
4406
- "refused pre-CAS (the checkpoint stays pending): re-resume with the original principal, or omit the field to inherit the recorded one");
4407
- }
4408
- {
4409
- const recordedPlacementRoot = placementValueOrAbsent(cp.state.placementRootSessionId);
4410
- const suppliedPlacementRoot = placementValueOrAbsent(internals?.placementRoot);
4411
- if (recordedPlacementRoot !== undefined && suppliedPlacementRoot !== undefined && suppliedPlacementRoot !== recordedPlacementRoot) {
4412
- 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 — ` +
4413
- "resuming under a different placement fixed point would re-place the leg (and its descendants) on another target; " +
4414
- "refused pre-CAS (the checkpoint stays pending): re-resume with the recorded root, or omit internals.placementRoot to inherit it");
4415
- }
4416
- }
4417
- const owesDelivery = outcomeGate !== "resource_limit" ||
4418
- readPendingSteerQueue(cp.state).length > 0 ||
4419
- (cp.state.runningBackgroundTasks?.length ?? 0) > 0;
4420
- const usageWindowStore = this.deps.usageWindowStore;
4421
- if (owesDelivery && usageWindowStore !== undefined) {
4422
- const preCasWindows = resolveUsageWindows(this.deps.usageWindows);
4423
- if (preCasWindows !== undefined && preCasWindows.length > 0) {
4424
- const ledgerKey = (suppliedPrincipal ?? rowPrincipal) || GLOBAL_USAGE_KEY;
4425
- const assertWindowOpen = async () => {
4426
- const wait = usageRetryAfterMs(await usageWindowStore.read(ledgerKey, preCasWindows, Date.now()), preCasWindows);
4427
- if (wait !== undefined) {
4428
- throw new CheckpointError("resume.usage_window_exhausted", `a deployment usage window for ledger key ${JSON.stringify(ledgerKey)} is exhausted, and this checkpoint still owes a delivery a re-mint cannot carry — ` +
4429
- `refused pre-CAS (nothing consumed, nothing unpinned): the same token and the same decision are redeemable once the window frees, in ${String(wait)}ms`, { retryAfterMs: wait });
4430
- }
4431
- };
4432
- recheckGovernanceWindow = assertWindowOpen;
4433
- await assertWindowOpen();
4434
- }
4435
- }
4436
- }
4437
- if (outcomeGate === "wake" && wakeMessage !== undefined) {
4438
- const resumeScreenHooks = config.hooks ?? this.deps.hooks;
4439
- const screen = resumeScreenHooks?.userPromptSubmit;
4440
- if (screen !== undefined && resumeSignal?.aborted !== true) {
4441
- const screenedMessage = wakeMessage;
4442
- const identity = mintHookInvocationIdentity({
4443
- sessionId: cp.sessionId,
4444
- taskId: config.taskId ?? cp.sessionId,
4445
- legKind: "resume",
4446
- isDelegatedChild: effectiveDelegationFacts(internals, cp.state.isDelegatedChild).isDelegatedChild,
4447
- ...(internals?.insideFork === true ? { insideFork: true } : {}),
4448
- ...(internals?.agentName !== undefined ? { agentName: internals.agentName } : {}),
4449
- ...(internals?.parentToolCallId !== undefined ? { parentToolCallId: internals.parentToolCallId } : {}),
4450
- });
4451
- const screenTimeoutMs = resolveHookTimeoutMs(resumeScreenHooks?.timeoutMs, (badErr) => {
4452
- try {
4453
- this.deps.onError?.(badErr instanceof Error ? badErr : new Error(String(badErr)), { phase: "hook", sessionId: cp.sessionId });
4454
- }
4455
- catch {
4456
- }
4457
- }, resumeScreenHooks);
4458
- let decision;
4459
- try {
4460
- const seat = await runHookSeat("userPromptSubmit", { timeoutMs: screenTimeoutMs, ...(resumeSignal !== undefined ? { signal: resumeSignal } : {}), abortEnds: true }, (sig) => screen(screenedMessage.text, {
4461
- identity,
4462
- signal: sig,
4463
- source: "resume_message",
4464
- ...(screenedMessage.inputId !== undefined ? { inputId: screenedMessage.inputId } : {}),
4465
- ...(screenedMessage.actor !== undefined ? { actor: snapshotActorAssertion(screenedMessage.actor) } : {}),
4466
- }));
4467
- if (seat.expired) {
4468
- throw new CheckpointError("steering.blocked_by_hook", seat.cause === "timeout"
4469
- ? `the deployment's userPromptSubmit hook did not answer within its ${screenTimeoutMs}ms bound while screening this wake message; the resume was refused (fail-closed) and the checkpoint stays pending`
4470
- : `the resume was cancelled while the deployment's userPromptSubmit hook was still screening this wake message; the checkpoint stays pending`);
4471
- }
4472
- decision = seat.value;
4473
- }
4474
- catch (hookErr) {
4475
- if (hookErr instanceof CheckpointError)
4476
- throw hookErr;
4477
- const err = hookErr instanceof Error ? hookErr : new Error(String(hookErr));
4478
- try {
4479
- this.deps.onError?.(err, { phase: "hook", sessionId: cp.sessionId });
4480
- }
4481
- catch {
4482
- }
4483
- throw new CheckpointError("steering.blocked_by_hook", `the deployment's userPromptSubmit hook crashed while screening this wake message (${inlineUntrusted(err.message)}); the resume was refused (fail-closed) and the checkpoint stays pending`);
4484
- }
4485
- if (decision?.block !== undefined && decision.block !== "") {
4486
- throw new CheckpointError("steering.blocked_by_hook", `the deployment's userPromptSubmit hook blocked this wake message: ${inlineUntrusted(decision.block)}`);
4487
- }
4488
- if (decision?.additionalContext !== undefined && decision.additionalContext !== "") {
4489
- wakeHookContext = decision.additionalContext;
4490
- }
4491
- if (recheckGovernanceWindow !== undefined)
4492
- await recheckGovernanceWindow();
4493
- }
4494
- }
4495
- if (plainPolicyOutcome !== undefined &&
4496
- plainPolicyOutcome.decision === "allow" &&
4497
- plainPolicyOutcome.updatedInput !== undefined &&
4498
- cp.pendingAction.kind === "tool_approval" &&
4499
- cp.pendingAction.toolName !== ASK_USER_QUESTION_TOOL_NAME) {
4500
- const editedArgs = plainPolicyOutcome.updatedInput;
4501
- const editBudget = {
4502
- resourceRemainingMicroUsd: remainingBudgetMicroUsd(cp.resourceLedger),
4503
- resourceSpentMicroUsd: cp.resourceLedger?.spentMicroUsd ?? 0,
4504
- suspendCount: cp.suspendCount ?? 0,
4505
- };
4506
- const editedReq = {
4507
- toolName: cp.pendingAction.toolName,
4508
- args: editedArgs,
4509
- toolCallId: cp.pendingAction.toolCallId,
4510
- budget: editBudget,
4511
- ...(cp.state.handsCwd !== undefined ? { cwd: cp.state.handsCwd } : {}),
4512
- };
4513
- const editSignal = config.signal;
4514
- const raceEditAbort = async (p) => {
4515
- if (editSignal === undefined)
4516
- return p;
4517
- if (editSignal.aborted) {
4518
- throw new CheckpointError("checkpoint.resume_aborted", "the resume was aborted while re-adjudicating the approver's edit — the checkpoint stays pending and is resumable with a live signal");
4519
- }
4520
- return new Promise((resolve, reject) => {
4521
- const onAbort = () => reject(new CheckpointError("checkpoint.resume_aborted", "the resume was aborted while re-adjudicating the approver's edit — the checkpoint stays pending and is resumable with a live signal"));
4522
- editSignal.addEventListener("abort", onAbort, { once: true });
4523
- p.then((v) => { editSignal.removeEventListener("abort", onAbort); resolve(v); }, (e) => { editSignal.removeEventListener("abort", onAbort); reject(e); });
4524
- });
4525
- };
4526
- const ig = cp.state.inheritedGate;
4527
- if (ig?.requiresParentConstraint === true) {
4528
- const chain = ig.constraintChain;
4529
- const unprojectable = chain === undefined
4530
- ? "the checkpoint predates the frozen projection chain (no constraintChain recorded)"
4531
- : chain.some((e) => "opaque" in e)
4532
- ? "the inherited chain carries an OPAQUE layer (no projection exported)"
4533
- : chain.some((e) => !("opaque" in e) && e.requiresLiveRemainder)
4534
- ? "an inherited chain layer declares a live remainder beyond its projection"
4535
- : undefined;
4536
- if (unprojectable !== undefined) {
4537
- throw new CheckpointError("resume.constraint_unprojectable", `the approver EDITED this call's input, but the inherited ancestor chain cannot re-adjudicate the edit statically: ${unprojectable}. ` +
4538
- "Refusing the edited resume pre-CAS (the checkpoint stays pending and remains decidable) — approve the shown input as-is, deny it, " +
4539
- "or re-issue the edited action as a fresh call so the full live chain adjudicates it");
4540
- }
4541
- for (const entry of chain) {
4542
- const denied = checkToolPolicyProjection(entry, editedReq);
4543
- if (denied !== undefined) {
4544
- throw new CheckpointError("resume.constraint_rejected", `the approver EDITED this call's input and a FROZEN inherited ancestor constraint denies the edited args${denied.message ? `: ${denied.message}` : ""}. ` +
4545
- "Refused pre-CAS (fresh-redecision semantics: the checkpoint stays pending and the same token remains decidable — approve as shown, deny, or edit differently)");
4546
- }
4547
- }
4548
- }
4549
- const toolPolicyLocked = this.deps.lockedConfig?.keys?.includes("toolPolicy") === true;
4550
- const editBasePolicy = toolPolicyLocked
4551
- ? (this.deps.basePolicyForResumeEdit ?? this.deps.toolPolicy)
4552
- : (config.basePolicyForResumeEdit ?? this.deps.basePolicyForResumeEdit ?? config.toolPolicy ?? this.deps.toolPolicy);
4553
- if (editBasePolicy !== undefined) {
4554
- let rechecked;
4555
- try {
4556
- rechecked = refuseOutOfContractDecision(await raceEditAbort(Promise.resolve(editBasePolicy.check(editedReq, editSignal))));
4557
- }
4558
- catch (err) {
4559
- if (err instanceof CheckpointError)
4560
- throw err;
4561
- rechecked = { action: "deny", message: `the resume-edit policy re-check errored: ${err instanceof Error ? err.message : String(err)}` };
4562
- }
4563
- if (rechecked.action === "deny") {
4564
- throw new CheckpointError("resume.constraint_rejected", `the approver EDITED this call's input and the deployment's resume-edit policy denies the edited args${rechecked.message ? `: ${rechecked.message}` : ""}. ` +
4565
- "Refused pre-CAS (the checkpoint stays pending and remains decidable)");
4566
- }
4567
- if (rechecked.updatedInput !== undefined && !deepJsonEqual(rechecked.updatedInput, editedArgs)) {
4568
- throw new CheckpointError("resume.constraint_rejected", `the deployment's resume-edit policy REWROTE the approver's edited args to a different value than was approved — the durable resume cannot execute either the un-adjudicated edit or an unapproved rewrite; ` +
4569
- `refused pre-CAS (the checkpoint stays pending); re-submit the edited action so the rewrite is adjudicated and approved synchronously`);
4570
- }
4571
- }
4572
- const resumeHooks = config.hooks ?? this.deps.hooks;
4573
- if (resumeHooks?.preToolUse !== undefined && resumeHooks.preToolUseObservational === true) {
4574
- try {
4575
- await raceEditAbort(Promise.resolve(resumeHooks.preToolUse(cp.pendingAction.toolName, cloneObserverInput(editedArgs), {
4576
- toolCallId: cp.pendingAction.toolCallId,
4577
- toolName: cp.pendingAction.toolName,
4578
- })));
4579
- }
4580
- catch (err) {
4581
- if (err instanceof CheckpointError)
4582
- throw err;
4583
- throw new CheckpointError("resume.constraint_rejected", `the declared-observational PreToolUse face threw while observing the approver's edited args (${err instanceof Error ? err.message : String(err)}) — ` +
4584
- "refused fail-closed pre-CAS (the checkpoint stays pending and remains decidable)");
4585
- }
4586
- }
4587
- }
4588
- if (plainPolicyOutcome !== undefined) {
4589
- const parkedOrigin = cp.pendingAction.kind === "tool_approval" ? cp.pendingAction.origin : undefined;
4590
- if (!isAskOrigin(parkedOrigin)) {
4591
- throw new CheckpointError("checkpoint.unsupported_version", "this checkpoint's pending approval carries no origin word (a row minted before the ask's origin was persisted on the park) — its settlement cannot be recorded; the checkpoint stays pending");
4592
- }
4593
- const facts = plainPolicyOutcome.hostDecision;
4594
- const approverCell = facts.approver !== undefined ? { approver: facts.approver } : {};
4595
- const settlement = facts.decidedBy === "sla_timeout"
4596
- ? { kind: "park_sla_expired", who: { party: "host", ...approverCell }, when: Date.now() }
4597
- : { kind: plainPolicyOutcome.decision === "allow" ? "human_allowed" : "human_refused", who: { party: "person", ...approverCell }, when: Date.now(), ...(plainPolicyOutcome.decision === "deny" && plainPolicyOutcome.reason !== undefined ? { note: plainPolicyOutcome.reason } : {}) };
4598
- plainPolicyOutcome = { ...plainPolicyOutcome, gateOutcome: mintGateOutcome({ ...(plainPolicyOutcome.decision === "deny" ? { deniedBy: "ask_resolution" } : {}), settled: { settlement, origin: parkedOrigin } }) };
4599
- }
4600
- const outcomeForStore = plainPolicyOutcome ?? plainReviewOutcome ?? plainParkOutcome;
4601
- if (outcomeForStore === undefined) {
4602
- 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`);
4603
- }
4604
- if (this.deps.resumePreflight !== undefined) {
4605
- const preflight = this.deps.resumePreflight;
4606
- const rawDeadline = this.deps.resumePreflightTimeoutMs;
4607
- let preflightTimeoutMs = RESUME_PREFLIGHT_DEFAULT_TIMEOUT_MS;
4608
- if (rawDeadline !== undefined) {
4609
- if (typeof rawDeadline === "number" && Number.isFinite(rawDeadline) && rawDeadline > 0 && rawDeadline <= MAX_HOOK_TIMEOUT_MS) {
4610
- preflightTimeoutMs = rawDeadline;
4611
- }
4612
- else {
4613
- try {
4614
- 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 });
4615
- }
4616
- catch {
4617
- }
4618
- }
4619
- }
4620
- const info = {
4621
- token,
4622
- sessionId: cp.sessionId,
4623
- ...(cp.state.placementRootSessionId !== undefined ? { placementRootSessionId: cp.state.placementRootSessionId } : {}),
4624
- ...(cp.principal ? { principal: cp.principal } : {}),
4625
- ...(cp.state.workspaceHandle !== undefined ? { workspaceHandle: structuredClone(cp.state.workspaceHandle) } : {}),
4626
- gateKind: cp.gate.kind,
4627
- };
4628
- const refusalTail = "; the checkpoint stays pending and the same token is redeemable once the obstacle clears";
4629
- let verdict;
4630
- try {
4631
- const seat = await runHookSeat("resumePreflight", {
4632
- timeoutMs: preflightTimeoutMs,
4633
- ...(resumeSignal !== undefined ? { signal: resumeSignal } : {}),
4634
- abortEnds: true,
4635
- onBadTimeout: (badErr) => {
4636
- try {
4637
- this.deps.onError?.(badErr instanceof Error ? badErr : new Error(String(badErr)), { phase: "hook", sessionId: cp.sessionId });
4638
- }
4639
- catch {
4640
- }
4641
- },
4642
- owner: this.deps,
4643
- }, (sig) => preflight(info, sig));
4644
- if (seat.expired) {
4645
- throw new CheckpointError("resume.preflight_rejected", seat.cause === "timeout"
4646
- ? `the deployment's resumePreflight did not answer within its ${preflightTimeoutMs}ms deadline while screening this resume — refused fail-closed (retry-later arm)${refusalTail}`
4647
- : `the resume was cancelled while the deployment's resumePreflight was still screening it — refused (retry-later arm)${refusalTail}`);
4648
- }
4649
- verdict = seat.value;
4650
- }
4651
- catch (preflightErr) {
4652
- if (preflightErr instanceof CheckpointError)
4653
- throw preflightErr;
4654
- const err = preflightErr instanceof Error ? preflightErr : new Error(String(preflightErr));
4655
- try {
4656
- this.deps.onError?.(err, { phase: "hook", sessionId: cp.sessionId });
4657
- }
4658
- catch {
4659
- }
4660
- 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}`);
4661
- }
4662
- const verdictOk = verdict !== null && typeof verdict === "object" ? verdict.ok : undefined;
4663
- if (verdictOk !== true) {
4664
- const bag = verdict !== null && typeof verdict === "object" ? verdict : undefined;
4665
- const disposition = bag?.disposition;
4666
- const suppliedMessage = bag?.message;
4667
- const retryAfterMs = bag?.retryAfterMs;
4668
- const readableMessage = typeof suppliedMessage === "string" && suppliedMessage !== "" ? suppliedMessage : undefined;
4669
- const said = readableMessage !== undefined ? `: ${inlineUntrusted(readableMessage)}` : " (no readable verdict — fail-closed)";
4670
- if (disposition === "terminal" && readableMessage !== undefined) {
4671
- this.locallyClaimedTokens.add(token);
4672
- let settled;
4673
- try {
4674
- settled = await store.expire(token, cp.scope);
4675
- }
4676
- catch (expireErr) {
4677
- this.locallyClaimedTokens.delete(token);
4678
- throw expireErr;
4679
- }
4680
- if (!settled) {
4681
- this.locallyClaimedTokens.delete(token);
4682
- const live = await store.get(token);
4683
- if (live?.status === "pending") {
4684
- 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)`);
4685
- }
4686
- 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`);
4687
- }
4688
- this.parentConstraintRegistry.delete(token);
4689
- const terminalReap = this.suspendedEnvReaps.get(token);
4690
- this.suspendedEnvReaps.delete(token);
4691
- this.locallyClaimedTokens.delete(token);
4692
- if (terminalReap?.env !== undefined && hasDestroy(terminalReap.env)) {
4693
- const terminalEnv = terminalReap.env;
4694
- await settleTeardownLeg(() => terminalEnv.destroy(), "terminal-preflight env destroy", (envErr) => {
4695
- try {
4696
- this.deps.onError?.(envErr, { phase: "config", sessionId: terminalReap.sessionId });
4697
- }
4698
- catch {
4699
- }
4700
- });
4701
- }
4702
- try {
4703
- 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 });
4704
- }
4705
- catch {
4706
- }
4707
- 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`);
4708
- }
4709
- const waitHint = typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs >= 0 ? retryAfterMs : undefined;
4710
- 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);
4711
- }
4712
- if (recheckGovernanceWindow !== undefined)
4713
- await recheckGovernanceWindow();
4714
- }
4715
- this.locallyClaimedTokens.add(token);
4716
- let won;
4717
- let claimLossStatus;
4718
- if (store.claimTerminal !== undefined) {
4719
- const claim = await store.claimTerminal(token, cp.scope, { kind: "resolve", outcome: outcomeForStore, expect: { rev: cp.rev ?? 0 } });
4720
- won = claim.claimed;
4721
- if (!claim.claimed)
4722
- claimLossStatus = claim.current.status;
4723
- }
4724
- else {
4725
- won = await store.resolve(token, cp.scope, outcomeForStore, { rev: cp.rev ?? 0 });
4726
- }
4727
- if (!won)
4728
- this.locallyClaimedTokens.delete(token);
4729
- if (!won) {
4730
- const stillPending = claimLossStatus !== undefined ? claimLossStatus === "pending" : (await store.get(token))?.status === "pending";
4731
- if (stillPending) {
4732
- throw new CheckpointError("checkpoint.reopened_concurrently", "checkpoint changed concurrently (its revision advanced via a resolve/reopen cycle since this resume validated) — not executed; re-resume against the current state");
4733
- }
4734
- this.parentConstraintRegistry.delete(token);
4735
- throw new CheckpointError("checkpoint.already_resolved", "checkpoint already resolved or expired — not re-executed (idempotent)");
4736
- }
3025
+ return await resumeAdmission({
3026
+ token,
3027
+ outcome,
3028
+ taskConfig,
3029
+ internals,
3030
+ runner: this.depsSeat,
3031
+ parentConstraintRegistry: this.parentConstraintRegistry,
3032
+ next: (admitted) => this.resumeLadder(token, outcome, admitted),
3033
+ });
3034
+ }
3035
+ async resumeLadder(token, outcome, admitted) {
3036
+ const { config, internals, store, cp, outcomeGate, resumeSignal, wakeMessage } = admitted;
3037
+ const { plainReviewOutcome, plainParkOutcome } = resumeReviewOutcome({ cp, outcome, outcomeGate, wakeMessage });
3038
+ const { plainPolicyOutcome: capturedPolicyOutcome, redeemedAnswer } = resumePolicyOutcome({ cp, outcome, config, runner: this.depsSeat });
3039
+ const { resumeInternals } = resumeCheckpointScreen({ cp, resumeSignal, internals, runner: this.depsSeat });
3040
+ const { recheckGovernanceWindow, windowEntrance } = resumeInternalsAndConfig({ cp, config, internals, outcomeGate, runner: this.depsSeat });
3041
+ if (windowEntrance !== undefined)
3042
+ await windowEntrance;
3043
+ return await resumeApply({
3044
+ cp, config, internals, outcomeGate, wakeMessage, resumeSignal, recheckGovernanceWindow,
3045
+ plainPolicyOutcome: capturedPolicyOutcome, plainReviewOutcome, plainParkOutcome,
3046
+ runner: this.depsSeat, effectiveDelegationFacts,
3047
+ next: ({ wakeHookContext, plainPolicyOutcome, outcomeForStore }) => resumePreflight({
3048
+ token, cp, resumeSignal, recheckGovernanceWindow, store, runner: this.depsSeat,
3049
+ locallyClaimedTokens: this.locallyClaimedTokens, parentConstraintRegistry: this.parentConstraintRegistry, suspendedEnvReaps: this.suspendedEnvReaps,
3050
+ next: () => resumeClaim({
3051
+ token, cp, store, outcomeForStore,
3052
+ locallyClaimedTokens: this.locallyClaimedTokens, parentConstraintRegistry: this.parentConstraintRegistry,
3053
+ next: () => this.resumeHandoff({ token, cp, store, config, internals, resumeInternals, plainPolicyOutcome, redeemedAnswer, outcomeForStore, wakeMessage, wakeHookContext, resumeSignal }),
3054
+ }),
3055
+ }),
3056
+ });
3057
+ }
3058
+ async resumeHandoff(claimed) {
3059
+ const { token, cp, store, config, internals, resumeInternals, plainPolicyOutcome, redeemedAnswer, outcomeForStore, wakeMessage, wakeHookContext, resumeSignal } = claimed;
4737
3060
  let consumeFlipDone = false;
4738
3061
  if (internals?.afterCheckpointResolve !== undefined) {
4739
3062
  await internals.afterCheckpointResolve();
@@ -4798,6 +3121,12 @@ export class Runner {
4798
3121
  e.code = "resume.tool_unavailable";
4799
3122
  throw e;
4800
3123
  }
3124
+ const liveRoster = prepared.toolRosterDeltas.current;
3125
+ if (pendingAction.kind === "tool_approval" && liveRoster !== undefined && judgeParkedToolIdentity(pendingAction, rosterEntryNamed(liveRoster, pendingAction.toolName)) === "mismatch") {
3126
+ const e = new Error(`the approved tool "${pendingAction.toolName}" on this leg is not the tool the approval was minted against (its contract / shape / revision differ) — refusing to execute a replacement wearing the name; the checkpoint is reopened for a retry with the original tool present`);
3127
+ e.code = "resume.tool_contract_mismatch";
3128
+ throw e;
3129
+ }
4801
3130
  const completed = new Set(pendingAction.completedCallIds);
4802
3131
  const deferredIds = pendingAction.batchToolCallIds.filter((id) => id !== pendingAction.toolCallId && !completed.has(id));
4803
3132
  const names = new Map();
@@ -4826,7 +3155,7 @@ export class Runner {
4826
3155
  return l !== undefined && l !== name ? { label: l } : {};
4827
3156
  })();
4828
3157
  emit({ type: "tool_start", toolCallId: id, toolName: name, ...displayLabel, args: deferredArgs.get(id) ?? {} });
4829
- emit({ type: "tool_end", toolCallId: id, toolName: name, ...displayLabel, isError: true, ...toolEndBodyFrom({ content: formatHookFeedback(DEFERRED_REISSUE, prepared.reminderMark) }, true) });
3158
+ emit({ type: "tool_end", toolCallId: id, toolName: name, ...displayLabel, isError: true, ...toolEndBodyFrom({ content: formatHookFeedback(DEFERRED_REISSUE, prepared.reminderMark) }, true, prepared.structuredProjector) });
4830
3159
  const eid = await prepared.session.appendMessage(toolResultMsg(id, name, formatHookFeedback(DEFERRED_REISSUE, prepared.reminderMark), true));
4831
3160
  emitCommitted(eid, "toolResult", id);
4832
3161
  }
@@ -4845,7 +3174,7 @@ export class Runner {
4845
3174
  const pendingLabel = (() => { const l = prepared.tools.find((t) => t.name === pendingAction.toolName)?.label; return l !== undefined && l !== pendingAction.toolName ? { label: l } : {}; })();
4846
3175
  emit({ type: "tool_start", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, args: resolvedArgs });
4847
3176
  const settleEnd = async (isError, result, gate = decidedGate) => {
4848
- emit({ type: "tool_end", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, isError, ...toolEndBodyFrom(result, isError, gate) });
3177
+ emit({ type: "tool_end", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, isError, ...toolEndBodyFrom(result, isError, prepared.structuredProjector, gate) });
4849
3178
  const word = await resume.recordExecutionOutcome(gate);
4850
3179
  settleExecutionRecord(this.deps.onNotice, word, { toolName: pendingAction.toolName, gate, sessionId: prepared.sessionId, runId: prepared.runId, scope: resume.cp.scope, ...(resume.cp.checkpointId !== undefined ? { checkpointId: resume.cp.checkpointId } : {}) });
4851
3180
  };
@@ -4878,6 +3207,7 @@ export class Runner {
4878
3207
  toolCallId: pendingAction.toolCallId,
4879
3208
  budget: beltBudget,
4880
3209
  ...(prepared.cwdRef !== undefined ? { cwd: prepared.cwdRef.current } : {}),
3210
+ ...callFaceSeat(toolCallFaceOf(prepared.toolRosterDeltas.current, pendingAction.toolName)),
4881
3211
  }, prepared.abortController.signal));
4882
3212
  const beltUnsafe = rechecked.action === "deny" ||
4883
3213
  (rechecked.updatedInput !== undefined && !deepJsonEqual(rechecked.updatedInput, resolvedArgs));
@@ -4935,6 +3265,7 @@ export class Runner {
4935
3265
  toolName: pendingAction.toolName,
4936
3266
  args: resolvedArgs,
4937
3267
  toolCallId: pendingAction.toolCallId,
3268
+ ...callFaceSeat(toolCallFaceOf(prepared.toolRosterDeltas.current, pendingAction.toolName)),
4938
3269
  budget: beltBudget,
4939
3270
  ...(prepared.cwdRef !== undefined ? { cwd: prepared.cwdRef.current } : {}),
4940
3271
  }, prepared.abortController.signal));
@@ -4946,6 +3277,30 @@ export class Runner {
4946
3277
  return;
4947
3278
  }
4948
3279
  }
3280
+ if (prepared.persistedRuleLane !== undefined) {
3281
+ const unreadable = { unreadable: true };
3282
+ const read = await settleOrgVerdictWithin(Promise.resolve()
3283
+ .then(() => prepared.persistedRuleLane.adjudicate({
3284
+ toolName: pendingAction.toolName,
3285
+ args: resolvedArgs,
3286
+ toolCallId: pendingAction.toolCallId,
3287
+ ...callFaceSeat(toolCallFaceOf(prepared.toolRosterDeltas.current, pendingAction.toolName)),
3288
+ budget: beltBudget,
3289
+ ...(prepared.cwdRef !== undefined ? { cwd: prepared.cwdRef.current } : {}),
3290
+ }))
3291
+ .then((answer) => normalizePersistedRuleHit(answer)), unreadable, { signal: prepared.abortController.signal, timeoutMs: ORG_ADJUDICATION_TIMEOUT_MS });
3292
+ const tightened = applyPersistedTightening({ action: "allow" }, read);
3293
+ if (tightened.tightened === "deny" || tightened.tightened === "unreadable") {
3294
+ const why = tightened.tightened === "deny"
3295
+ ? `a standing deny rule of yours (${disclosedRuleSet(read.hit?.rules ?? [])}) now refuses it`
3296
+ : "your persisted permission rules could not be read on this leg";
3297
+ const ruleDenial = formatHookFeedback(`The approved tool call "${pendingAction.toolName}" was not executed: ${why}. This approval is spent — the call has to be re-issued and approved again.`, prepared.reminderMark);
3298
+ await settleEnd(true, { content: ruleDenial }, vetoed("persisted_rule"));
3299
+ const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, ruleDenial, true));
3300
+ emitCommitted(eid, "toolResult", pendingAction.toolCallId);
3301
+ return;
3302
+ }
3303
+ }
4949
3304
  const tool = prepared.tools.find((t) => t.name === pendingAction.toolName);
4950
3305
  if (!tool) {
4951
3306
  const e = new Error(`the approved tool "${pendingAction.toolName}" is no longer available on resume`);