pi-subagents 0.65.1 → 0.67.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 (148) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/README.md +5 -4
  3. package/agents/evidence-auditor.md +34 -0
  4. package/agents/researcher.md +23 -13
  5. package/agents/reviewer.md +3 -2
  6. package/docs/agents.md +20 -3
  7. package/docs/configuration.md +25 -5
  8. package/docs/extension-api.md +124 -18
  9. package/docs/missions.md +8 -0
  10. package/docs/models.md +59 -2
  11. package/docs/observability.md +46 -6
  12. package/docs/standalone-background.md +49 -0
  13. package/docs/tool-reference.md +20 -10
  14. package/docs/watchdog.md +35 -4
  15. package/docs/workflows.md +40 -19
  16. package/inspector-runner.mjs +2 -2
  17. package/package.json +2 -1
  18. package/prompts/parallel-review.md +1 -1
  19. package/{runner-server-preload.mjs → runner-peer-preload.mjs} +8 -3
  20. package/skills/pi-subagents/SKILL.md +14 -0
  21. package/skills/pi-subagents/references/execution-controls.md +20 -5
  22. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
  23. package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
  24. package/src/agents/advertised-agent-prompt.ts +94 -0
  25. package/src/agents/agent-management.ts +14 -1
  26. package/src/agents/agent-serializer.ts +2 -0
  27. package/src/agents/agents.ts +14 -0
  28. package/src/agents/builtin-names.ts +1 -0
  29. package/src/api/delegation.ts +4 -0
  30. package/src/api/preflight.ts +76 -45
  31. package/src/api/shared-types.ts +3 -1
  32. package/src/api/workflow-resources.ts +6 -0
  33. package/src/extension/fanout-child.ts +63 -4
  34. package/src/extension/index.ts +58 -8
  35. package/src/extension/public-execution.ts +4 -3
  36. package/src/extension/rpc.ts +8 -21
  37. package/src/extension/schemas.ts +71 -80
  38. package/src/extension/tool-description.ts +29 -81
  39. package/src/inspectors/actions.ts +148 -0
  40. package/src/inspectors/ghostty/actions.ts +74 -0
  41. package/src/inspectors/ghostty/plugin.ts +17 -0
  42. package/src/inspectors/herdr/actions.ts +99 -179
  43. package/src/inspectors/herdr/plugin.ts +20 -0
  44. package/src/inspectors/herdr/project-panes.ts +1 -1
  45. package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
  46. package/src/inspectors/plugins.ts +8 -0
  47. package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
  48. package/src/inspectors/types.ts +51 -0
  49. package/src/intercom/intercom-bridge.ts +50 -8
  50. package/src/intercom/native-supervisor-channel.ts +104 -67
  51. package/src/runs/background/active-async-capacity.ts +22 -18
  52. package/src/runs/background/async-execution.ts +45 -56
  53. package/src/runs/background/async-job-tracker.ts +35 -3
  54. package/src/runs/background/async-resume.ts +5 -9
  55. package/src/runs/background/async-status-snapshot.ts +10 -12
  56. package/src/runs/background/async-status.ts +17 -9
  57. package/src/runs/background/auto-drain.ts +44 -30
  58. package/src/runs/background/binary-bootstrap.ts +33 -0
  59. package/src/runs/background/chain-root-attachment.ts +8 -0
  60. package/src/runs/background/control-channel.ts +78 -44
  61. package/src/runs/background/fleet-view.ts +30 -2
  62. package/src/runs/background/notify.ts +117 -13
  63. package/src/runs/background/owned-process-tree.ts +35 -8
  64. package/src/runs/background/process-terminal.ts +23 -23
  65. package/src/runs/background/run-child-session.ts +121 -36
  66. package/src/runs/background/run-status.ts +78 -5
  67. package/src/runs/background/runner-aliases.ts +28 -9
  68. package/src/runs/background/runner-child-launch.ts +88 -0
  69. package/src/runs/background/runner-child-sessions.ts +5 -4
  70. package/src/runs/background/scheduled-runs.ts +40 -13
  71. package/src/runs/background/stale-run-reconciler.ts +3 -1
  72. package/src/runs/background/steering.ts +20 -2
  73. package/src/runs/background/subagent-runner.ts +458 -239
  74. package/src/runs/background/subagent-wait.ts +54 -8
  75. package/src/runs/background/wait-completions.ts +4 -0
  76. package/src/runs/background/wait-tool.ts +1 -1
  77. package/src/runs/foreground/async-steering-action.ts +37 -7
  78. package/src/runs/foreground/execution.ts +145 -56
  79. package/src/runs/foreground/prompt-audit.ts +3 -1
  80. package/src/runs/foreground/subagent-executor.ts +584 -297
  81. package/src/runs/foreground/workflow-detach-reconcile.ts +10 -5
  82. package/src/runs/foreground/workflow-foreground-steering.ts +57 -2
  83. package/src/runs/shared/acceptance.ts +7 -4
  84. package/src/runs/shared/agent-contract.ts +1 -1
  85. package/src/runs/shared/async-status-projection.ts +51 -47
  86. package/src/runs/shared/capability-ceiling.ts +2 -0
  87. package/src/runs/shared/child-hooks.ts +167 -3
  88. package/src/runs/shared/child-launch.ts +28 -13
  89. package/src/runs/shared/child-lifecycle.ts +6 -3
  90. package/src/runs/shared/child-runtime-config.ts +3 -1
  91. package/src/runs/shared/child-session.ts +75 -8
  92. package/src/runs/shared/child-tool-plan.ts +124 -5
  93. package/src/runs/shared/completion-evidence.ts +2 -2
  94. package/src/runs/shared/completion-guard.ts +6 -3
  95. package/src/runs/shared/effective-system-prompt.ts +33 -0
  96. package/src/runs/shared/external-cli-runner.ts +9 -7
  97. package/src/runs/shared/host-step-status.ts +11 -11
  98. package/src/runs/shared/llm-intent-arbiter.ts +21 -11
  99. package/src/runs/shared/model-fallback.ts +12 -6
  100. package/src/runs/shared/nested-events.ts +5 -5
  101. package/src/runs/shared/orca-progress-tabs.ts +7 -1
  102. package/src/runs/shared/parallel-handoff.ts +57 -12
  103. package/src/runs/shared/parallel-utils.ts +2 -2
  104. package/src/runs/shared/pi-spawn.ts +10 -0
  105. package/src/runs/shared/readonly-drain-observation.ts +42 -0
  106. package/src/runs/shared/readonly-model-continuation.ts +69 -0
  107. package/src/runs/shared/readonly-session-evidence.ts +307 -0
  108. package/src/runs/shared/run-fanout-budget.ts +8 -8
  109. package/src/runs/shared/runtime-acknowledged-extensions.ts +3 -3
  110. package/src/runs/shared/subagent-prompt-runtime.ts +20 -4
  111. package/src/runs/shared/task-intent.ts +46 -13
  112. package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
  113. package/src/runs/shared/worktree-setup-command.ts +190 -0
  114. package/src/runs/shared/worktree.ts +366 -208
  115. package/src/shared/fork-context.ts +15 -72
  116. package/src/shared/launch-contract.ts +65 -2
  117. package/src/shared/opencode-session-headers.ts +30 -0
  118. package/src/shared/types.ts +85 -61
  119. package/src/shared/utils.ts +7 -2
  120. package/src/shared/workflow-child-permit.ts +18 -13
  121. package/src/slash/delegation-adapters.ts +3 -1
  122. package/src/slash/delegation-request.ts +14 -0
  123. package/src/slash/slash-commands.ts +2 -1
  124. package/src/slash/subagents-admin.ts +11 -4
  125. package/src/tui/fleet-status.ts +164 -19
  126. package/src/tui/fleet.ts +27 -19
  127. package/src/tui/render.ts +172 -33
  128. package/src/watchdog/child-status.ts +8 -0
  129. package/src/watchdog/model-selection.ts +20 -0
  130. package/src/watchdog/permission-arbiter.ts +3 -1
  131. package/src/watchdog/register-child.ts +1 -0
  132. package/src/watchdog/register-main.ts +31 -27
  133. package/src/watchdog/review.ts +132 -67
  134. package/src/watchdog/runtime.ts +82 -20
  135. package/src/watchdog/scope.ts +1 -1
  136. package/src/watchdog/settings.ts +9 -3
  137. package/src/watchdog/tool-actions.ts +13 -12
  138. package/src/watchdog/turn-delta.ts +23 -0
  139. package/src/watchdog/types.ts +4 -0
  140. package/src/workflows/chat-progress.ts +3 -3
  141. package/src/workflows/scripted-workflow.ts +275 -17
  142. package/src/workflows/workflow-checklist.ts +13 -17
  143. package/src/workflows/workflow-child-summary.ts +57 -8
  144. package/src/workflows/workflow-preflight.ts +19 -19
  145. package/src/workflows/workflow-receipt.ts +3 -3
  146. package/src/workflows/workflow-resources.ts +96 -21
  147. package/src/workflows/workflow-settlement.ts +3 -0
  148. /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
@@ -5,6 +5,7 @@ import { createRequire } from "node:module";
5
5
  import * as path from "node:path";
6
6
  import { pathToFileURL } from "node:url";
7
7
  import type { Message } from "@earendil-works/pi-ai";
8
+ import { arbitrateCompletionGuardRescue, createTaskMutationArbiter } from "../shared/llm-intent-arbiter.ts";
8
9
 
9
10
  // Detached runners skip Pi's CLI proxy setup. Keep fetch on the same Undici dispatcher.
10
11
  function ensureProxyAwareHttpDispatcher(): void {
@@ -20,7 +21,8 @@ function ensureProxyAwareHttpDispatcher(): void {
20
21
  console.error(`[pi-subagents] proxy-aware HTTP dispatcher not installed: ${error instanceof Error ? error.message : String(error)}`);
21
22
  }
22
23
  }
23
- ensureProxyAwareHttpDispatcher();
24
+ const isRunnerEntrypoint = Boolean(process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href);
25
+ if (isRunnerEntrypoint) ensureProxyAwareHttpDispatcher();
24
26
  import { writeAtomicJson } from "../../shared/atomic-json.ts";
25
27
  import { writeAsyncResultFile, writePendingAsyncResultFile } from "./result-files.ts";
26
28
  import { createFileCoalescer } from "../../shared/file-coalescer.ts";
@@ -45,10 +47,10 @@ import {
45
47
  type AsyncStatus,
46
48
  type ChainOutputMap,
47
49
  type CostSummary,
48
- type LaunchResolvedChildExtensionsV1,
49
- type RuntimeAcknowledgedChildExtensionsV1,
50
+ type LaunchResolvedChildExtensions,
51
+ type RuntimeAcknowledgedChildExtensions,
50
52
  type ModelAttempt,
51
- type PiWriterProcessInstanceExitV1,
53
+ type PiWriterProcessInstanceExit,
52
54
  type NestedRouteInfo,
53
55
  type NestedRunSummary,
54
56
  type ResolvedControlConfig,
@@ -90,10 +92,14 @@ import {
90
92
  DEFAULT_GLOBAL_CONCURRENCY_LIMIT,
91
93
  Semaphore,
92
94
  } from "../shared/parallel-utils.ts";
93
- import { applyThinkingSuffix, deriveForkPromptCacheKey, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/child-tool-plan.ts";
94
- import { buildInProcessChildLaunch, type InheritedChildRuntime } from "../shared/child-launch.ts";
95
- import type { ChildSessionFactory } from "../shared/child-session.ts";
96
- import { runChildSession, type ChildEvent, type RunChildSessionResult, type StepSteerHandler } from "./run-child-session.ts";
95
+ import { applyThinkingSuffix, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/child-tool-plan.ts";
96
+ import type { InheritedChildRuntime } from "../shared/child-launch.ts";
97
+ import { buildRunnerChildLaunch } from "./runner-child-launch.ts";
98
+ import { normalizeExtensionBindings } from "../shared/extension-bindings.ts";
99
+ import type { ChildSessionFactory, DefaultChildSessionFactoryOptions } from "../shared/child-session.ts";
100
+ import { getSettledReadonlyChild, runChildSession, type ChildEvent, type RunChildSessionInput, type RunChildSessionResult, type SteerDelivery, type StepSteerHandler } from "./run-child-session.ts";
101
+ import { planReadonlyModelContinuation, READONLY_CONTINUATION_PROMPT, type LogicalRecoveryState } from "../shared/readonly-model-continuation.ts";
102
+ import { getReadonlySessionEvidence } from "../shared/readonly-session-evidence.ts";
97
103
  import { loadRunnerChildSessionFactory } from "./runner-child-sessions.ts";
98
104
  import { SUBAGENT_CHILD_ENV } from "../shared/child-runtime-config.ts";
99
105
  import { deriveChildSessionName } from "../../shared/child-session-name.ts";
@@ -107,8 +113,8 @@ import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelSt
107
113
  import { claimRunFanoutBatch, getRunFanoutBudgetSnapshot } from "../shared/run-fanout-budget.ts";
108
114
  import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDir, writeNestedEvent } from "../shared/nested-events.ts";
109
115
  import { formatModelAttemptNote, formatSubagentModelVerificationError, isContextOverflow, isRetryableModelFailureAttempt, recordRetryableModelFailure } from "../shared/model-fallback.ts";
110
- import { markProcessTerminalCandidateLeaseRelease, writeProcessTerminalCandidate, type ProcessTerminalCandidate } from "./process-terminal.ts";
111
- import { createSteeringStatus, recordSteeringRequest, steeringStatus, terminalSteeringNoticeState, updateSteeringTarget } from "./steering.ts";
116
+ import { markProcessTerminalCandidateLeaseRelease, processTerminalPath, writeProcessTerminalCandidate, type ProcessTerminalCandidate } from "./process-terminal.ts";
117
+ import { createSteeringStatus, recordSteeringRequest, steeringStatus, terminalSteeringNoticeState, unconsumedSteerReason, updateSteeringTarget } from "./steering.ts";
112
118
  import { PROMPT_REDACTED, detectSubagentError, extractTextFromContent, extractToolArgsPreview, formatEmptyTerminalAssistantResponseError, getFinalOutput, hasEmptyTerminalAssistantResponse, readStatus } from "../../shared/utils.ts";
113
119
  import { evaluateCompletionMutationGuard, expectsImplementationMutation, hasMutationToolCapability, validateImplementationToolContract } from "../shared/completion-guard.ts";
114
120
  import { planCompletionEvidence, projectSettlementDiagnostic } from "../shared/completion-evidence.ts";
@@ -129,6 +135,9 @@ import type { TokenUsage } from "../../shared/types.ts";
129
135
  import {
130
136
  cleanupWorktrees,
131
137
  createWorktrees,
138
+ withWorktreeTransaction,
139
+ WorktreeSetupError,
140
+ type WorktreeSetupProgress,
132
141
  diffWorktrees,
133
142
  findWorktreeTaskCwdConflict,
134
143
  formatWorktreeDiffSummary,
@@ -136,21 +145,20 @@ import {
136
145
  WORKTREE_AGENT_CWD_PLACEHOLDER,
137
146
  type WorktreeSetup,
138
147
  } from "../shared/worktree.ts";
139
- import { findModelInfo, resolveEffectiveThinking } from "../../shared/model-info.ts";
148
+ import { findModelInfo, resolveEffectiveThinking, splitKnownThinkingSuffix } from "../../shared/model-info.ts";
140
149
  import { assertThinkingWithinCeiling } from "../../shared/thinking-ceiling.ts";
141
- import { launchBindingDigest } from "../../shared/launch-contract.ts";
150
+ import { resolveLaunchBinding } from "../../shared/launch-contract.ts";
142
151
  import { writeInitialProgressFile } from "../../shared/settings.ts";
143
152
  import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
144
153
  import { acceptanceFailureMessage, aggregateAcceptanceReport, buildSkippedAcceptanceLedger, evaluateAcceptance, formatAcceptancePrompt, resolveAcceptanceReportMode, resolveEffectiveAcceptance, stripAcceptanceReport } from "../shared/acceptance.ts";
145
- import { attachContractProjections, isAgentContractV1 } from "../shared/agent-contract.ts";
154
+ import { attachContractProjections, isAgentContract } from "../shared/agent-contract.ts";
146
155
  import { waitForImportedAsyncRoot } from "./chain-root-attachment.ts";
147
- import { normalizeExtensionBindings } from "../shared/extension-bindings.ts";
148
156
  import { appendRunnerStepsToStatus, consumeChainAppendRequests, countPendingChainAppendRequests, statusStepDescription } from "./chain-append.ts";
149
157
  import { asyncStatusChildIdentity } from "../shared/child-identity.ts";
150
158
  import { initialToolBudgetState, toolBudgetState } from "../shared/tool-budget.ts";
151
159
  import { effectiveToolTimeoutMs, formatToolTimeoutMessage, toolTimeoutCallKey } from "../shared/tool-timeout.ts";
152
160
  import { usageBudgetExceededMessage, usageBudgetState } from "../shared/usage-budget.ts";
153
- import { formatParallelHandoffError, formatParallelHandoffReference, parallelHandoffPath, writeParallelHandoffGroup, writePendingParallelHandoff } from "../shared/parallel-handoff.ts";
161
+ import { formatParallelHandoffError, formatParallelHandoffReference, parallelHandoffPath, writeParallelHandoffGroup, writeWorktreeSetupHandoff } from "../shared/parallel-handoff.ts";
154
162
  import { resolveWatchdogConfig } from "../../watchdog/settings.ts";
155
163
  import { acquireSessionLease, type SessionLeaseRequest } from "../shared/session-lease.ts";
156
164
  import { buildExternalCliPrompt, runExternalCli } from "../shared/external-cli-runner.ts";
@@ -176,7 +184,7 @@ const INTERCOM_DETACH_RECEIPT = "Detached for intercom coordination before task
176
184
  // child host.
177
185
  process.env[SUBAGENT_CHILD_ENV] = "1";
178
186
 
179
- interface SubagentRunConfig {
187
+ export interface SubagentRunConfig {
180
188
  id: string;
181
189
  steps: RunnerStep[];
182
190
  resultPath: string;
@@ -224,9 +232,11 @@ interface SubagentRunConfig {
224
232
  globalConcurrencyLimit?: number;
225
233
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
226
234
  runFanoutBudget?: RunFanoutBudgetDescriptor;
235
+ /** Builtin tool names the host runtime provides; used to intersect agent-declared tools. */
236
+ hostAvailableBuiltins?: readonly string[];
227
237
  launchContractDigest?: string;
228
- launchResolvedExtensions?: LaunchResolvedChildExtensionsV1;
229
- runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
238
+ launchResolvedExtensions?: LaunchResolvedChildExtensions;
239
+ runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions;
230
240
  runnerProcessInstanceId?: string;
231
241
  launchBarrierToken?: string;
232
242
  parentWorkflowRunId?: string;
@@ -241,8 +251,8 @@ interface StepResult {
241
251
  context?: "fresh" | "fork";
242
252
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
243
253
  capabilityAudit?: import("../shared/capability-ceiling.ts").SubagentCapabilityAudit;
244
- launchResolvedExtensions?: LaunchResolvedChildExtensionsV1;
245
- runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
254
+ launchResolvedExtensions?: LaunchResolvedChildExtensions;
255
+ runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions;
246
256
  output: string;
247
257
  outputState?: SubagentOutputState;
248
258
  error?: string;
@@ -674,6 +684,8 @@ interface SingleStepContext {
674
684
  registerStop?: (stop: (() => void) | undefined) => void;
675
685
  /** Receives the live child's steer handler while its session runs. */
676
686
  registerSteer?: (steer: StepSteerHandler | undefined) => void;
687
+ /** Reports a live child's later steer consumption or unconsumed settlement. */
688
+ onSteerOutcome?: (request: SteerRequest, delivery: SteerDelivery) => void;
677
689
  timeoutSignal?: AbortSignal;
678
690
  stopSignal?: AbortSignal;
679
691
  timeoutMessage?: string;
@@ -687,19 +699,23 @@ interface SingleStepContext {
687
699
  nestedRoute?: NestedRouteInfo;
688
700
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
689
701
  runFanoutBudget?: RunFanoutBudgetDescriptor;
702
+ hostAvailableBuiltins?: readonly string[];
690
703
  onAttemptStart?: (attempt: { model?: string; thinking?: string; contextLimit?: number }) => void;
691
704
  onChildEvent?: (event: ChildEvent) => void;
692
705
  onExternalProcess?: (process: ExternalProcessStatus) => void;
693
706
  onExternalJob?: (status: ExternalJobStatus) => void;
694
707
  skipAcceptance?: () => boolean;
695
- usageBudgetExhausted?: () => boolean;
708
+ /** Authoritative owner decision after event delivery; undefined includes incomplete run-wide usage. */
709
+ usageBudgetExhausted?: () => boolean | undefined;
710
+ /** Existing run-owned budget configuration; cost allowance is not settled by the live token ledger. */
711
+ usageBudget?: UsageBudgetConfig;
696
712
  /** False when sibling work in the same Git worktree could have caused the tracked diff. */
697
713
  trackedMutationEvidenceForCompletionGuard?: boolean;
698
714
  orcaProgressTab?: OrcaProgressTab;
699
715
  }
700
716
 
701
717
  /** Run a single pi agent step, returning output and metadata */
702
- async function runSingleStepInner(
718
+ export async function runSingleStepInner(
703
719
  step: SubagentStep,
704
720
  ctx: SingleStepContext,
705
721
  ): Promise<StepResult & { completionGuardTriggered?: boolean }> {
@@ -800,6 +816,7 @@ async function runSingleStepInner(
800
816
  capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
801
817
  inheritedCapabilityCeiling: ctx.inheritedChildRuntime?.capabilityCeiling,
802
818
  permissionRules: step.permissionRules,
819
+ hostAvailableBuiltins: ctx.hostAvailableBuiltins,
803
820
  }));
804
821
  const contractTools = resolvedTaskToolPlan.explicitToolAllowlist ? resolvedTaskToolPlan.effectiveToolAllowlist : undefined;
805
822
  const contractError = validateImplementationToolContract({
@@ -827,8 +844,8 @@ async function runSingleStepInner(
827
844
  // Derive from the pre-acceptance task so internal acceptance/recovery
828
845
  // instructions never leak into the display name.
829
846
  const childSessionName = step.sessionName ?? deriveChildSessionName({ agent: step.agent, task, label: step.label });
830
- if (step.effectiveAcceptance) {
831
- const acceptancePrompt = formatAcceptancePrompt(step.effectiveAcceptance, { reportOptional: isAgentContractV1(step.agentContract), structuredOutput: Boolean(step.structuredOutput?.acceptanceReportPath) });
847
+ if (step.effectiveAcceptance && (step.runner?.type === "external-cli" || step.runner?.type === "external-job")) {
848
+ const acceptancePrompt = formatAcceptancePrompt(step.effectiveAcceptance, { reportOptional: isAgentContract(step.agentContract), structuredOutput: Boolean(step.structuredOutput?.acceptanceReportPath) });
832
849
  if (acceptancePrompt) task = `${task}\n${acceptancePrompt}`;
833
850
  }
834
851
  const sessionEnabled = Boolean(step.sessionFile) || ctx.sessionEnabled;
@@ -1035,11 +1052,31 @@ async function runSingleStepInner(
1035
1052
  let modelIndex = 0;
1036
1053
  let contextOverflow = false;
1037
1054
  let launchWarningsEmitted = false;
1038
- let abortRecoveryAttempted = false;
1055
+ let recoveryState: LogicalRecoveryState = "unused";
1056
+ let readonlyContinuation: RunChildSessionInput["readonlyContinuation"];
1057
+ const continuationBudget = () => {
1058
+ if (step.toolBudget) return "tool-budget-configured" as const;
1059
+ // Refresh the authoritative run ledger, already fed synchronously by onChildEvent.
1060
+ const exhausted = ctx.usageBudgetExhausted?.();
1061
+ if (exhausted === true) return "exhausted" as const;
1062
+ if (!ctx.usageBudget) return "unconfigured" as const;
1063
+ if (ctx.usageBudget.costUsd || !ctx.onChildEvent || exhausted !== false) return "unknown" as const;
1064
+ return "available" as const;
1065
+ };
1066
+ const lifecycleAllowsContinuation = () => !ctx.timeoutSignal?.aborted && !ctx.stopSignal?.aborted
1067
+ && !ctx.skipAcceptance?.() && (ctx.deadlineAt === undefined || Date.now() < ctx.deadlineAt);
1068
+ const canContinue = () => lifecycleAllowsContinuation() && ["available", "unconfigured"].includes(continuationBudget());
1069
+ const failContinuationLaunch = (candidate: string | undefined, error: unknown) => {
1070
+ const message = error instanceof Error ? error.message : String(error);
1071
+ modelAttempts.push({ model: candidate ?? "default", success: false, exitCode: 1, error: message });
1072
+ if (candidate) attemptedModels.push(candidate);
1073
+ if (finalResult) finalResult = { ...finalResult, exitCode: 1, error: message };
1074
+ };
1039
1075
  let nextAttemptTask = task;
1040
1076
  modelAttemptsLoop: while (modelIndex < candidates.length) {
1041
1077
  if (ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break;
1042
- const recoveringAbort = abortRecoveryAttempted;
1078
+ if (readonlyContinuation && !canContinue()) break;
1079
+ const recoveringAbort = recoveryState === "abort-recovery";
1043
1080
  const attemptTask = nextAttemptTask;
1044
1081
  const candidate = candidates[modelIndex];
1045
1082
  const expectedModelForVerification = candidate && !(step.skipPrimaryModelVerification && modelIndex === 0) ? candidate : undefined;
@@ -1047,6 +1084,10 @@ async function runSingleStepInner(
1047
1084
  assertThinkingWithinCeiling({ model: candidate, configThinking: step.thinking, ceiling: step.thinkingCeiling, agent: step.agent, runId: ctx.id });
1048
1085
  } catch (error) {
1049
1086
  const message = error instanceof Error ? error.message : String(error);
1087
+ if (readonlyContinuation) {
1088
+ failContinuationLaunch(candidate, error);
1089
+ break modelAttemptsLoop;
1090
+ }
1050
1091
  return omitUndefinedProperties({ agent: step.agent, output: message, error: message, exitCode: 1, context: step.context, thinkingCeiling: step.thinkingCeiling });
1051
1092
  }
1052
1093
  ctx.onAttemptStart?.(omitUndefinedProperties({
@@ -1072,56 +1113,22 @@ async function runSingleStepInner(
1072
1113
  })
1073
1114
  : undefined;
1074
1115
  let watchdogSink: ((event: ChildWatchdogStatusEvent) => void) | undefined;
1075
- const launch = buildInProcessChildLaunch(omitUndefinedProperties({
1076
- parentSessionId: step.parentSessionId,
1077
- forkCacheKey: step.context === "fork" ? deriveForkPromptCacheKey(step.parentSessionId) : undefined,
1078
- sessionEnabled,
1079
- sessionDir,
1080
- sessionFile: step.sessionFile,
1081
- model: candidate,
1082
- inheritProjectContext: step.inheritProjectContext,
1083
- inheritGlobalContext: step.inheritGlobalContext,
1084
- inheritSkills: step.inheritSkills,
1085
- requireReadTool: Boolean(step.skills?.length),
1086
- tools: step.tools,
1087
- excludeTools: step.excludeTools,
1088
- allowNestedSubagents: step.allowNestedSubagents,
1089
- extensions: step.extensions,
1090
- subagentOnlyExtensions: step.subagentOnlyExtensions,
1091
- fast: step.fast,
1092
- modelCandidates: step.modelCandidates,
1093
- systemPrompt: step.systemPrompt ?? "",
1094
- systemPromptMode: step.systemPromptMode,
1095
- mcpDirectTools: step.mcpDirectTools,
1096
- extensionBindings,
1097
- capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
1098
- cwd: step.cwd ?? ctx.cwd,
1099
- intercomSessionName: ctx.childIntercomTarget,
1100
- sessionName: childSessionName,
1101
- orchestratorIntercomTarget: ctx.orchestratorIntercomTarget,
1102
- runId: ctx.id,
1103
- childAgentName: step.agent,
1104
- childIndex: ctx.flatIndex,
1105
- nestedRoute: ctx.nestedRoute,
1106
- runFanoutBudget: ctx.runFanoutBudget ? {
1107
- ...ctx.runFanoutBudget,
1108
- ...(step.runFanoutPath ? { parentPath: `${ctx.runFanoutBudget.parentPath ? `${ctx.runFanoutBudget.parentPath}/` : ""}${step.runFanoutPath}` } : {}),
1109
- } : undefined,
1110
- structuredOutput: effectiveStructuredOutput,
1111
- toolBudget: step.toolBudget,
1112
- permissionRules: step.permissionRules,
1113
- permissionAuditPath: step.permissionRules && ctx.artifactsDir
1114
- ? path.join(ctx.artifactsDir, "permission-audit", `${ctx.id}-${ctx.flatIndex}.jsonl`)
1115
- : undefined,
1116
- childWatchdog,
1117
- watchdogStatus: (event) => watchdogSink?.(event),
1118
- waitToolEnabled: step.waitToolEnabled,
1119
- waitToolDefaultTimeoutMs: step.waitToolDefaultTimeoutMs,
1120
- thinkingCeiling: step.thinkingCeiling,
1121
- maxSubagentDepth: step.maxSubagentDepth,
1122
- inherited: ctx.inheritedChildRuntime,
1123
- host: "runner",
1124
- }));
1116
+ let launch: ReturnType<typeof buildRunnerChildLaunch>;
1117
+ try {
1118
+ launch = buildRunnerChildLaunch(step, ctx, {
1119
+ sessionEnabled,
1120
+ sessionDir,
1121
+ model: candidate,
1122
+ sessionName: childSessionName,
1123
+ structuredOutput: effectiveStructuredOutput,
1124
+ childWatchdog,
1125
+ watchdogStatus: (event) => watchdogSink?.(event),
1126
+ });
1127
+ } catch (error) {
1128
+ if (!readonlyContinuation) throw error;
1129
+ failContinuationLaunch(candidate, error);
1130
+ break modelAttemptsLoop;
1131
+ }
1125
1132
  if (effectiveStructuredOutput && launch.config.structuredOutput) {
1126
1133
  // The runner reads the value back from the runtime's files after the run.
1127
1134
  launch.config.structuredOutput.capture = createStructuredOutputFileCapture(effectiveStructuredOutput);
@@ -1148,31 +1155,27 @@ async function runSingleStepInner(
1148
1155
  capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
1149
1156
  inheritedCapabilityCeiling: ctx.inheritedChildRuntime?.capabilityCeiling,
1150
1157
  permissionRules: step.permissionRules,
1158
+ hostAvailableBuiltins: ctx.hostAvailableBuiltins,
1151
1159
  }));
1152
1160
  launchResolvedExtensions = projectLaunchResolvedChildExtensions(toolPlan);
1153
- actualLaunchContractDigest = launchBindingDigest(omitUndefinedProperties({
1161
+ actualLaunchContractDigest = resolveLaunchBinding({
1154
1162
  definitionDigest: step.definitionDigest,
1155
- task: step.launchBindingTask ?? task,
1156
- ...(candidate ? { model: candidate } : {}),
1157
- modelCandidates: candidates as string[],
1158
- ...(step.fast !== undefined ? { fast: step.fast } : {}),
1159
- ...(resolveEffectiveThinking(candidate, step.thinking) ? { thinking: resolveEffectiveThinking(candidate, step.thinking) } : {}),
1160
- ...(step.thinkingCeiling ? { thinkingCeiling: step.thinkingCeiling } : {}),
1161
- systemPrompt: step.systemPrompt ?? "",
1162
1163
  systemPromptMode: step.systemPromptMode,
1163
1164
  inheritProjectContext: step.inheritProjectContext,
1164
1165
  inheritGlobalContext: step.inheritGlobalContext,
1165
1166
  inheritSkills: step.inheritSkills,
1167
+ task: step.launchBindingTask ?? task,
1168
+ modelCandidates: candidates as string[],
1169
+ fast: step.fast,
1170
+ thinking: resolveEffectiveThinking(candidate, step.thinking),
1171
+ systemPrompt: step.systemPrompt ?? "",
1166
1172
  skills: step.skills,
1167
- tools: toolPlan.effectiveToolAllowlist,
1168
- ...(toolPlan.excludeTools.length > 0 ? { excludeTools: toolPlan.excludeTools } : {}),
1169
- extensions: toolPlan.extensionArgs,
1170
- mcpDirectTools: toolPlan.effectiveMcpTools,
1171
- ...(step.outputPath ? { outputPath: step.outputPath } : {}),
1172
- ...(step.outputMode ? { outputMode: step.outputMode } : {}),
1173
- ...(step.structuredOutputSchema ? { structuredOutputSchema: step.structuredOutputSchema } : {}),
1174
- ...(extensionBindings ? { extensionBindings } : {}),
1175
- }));
1173
+ toolPlan,
1174
+ outputPath: step.outputPath,
1175
+ outputMode: step.outputMode,
1176
+ structuredOutputSchema: step.structuredOutputSchema,
1177
+ extensionBindings,
1178
+ }).launchContractDigest;
1176
1179
  }
1177
1180
  capabilityAudit = attemptCapabilityAudit;
1178
1181
  // Each attempt rewrites the step output log; synchronous appends keep a
@@ -1181,6 +1184,9 @@ async function runSingleStepInner(
1181
1184
  const run = await runChildSession(omitUndefinedProperties({
1182
1185
  factory: ctx.childSessions,
1183
1186
  launch,
1187
+ collectReadonlyEvidence: true,
1188
+ readonlyContinuation,
1189
+ canContinue,
1184
1190
  prompt: `Task: ${attemptTask}`,
1185
1191
  childWatchdog,
1186
1192
  childEventContext: { runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
@@ -1197,6 +1203,7 @@ async function runSingleStepInner(
1197
1203
  registerTimeout: ctx.registerTimeout,
1198
1204
  registerStop: ctx.registerStop,
1199
1205
  registerSteer: ctx.registerSteer,
1206
+ onSteerOutcome: ctx.onSteerOutcome,
1200
1207
  registerWatchdogStatus: (sink) => { watchdogSink = sink; },
1201
1208
  timeoutMessage: ctx.timeoutMessage,
1202
1209
  stopMessage: ctx.stopMessage,
@@ -1209,6 +1216,9 @@ async function runSingleStepInner(
1209
1216
  modelResponseAliases: step.modelResponseAliases,
1210
1217
  mutationTools: step.mutationTools,
1211
1218
  }));
1219
+ // A parked run still owes completion evidence when it actually finishes.
1220
+ // Stopped/timedOut runs already have terminal failures; checking completion evidence there is meaningless.
1221
+ const completionDiagnosticsEligible = !run.interrupted && !run.stopped && !run.timedOut;
1212
1222
  const toolDiagnostic = run.exitCode === 0 && !run.error ? launch.capture.toolDiagnostic() : undefined;
1213
1223
  const toolAvailabilityError = toolDiagnostic ? formatChildToolDiagnostic(toolDiagnostic) : undefined;
1214
1224
  const runtimeAcknowledgedExtensions = launch.capture.runtimeAcknowledgedExtensions();
@@ -1224,7 +1234,7 @@ async function runSingleStepInner(
1224
1234
  let structuredOutput: unknown;
1225
1235
  let structuredError: string | undefined;
1226
1236
  let validatedStructuredOutput = false;
1227
- if (effectiveStructuredOutput && run.exitCode === 0 && !run.error && !toolAvailabilityError && !midToolExitError) {
1237
+ if (completionDiagnosticsEligible && effectiveStructuredOutput && run.exitCode === 0 && !run.error && !toolAvailabilityError && !midToolExitError) {
1228
1238
  if (!run.structuredOutputToolInvoked) {
1229
1239
  structuredError = MISSING_STRUCTURED_OUTPUT_CALL_ERROR;
1230
1240
  } else {
@@ -1246,13 +1256,13 @@ async function runSingleStepInner(
1246
1256
  const errorMessages = validatedStructuredOutput
1247
1257
  ? run.messages.slice(run.structuredOutputMessageStartIndex ?? run.messages.length)
1248
1258
  : run.messages;
1249
- const hiddenError = run.exitCode === 0 && !run.error && !toolAvailabilityError && !structuredError && !midToolExitError
1259
+ const hiddenError = completionDiagnosticsEligible && run.exitCode === 0 && !run.error && !toolAvailabilityError && !structuredError && !midToolExitError
1250
1260
  ? detectSubagentError(errorMessages)
1251
1261
  : null;
1252
1262
  const terminalEmptyAfterUsefulWork = !validatedStructuredOutput
1253
1263
  && hasEmptyTerminalAssistantResponse(run.messages)
1254
1264
  && (run.toolCount > 0 || Boolean(run.finalOutput.trim()));
1255
- const emptyOutputError = run.exitCode === 0
1265
+ const emptyOutputError = completionDiagnosticsEligible && run.exitCode === 0
1256
1266
  && !run.error
1257
1267
  && !toolAvailabilityError
1258
1268
  && !structuredError
@@ -1261,13 +1271,13 @@ async function runSingleStepInner(
1261
1271
  && (!hiddenError?.hasError || hasEmptyTerminalAssistantResponse(run.messages))
1262
1272
  ? formatEmptyTerminalAssistantResponseError(run.messages)
1263
1273
  : undefined;
1264
- const completionGuardEnabled = isAgentContractV1(step.agentContract) ? step.completionGuard === true : step.completionGuard !== false;
1274
+ const completionGuardEnabled = isAgentContract(step.agentContract) ? step.completionGuard === true : step.completionGuard !== false;
1265
1275
  const completionToolPlan = resolvedTaskToolPlan;
1266
1276
  const completionTools = completionToolPlan ? (completionToolPlan.explicitToolAllowlist ? completionToolPlan.effectiveToolAllowlist : undefined) : step.tools;
1267
1277
  const mutationEvidence = collectTrackedMutationEvidence(mutationSnapshot, step.cwd ?? ctx.cwd);
1268
1278
  finalMutationEvidence = mutationEvidence;
1269
1279
  const completionMutationEvidence = ctx.trackedMutationEvidenceForCompletionGuard === false ? undefined : mutationEvidence;
1270
- const completionGuard = run.exitCode === 0 && !run.error && !structuredError && !hiddenError?.hasError && !midToolExitError && !emptyOutputError && completionGuardEnabled
1280
+ const completionGuard = completionDiagnosticsEligible && run.exitCode === 0 && !run.error && !structuredError && !hiddenError?.hasError && !midToolExitError && !emptyOutputError && completionGuardEnabled
1271
1281
  ? evaluateCompletionMutationGuard(omitUndefinedProperties({
1272
1282
  agent: step.agent,
1273
1283
  task: taskForCompletionGuard,
@@ -1280,14 +1290,27 @@ async function runSingleStepInner(
1280
1290
  }))
1281
1291
  : undefined;
1282
1292
  const mutationAttemptObserved = run.observedMutationAttempt === true || completionMutationEvidence?.attemptedMutation === true;
1293
+ let arbitration = { triggered: completionGuard?.triggered === true && !mutationAttemptObserved, rescued: false };
1294
+ if (arbitration.triggered) {
1295
+ const modelContext = launch.capture.completionIntentContext?.();
1296
+ arbitration = await arbitrateCompletionGuardRescue({
1297
+ guardTriggered: true,
1298
+ task: taskForCompletionGuard,
1299
+ // Construct lazily too: the shared gate refuses overlength tasks before
1300
+ // registry/auth/model work. The child has already shut down normally.
1301
+ arbiter: modelContext ? async (task) => createTaskMutationArbiter(modelContext)?.(task) ?? "unavailable" : undefined,
1302
+ });
1303
+ }
1283
1304
  const completionEvidence = planCompletionEvidence({
1284
1305
  guard: completionGuard,
1306
+ guardTriggered: arbitration.triggered,
1307
+ arbiterRescued: arbitration.rescued,
1285
1308
  completionGuardEnabled,
1286
1309
  mutationCapable: hasMutationToolCapability(completionTools, completionToolPlan?.effectiveMcpTools ?? step.mcpDirectTools),
1287
1310
  implementationMutationExpected: expectsImplementationMutation(step.agent, taskForCompletionGuard),
1288
1311
  mutationAttemptObserved,
1289
1312
  mutationEvidence: completionMutationEvidence,
1290
- agentContractV1: isAgentContractV1(step.agentContract),
1313
+ agentContractEnabled: isAgentContract(step.agentContract),
1291
1314
  });
1292
1315
  const finalOutputHasPersistableFileContent = run.exitCode === 0 && !run.error && !emptyOutputError && Boolean(stripAcceptanceReport(run.finalOutput).trim());
1293
1316
  const requiredOutput = step.outputMode === "file-only" && step.outputPath
@@ -1296,7 +1319,7 @@ async function runSingleStepInner(
1296
1319
  ? { kind: "structured" as const, path: effectiveStructuredOutput.outputPath, missing: !fs.existsSync(effectiveStructuredOutput.outputPath) }
1297
1320
  : undefined;
1298
1321
  finalRequiredOutputMissing = requiredOutput?.missing;
1299
- const missingRequiredOutputError = formatRequiredOutputError(requiredOutput);
1322
+ const missingRequiredOutputError = completionDiagnosticsEligible ? formatRequiredOutputError(requiredOutput) : undefined;
1300
1323
  const missingRequiredOutputAfterMutation = Boolean(missingRequiredOutputError) && (mutationAttemptObserved || Boolean(mutationEvidence.changedFiles.length));
1301
1324
  const effectiveExitCode = toolAvailabilityError || completionEvidence.legacyFailureError || midToolExitError || structuredError || emptyOutputError || missingRequiredOutputError
1302
1325
  ? 1
@@ -1347,7 +1370,7 @@ async function runSingleStepInner(
1347
1370
  messages: run.messages,
1348
1371
  error,
1349
1372
  sessionAvailable: Boolean(step.sessionFile && fs.existsSync(step.sessionFile)),
1350
- alreadyResumed: abortRecoveryAttempted,
1373
+ alreadyResumed: recoveryState !== "unused",
1351
1374
  stopped: run.stopped || ctx.stopSignal?.aborted || ctx.skipAcceptance?.(),
1352
1375
  interrupted: run.interrupted,
1353
1376
  timedOut: run.timedOut || ctx.timeoutSignal?.aborted,
@@ -1367,15 +1390,59 @@ async function runSingleStepInner(
1367
1390
  if (run.stopped || run.timedOut || ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break modelAttemptsLoop;
1368
1391
  if (abortRecovery?.action === "settle" && abortRecovery.diagnostic) break modelAttemptsLoop;
1369
1392
  if (attempt.success) break modelAttemptsLoop;
1393
+ // The shared token is consumed before sibling creation; no third dispatch of any kind.
1394
+ if (recoveryState === "readonly-continuation") break modelAttemptsLoop;
1370
1395
  if (recoveringAbort) break modelAttemptsLoop;
1371
1396
  if (abortRecovery?.action === "resume") {
1372
- abortRecoveryAttempted = true;
1397
+ recoveryState = "abort-recovery";
1373
1398
  nextAttemptTask = abortRecovery.prompt;
1374
1399
  attemptNotes.push("[abort-recovery] provider/transport abort after useful progress; resuming the retained child session once.");
1375
1400
  continue;
1376
1401
  }
1377
1402
  if (completionEvidence.guardTriggered) break modelAttemptsLoop;
1378
1403
 
1404
+ const source = getSettledReadonlyChild(run);
1405
+ if (source) {
1406
+ const captured = launch.capture.completionIntentContext?.();
1407
+ const sourceModel = captured?.model;
1408
+ const retained = getReadonlySessionEvidence(source)!;
1409
+ // Modality/capacity assessment, not another history/provenance validator.
1410
+ // Images and unknown content have no certified token bound in this host slice.
1411
+ const textOnly = (JSON.parse(retained.contextJson) as Message[]).every((message) => typeof message.content === "string"
1412
+ || (Array.isArray(message.content) && message.content.every((block) => ["text", "thinking", "toolCall"].includes(block.type))));
1413
+ const retainedBytes = Buffer.byteLength(retained.contextJson, "utf8");
1414
+ const resolvedCandidates = candidates.map((reference, index) => {
1415
+ // Only exact registry identities qualify; aliases/default guesses remain ineligible.
1416
+ const base = reference ? splitKnownThinkingSuffix(reference).baseModel : "";
1417
+ const slash = base.indexOf("/");
1418
+ const model = slash > 0 ? captured?.modelRegistry.find(base.slice(0, slash), base.slice(slash + 1)) : undefined;
1419
+ // Reserve the source window for runtime/system/tool overhead, plus a conservative
1420
+ // byte bound for the complete retained text and new prompt; never assume a fit
1421
+ // merely because a 429 request was sent. Equal/smaller windows remain denied.
1422
+ const compatible = textOnly && model && sourceModel && model.input?.includes("text")
1423
+ && Number.isFinite(sourceModel.contextWindow) && sourceModel.contextWindow > 0
1424
+ && model.contextWindow >= sourceModel.contextWindow + retainedBytes + Buffer.byteLength(READONLY_CONTINUATION_PROMPT, "utf8")
1425
+ && model.maxTokens > 0 && model.maxTokens <= sourceModel.maxTokens;
1426
+ return { resolved: model ? { provider: model.provider, model: model.id, api: model.api } : undefined,
1427
+ tried: index <= modelIndex, compatibility: compatible ? "compatible" as const : "unknown" as const };
1428
+ });
1429
+ const plan = planReadonlyModelContinuation({ source, recoveryState, candidates: resolvedCandidates, currentIndex: modelIndex,
1430
+ lifecycleAllowsContinuation: lifecycleAllowsContinuation() && !run.interrupted && !run.stopped && !run.timedOut,
1431
+ effectsAllowContinuation: !run.currentTool && !run.observedMutationAttempt && !mutationEvidence.attemptedMutation
1432
+ && !structuredError && !effectiveStructuredOutput && !missingRequiredOutputError && !toolAvailabilityError
1433
+ && !completionEvidence.guardTriggered && !midToolExitError && !hiddenError?.hasError,
1434
+ budget: continuationBudget(), knownContextOverflow: isContextOverflow(error) });
1435
+ if (plan.kind === "continue" && canContinue()) {
1436
+ recoveryState = plan.recoveryState;
1437
+ const selected = resolvedCandidates[plan.candidateIndex]!.resolved!;
1438
+ readonlyContinuation = { source, expected: plan.expected, modelId: `${selected.provider}/${selected.model}` };
1439
+ nextAttemptTask = plan.prompt;
1440
+ modelIndex = plan.candidateIndex;
1441
+ attemptNotes.push(`[readonly-continuation] ${attempt.model} returned HTTP 429 after read-only progress; continuing the retained session once with ${candidates[modelIndex]}.`);
1442
+ continue;
1443
+ }
1444
+ }
1445
+
1379
1446
  const retryableModelFailure = isRetryableModelFailureAttempt({ error, messages: run.messages, toolCount: run.toolCount });
1380
1447
  if (retryableModelFailure) recordRetryableModelFailure(candidate ?? run.model ?? step.model, error);
1381
1448
  if (isContextOverflow(error)) {
@@ -1457,7 +1524,7 @@ async function runSingleStepInner(
1457
1524
  cwd: step.cwd ?? ctx.cwd,
1458
1525
  signal: combinedAbortSignal([ctx.timeoutSignal, ctx.stopSignal]),
1459
1526
  abortMessage: ctx.stopSignal?.aborted ? ctx.stopMessage ?? "Subagent stopped by user." : ctx.timeoutMessage ?? "Subagent timed out.",
1460
- reportOptional: isAgentContractV1(step.agentContract),
1527
+ reportOptional: isAgentContract(step.agentContract),
1461
1528
  artifactsDir: ctx.artifactsDir,
1462
1529
  runId: ctx.id,
1463
1530
  watchdog: finalResult?.watchdog,
@@ -1473,7 +1540,7 @@ async function runSingleStepInner(
1473
1540
  : acceptance
1474
1541
  : undefined;
1475
1542
  const acceptanceFailure = effectiveAcceptance ? acceptanceFailureMessage(effectiveAcceptance) : undefined;
1476
- const acceptanceCanFailRun = acceptanceFailure && effectiveAcceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted && !timedOutAfterAcceptance && !stoppedAfterAcceptance && !isAgentContractV1(step.agentContract);
1543
+ const acceptanceCanFailRun = acceptanceFailure && effectiveAcceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted && !timedOutAfterAcceptance && !stoppedAfterAcceptance && !isAgentContract(step.agentContract);
1477
1544
  const effectiveFinalExitCode = timedOutAfterAcceptance || stoppedAfterAcceptance ? 1 : acceptanceCanFailRun ? 1 : finalResult?.exitCode ?? 1;
1478
1545
  const intercomDetachReceipt = finalResult?.finalOutput === INTERCOM_DETACH_RECEIPT;
1479
1546
  const baseFinalError = stoppedAfterAcceptance
@@ -1515,7 +1582,7 @@ async function runSingleStepInner(
1515
1582
  ...(capabilityAudit ? { capabilityCeiling: capabilityAudit.ceiling, capabilityAudit } : {}),
1516
1583
  launchContractDigest: actualLaunchContractDigest,
1517
1584
  launchResolvedExtensions,
1518
- ...((finalResult as (RunChildSessionResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }) | undefined)?.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: (finalResult as RunChildSessionResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }).runtimeAcknowledgedExtensions } : {}),
1585
+ ...((finalResult as (RunChildSessionResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions }) | undefined)?.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: (finalResult as RunChildSessionResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions }).runtimeAcknowledgedExtensions } : {}),
1519
1586
  ...(transcriptWriter ? { transcriptPath: artifactPaths.transcriptPath } : {}),
1520
1587
  transcriptError: transcriptWriter?.getError(),
1521
1588
  skills: step.skills,
@@ -1544,6 +1611,7 @@ async function runSingleStepInner(
1544
1611
  totalCost: costSummaryFromAttempts(modelAttempts),
1545
1612
  usage,
1546
1613
  artifactPaths,
1614
+ savedOutputPath: finalizedOutput.savedPath,
1547
1615
  outputSaveError: [resolvedOutput.saveError, artifactErrors.outputSaveError].filter(Boolean).join("\n") || undefined,
1548
1616
  metadataSaveError: artifactErrors.metadataSaveError,
1549
1617
  transcriptPath: transcriptWriter ? artifactPaths?.transcriptPath : undefined,
@@ -1563,9 +1631,9 @@ async function runSingleStepInner(
1563
1631
  watchdog: finalResult?.watchdog,
1564
1632
  ...(capabilityAudit ? { capabilityCeiling: capabilityAudit.ceiling, capabilityAudit } : {}),
1565
1633
  launchResolvedExtensions,
1566
- ...((finalResult as (RunChildSessionResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }) | undefined)?.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: (finalResult as RunChildSessionResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }).runtimeAcknowledgedExtensions } : {}),
1634
+ ...((finalResult as (RunChildSessionResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions }) | undefined)?.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: (finalResult as RunChildSessionResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions }).runtimeAcknowledgedExtensions } : {}),
1567
1635
  });
1568
- return isAgentContractV1(step.agentContract) ? attachContractProjections(result as unknown as import("../../shared/types.ts").SingleResult) as unknown as typeof result : result;
1636
+ return isAgentContract(step.agentContract) ? attachContractProjections(result as unknown as import("../../shared/types.ts").SingleResult) as unknown as typeof result : result;
1569
1637
  }
1570
1638
 
1571
1639
  async function runSingleStep(
@@ -1656,12 +1724,15 @@ function markParallelGroupSetupFailure(input: {
1656
1724
  const statusStep = requiredStatusStep(input.statusPayload, flatTaskIndex);
1657
1725
  const task = input.group.parallel[taskIndex];
1658
1726
  if (!task) throw new Error(`Missing parallel task at index ${taskIndex}`);
1659
- statusStep.status = "failed";
1727
+ const stopped = statusStep.stopped || statusStep.stopRequested || input.statusPayload.stopped;
1728
+ const paused = !stopped && input.statusPayload.state === "paused";
1729
+ statusStep.status = stopped ? "stopped" : paused ? "paused" : "failed";
1660
1730
  statusStep.startedAt = input.failedAt;
1661
1731
  statusStep.endedAt = input.failedAt;
1662
1732
  statusStep.durationMs = 0;
1663
- statusStep.exitCode = 1;
1664
- input.results.push(omitUndefinedProperties({ agent: task.agent, context: task.context, output: input.setupError, success: false, exitCode: 1, sessionFile: task.sessionFile }));
1733
+ statusStep.exitCode = paused ? 0 : 1;
1734
+ statusStep.error ??= input.setupError;
1735
+ input.results.push(omitUndefinedProperties({ agent: task.agent, context: task.context, output: input.setupError, error: input.setupError, success: false, exitCode: paused ? 0 : 1, sessionFile: task.sessionFile, stopped: stopped || undefined, interrupted: paused || undefined, timedOut: input.statusPayload.timedOut || undefined }));
1665
1736
  }
1666
1737
  input.statusPayload.currentStep = input.groupStartFlatIndex;
1667
1738
  input.statusPayload.lastUpdate = input.failedAt;
@@ -1818,7 +1889,10 @@ async function runSingleStepWithTimeout(
1818
1889
  ctx: SingleStepContext,
1819
1890
  parentDeadlineAt?: number,
1820
1891
  ): Promise<SingleStepResult> {
1821
- if (step.timeoutMs === undefined) return runSingleStep(step, ctx);
1892
+ if (step.timeoutMs === undefined) return runSingleStep(step, parentDeadlineAt === undefined ? ctx : {
1893
+ ...ctx,
1894
+ deadlineAt: ctx.deadlineAt === undefined ? parentDeadlineAt : Math.min(ctx.deadlineAt, parentDeadlineAt),
1895
+ });
1822
1896
 
1823
1897
  const parentRemainingMs = parentDeadlineAt === undefined ? undefined : Math.max(0, parentDeadlineAt - Date.now());
1824
1898
  const timeoutMs = parentRemainingMs === undefined ? step.timeoutMs : Math.min(step.timeoutMs, parentRemainingMs);
@@ -1855,7 +1929,7 @@ async function runSingleStepWithTimeout(
1855
1929
  }
1856
1930
  }
1857
1931
 
1858
- async function runSubagent(
1932
+ export async function runSubagent(
1859
1933
  config: SubagentRunConfig,
1860
1934
  childSessions: ChildSessionFactory,
1861
1935
  ): Promise<void> {
@@ -1893,6 +1967,8 @@ async function runSubagent(
1893
1967
  const stopMessage = "Subagent stopped by user.";
1894
1968
  const timeoutAbortController = new AbortController();
1895
1969
  const stopAbortController = new AbortController();
1970
+ const setupInterruptController = new AbortController();
1971
+ const setupSignal = AbortSignal.any([timeoutAbortController.signal, stopAbortController.signal, setupInterruptController.signal]);
1896
1972
  let previousCumulativeTokens: TokenUsage = { input: 0, output: 0, total: 0 };
1897
1973
  let latestSessionFile: string | undefined;
1898
1974
 
@@ -2059,20 +2135,29 @@ async function runSubagent(
2059
2135
  },
2060
2136
  onError: (error, filePath) => console.error(`Failed to update async run index '${filePath}':`, error),
2061
2137
  });
2062
- const queueActiveRunIndex = (): void => {
2063
- const state = statusPayload.state;
2138
+ const queueActiveRunIndex = (status: AsyncStatus): void => {
2139
+ const state = status.state;
2064
2140
  if (state === lastIndexedStatusState && indexPersistence.pendingCount() === 0) return;
2065
- indexPersistence.write(asyncDir, { state, toolCallId: statusPayload.toolCallId }, (_filePath, payload) => {
2141
+ indexPersistence.write(asyncDir, { state, toolCallId: status.toolCallId }, (_filePath, payload) => {
2066
2142
  const indexPayload = payload as { state: AsyncStatus["state"]; toolCallId?: string };
2067
2143
  updateActiveRunIndex(asyncDir, indexPayload.state, indexPayload.toolCallId, { retryCapacityErrors: true });
2068
2144
  });
2069
2145
  };
2146
+ let finalResultCommitted = false;
2147
+ let finalResultPublication: { resolve(): void; reject(error: unknown): void } | undefined;
2070
2148
  const runPersistence = createCapacityResilientJsonWriter({
2071
2149
  keepAlive: true,
2072
- onSuccess: (filePath) => {
2073
- if (filePath === statusPath) queueActiveRunIndex();
2150
+ onSuccess: (filePath, payload) => {
2151
+ if (filePath === statusPath) queueActiveRunIndex(payload as AsyncStatus);
2152
+ if (filePath === resultPath && finalResultPublication) {
2153
+ finalResultCommitted = true;
2154
+ finalResultPublication.resolve();
2155
+ }
2156
+ },
2157
+ onError: (error, filePath) => {
2158
+ console.error(`Failed to persist async run state '${filePath}':`, error);
2159
+ if (filePath === resultPath) finalResultPublication?.reject(error);
2074
2160
  },
2075
- onError: (error, filePath) => console.error(`Failed to persist async run state '${filePath}':`, error),
2076
2161
  });
2077
2162
  try {
2078
2163
  fs.mkdirSync(asyncDir, { recursive: true });
@@ -2080,7 +2165,7 @@ async function runSubagent(
2080
2165
  if (!isStorageCapacityError(error)) throw error;
2081
2166
  console.error(`Failed to prepare async run storage '${asyncDir}' while storage is full:`, error);
2082
2167
  }
2083
- runPersistence.write(statusPath, statusPayload);
2168
+ runPersistence.write(statusPath, { ...statusPayload });
2084
2169
 
2085
2170
  let pendingParallelUsageCost: CostSummary = { inputTokens: 0, outputTokens: 0, costUsd: 0 };
2086
2171
  const currentUsageTotals = (): CostSummary => {
@@ -2099,6 +2184,13 @@ async function runSubagent(
2099
2184
  setOptionalProperty(statusPayload, "usageBudget", usageBudgetState(config.usageBudget, currentUsageTotals()));
2100
2185
  return statusPayload.usageBudget;
2101
2186
  };
2187
+ // Continuation admission only: the existing ledger has no in-flight cost or
2188
+ // external/import usage coverage. Never change ordinary budget enforcement.
2189
+ let continuationUsageUncertain = config.steps.some(isDynamicRunnerGroup) || flatSteps.some((step) => Boolean(step.runner || step.importAsyncRoot));
2190
+ const continuationUsageBudgetExhausted = (): boolean | undefined => {
2191
+ const exhausted = refreshUsageBudget()?.exhausted === true;
2192
+ return exhausted ? true : config.usageBudget && continuationUsageUncertain ? undefined : false;
2193
+ };
2102
2194
  const emitNestedSelfEvent = (type: "subagent.nested.updated" | "subagent.nested.completed"): void => {
2103
2195
  if (!config.nestedRoute || !config.nestedSelf) return;
2104
2196
  try {
@@ -2153,7 +2245,6 @@ async function runSubagent(
2153
2245
  for (const node of graph.nodes) updateNode(node);
2154
2246
  statusPayload.workflowGraph = graph;
2155
2247
  };
2156
- let finalResultCommitted = false;
2157
2248
  const statusResultState = (): AsyncStatus["state"] | undefined => {
2158
2249
  if (statusPayload.state === "running" || statusPayload.state === "queued") return undefined;
2159
2250
  return statusPayload.state;
@@ -2215,14 +2306,15 @@ async function runSubagent(
2215
2306
  }), (filePath, payload) => writePendingAsyncResultFile(filePath, payload as Record<string, unknown>));
2216
2307
  };
2217
2308
  const writeStatusPayloadNow = (): void => {
2309
+ if (finalResultPublication) return;
2218
2310
  refreshWorkflowGraph();
2219
2311
  writeRecoverableStatusResult();
2220
- runPersistence.write(statusPath, statusPayload);
2312
+ runPersistence.write(statusPath, { ...statusPayload });
2221
2313
  emitNestedSelfEvent(statusPayload.state === "running" || statusPayload.state === "queued" ? "subagent.nested.updated" : "subagent.nested.completed");
2222
2314
  };
2223
2315
  const statusWriteCoalescer = createFileCoalescer(writeStatusPayloadNow, 100);
2224
2316
  const writeStatusPayload = (immediate = true): void => {
2225
- if (immediate || statusPayload.state !== "running" || statusPayload.activityState !== undefined) {
2317
+ if (immediate || statusPayload.state !== "running") {
2226
2318
  if (!statusWriteCoalescer.flush(statusPath)) writeStatusPayloadNow();
2227
2319
  return;
2228
2320
  }
@@ -2825,7 +2917,10 @@ async function runSubagent(
2825
2917
  const applySteerDelivery = (requestId: string, index: number, delivery: { state: "delivered" | "queued" | "failed"; message: string }): void => {
2826
2918
  const lifecycle = steeringStatus(statusPayload);
2827
2919
  const request = lifecycle.recent.find((candidate) => candidate.id === requestId);
2828
- if (!request || !request.targets.some((target) => target.index === index)) return;
2920
+ const target = request?.targets.find((candidate) => candidate.index === index);
2921
+ if (!request || !target) return;
2922
+ if (target.state === "delivered" || target.state === "late" || target.state === "failed") return;
2923
+ if (target.state === delivery.state) return;
2829
2924
  const late = fs.existsSync(steeringMarkerPath(requestId));
2830
2925
  const now = Date.now();
2831
2926
  if (delivery.state === "delivered") {
@@ -2878,6 +2973,7 @@ async function runSubagent(
2878
2973
  const updateStepFromChildEvent = (flatIndex: number, event: ChildEvent): void => {
2879
2974
  const step = statusPayload.steps[flatIndex];
2880
2975
  if (!step) return;
2976
+ const previousActivityState = step.activityState;
2881
2977
  const now = Date.now();
2882
2978
  statusPayload.currentStep = flatIndex;
2883
2979
  if (isChildWatchdogStatusEvent(event)) {
@@ -3012,6 +3108,10 @@ async function runSubagent(
3012
3108
  appendRecentStepOutput(step, stripAcceptanceReport(extractTextFromContent(event.message.content)).split("\n").slice(-10));
3013
3109
  step.turnCount = (step.turnCount ?? 0) + 1;
3014
3110
  const usage = event.message.usage;
3111
+ if (config.usageBudget) {
3112
+ const known = (value: unknown) => typeof value === "number" && Number.isFinite(value) && value >= 0;
3113
+ if (!known(usage?.input ?? usage?.inputTokens) || !known(usage?.output ?? usage?.outputTokens)) continuationUsageUncertain = true;
3114
+ }
3015
3115
  if (usage) {
3016
3116
  const input = usage.input ?? usage.inputTokens ?? 0;
3017
3117
  const output = usage.output ?? usage.outputTokens ?? 0;
@@ -3031,7 +3131,8 @@ async function runSubagent(
3031
3131
  statusPayload.lastActivityAt = now;
3032
3132
  statusPayload.lastUpdate = now;
3033
3133
  maybeEmitActiveLongRunning(flatIndex, now);
3034
- writeStatusPayload(false);
3134
+ // A sibling may keep aggregate attention unchanged; publish this step's transition.
3135
+ writeStatusPayload(step.activityState !== previousActivityState);
3035
3136
  };
3036
3137
  const updateRunnerActivityState = (now: number): boolean => {
3037
3138
  if (!controlConfig.enabled) return false;
@@ -3103,10 +3204,55 @@ async function runSubagent(
3103
3204
  activityTimer.unref?.();
3104
3205
  }
3105
3206
 
3207
+ const publishSetupUnknown = (progress: WorktreeSetupProgress): void => {
3208
+ if (!progress.unknown) return;
3209
+ const proof = {
3210
+ version: 1 as const, state: "unknown" as const, runId: id,
3211
+ runnerProcessInstanceId: config.runnerProcessInstanceId ?? "unknown",
3212
+ reason: "process-tree-unverified" as const,
3213
+ diagnostic: `Worktree setup settlement unknown: ${progress.unknown}; manual reconciliation required; ${parallelHandoffPath(asyncDir)}`,
3214
+ };
3215
+ statusPayload.processTerminal = proof;
3216
+ // Publish the sticky proof independently of in-process child writer counts.
3217
+ writeAtomicJson(processTerminalPath(asyncDir), proof);
3218
+ };
3219
+ const finalizeWorktree = async (setup: WorktreeSetup, stepIndex: number, flatStartIndex: number, action: () => void, deadlineAt?: number): Promise<void> => {
3220
+ let admitted = false;
3221
+ try { await withWorktreeTransaction(() => {
3222
+ if (deadlineAt !== undefined && Date.now() >= deadlineAt) throw new Error("Run deadline expired before worktree cleanup");
3223
+ admitted = true;
3224
+ action();
3225
+ }); }
3226
+ catch (error) {
3227
+ if (admitted) throw error;
3228
+ const reason = `Worktree finalization retained; manual reconciliation required: ${error instanceof Error ? error.message : String(error)}`;
3229
+ statusPayload.parallelHandoff = writeParallelHandoffGroup({
3230
+ manifestPath: parallelHandoffPath(asyncDir), runId: id,
3231
+ mode: (config.resultMode ?? statusPayload.mode) === "parallel" ? "parallel" : (config.resultMode ?? statusPayload.mode) === "single" ? "single" : "chain",
3232
+ source: "async", cwd, stepIndex, flatStartIndex, setup, diffs: [], results: [],
3233
+ cleanup: { state: "partial", pruned: false, errors: [reason], tasks: setup.worktrees.map((worktree) => ({
3234
+ index: worktree.index, path: worktree.path, branch: worktree.branch, provider: worktree.provider, naming: worktree.naming,
3235
+ worktreeRemoved: false, branchRemoved: false, preserved: true, reason,
3236
+ })) },
3237
+ });
3238
+ previousOutput = [previousOutput, reason, formatParallelHandoffReference(statusPayload.parallelHandoff)].filter(Boolean).join("\n\n");
3239
+ writeStatusPayload();
3240
+ }
3241
+ };
3242
+ const cleanupRemainingWorktree = (setup: WorktreeSetup, stepIndex: number, flatStartIndex: number) => finalizeWorktree(setup, stepIndex, flatStartIndex, () => {
3243
+ const cleanup = cleanupWorktrees(setup);
3244
+ statusPayload.parallelHandoff = writeParallelHandoffGroup({
3245
+ manifestPath: parallelHandoffPath(asyncDir), runId: id,
3246
+ mode: (config.resultMode ?? statusPayload.mode) === "parallel" ? "parallel" : (config.resultMode ?? statusPayload.mode) === "single" ? "single" : "chain",
3247
+ source: "async", cwd, stepIndex, flatStartIndex, setup, diffs: [], results: [], cleanup,
3248
+ });
3249
+ writeStatusPayload();
3250
+ }, config.deadlineAt);
3106
3251
  const interruptRunner = () => {
3107
3252
  consumeInterruptRequest(asyncDir);
3108
3253
  if (interrupted || statusPayload.state !== "running") return;
3109
3254
  interrupted = true;
3255
+ setupInterruptController.abort();
3110
3256
  const now = Date.now();
3111
3257
  statusPayload.state = "paused";
3112
3258
  currentActivityState = undefined;
@@ -3340,14 +3486,14 @@ async function runSubagent(
3340
3486
  cwd,
3341
3487
  signal: combinedAbortSignal([timeoutAbortController.signal, stopAbortController.signal]),
3342
3488
  abortMessage: stopAbortController.signal.aborted ? stopMessage : timeoutMessage ?? "Subagent timed out.",
3343
- reportOptional: isAgentContractV1(step.agentContract),
3489
+ reportOptional: isAgentContract(step.agentContract),
3344
3490
  }))
3345
3491
  : undefined;
3346
3492
  const groupStopped = stopped || stopAbortController.signal.aborted;
3347
3493
  const groupTimedOut = !groupStopped && (timedOut || timeoutAbortController.signal.aborted);
3348
3494
  const effectiveGroupAcceptance = groupTimedOut || groupStopped ? undefined : groupAcceptance;
3349
3495
  if (placeholder && effectiveGroupAcceptance) placeholder.acceptance = effectiveGroupAcceptance;
3350
- const groupAcceptanceFailure = effectiveGroupAcceptance && (!isAgentContractV1(step.agentContract) || step.gateOn === "acceptance") ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
3496
+ const groupAcceptanceFailure = effectiveGroupAcceptance && (!isAgentContract(step.agentContract) || step.gateOn === "acceptance") ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
3351
3497
  if (groupTimedOut || groupStopped || groupAcceptanceFailure) {
3352
3498
  const errorMessage = groupStopped ? stopMessage : groupTimedOut ? timeoutMessage ?? "Subagent timed out." : groupAcceptanceFailure!;
3353
3499
  statusPayload.state = groupStopped ? "stopped" : "failed";
@@ -3549,10 +3695,12 @@ async function runSubagent(
3549
3695
  nestedRoute: config.nestedRoute,
3550
3696
  capabilityCeiling: config.capabilityCeiling,
3551
3697
  runFanoutBudget: config.runFanoutBudget,
3698
+ hostAvailableBuiltins: config.hostAvailableBuiltins,
3552
3699
  registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
3553
3700
  registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
3554
3701
  registerStop: (stop) => registerStepStop(fi, stop),
3555
3702
  registerSteer: (steer) => registerStepSteer(fi, steer),
3703
+ onSteerOutcome: (request, delivery) => applySteerDelivery(request.id, fi, delivery),
3556
3704
  timeoutSignal: timeoutAbortController.signal,
3557
3705
  stopSignal: stopAbortController.signal,
3558
3706
  trackedMutationEvidenceForCompletionGuard: false,
@@ -3564,7 +3712,8 @@ async function runSubagent(
3564
3712
  onExternalProcess: (process) => updateExternalProcess(fi, process),
3565
3713
  onExternalJob: (externalJob) => updateExternalJob(fi, externalJob),
3566
3714
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(fi),
3567
- usageBudgetExhausted: () => refreshUsageBudget()?.exhausted === true,
3715
+ usageBudgetExhausted: continuationUsageBudgetExhausted,
3716
+ usageBudget: config.usageBudget,
3568
3717
  orcaProgressTab,
3569
3718
  }), config.deadlineAt);
3570
3719
  const taskEndTime = Date.now();
@@ -3680,7 +3829,7 @@ async function runSubagent(
3680
3829
  const failures = parallelResults.filter((result) => result.exitCode !== 0 && result.exitCode !== -1);
3681
3830
  const acceptanceFailures = parallelResults
3682
3831
  .map((result, originalIndex) => ({ result, originalIndex, task: dynamicSteps[originalIndex] }))
3683
- .filter(({ result, task }) => isAgentContractV1(task?.agentContract ?? step.agentContract) && task?.gateOn === "acceptance" && result.acceptance?.status === "rejected");
3832
+ .filter(({ result, task }) => isAgentContract(task?.agentContract ?? step.agentContract) && task?.gateOn === "acceptance" && result.acceptance?.status === "rejected");
3684
3833
  if (acceptanceFailures.length > 0) {
3685
3834
  const message = acceptanceFailures
3686
3835
  .map(({ result, originalIndex }) => `Dynamic item ${originalIndex + 1} (${result.agent}, key ${materialized.items[originalIndex]?.key ?? originalIndex}) acceptance rejected: ${(result.acceptance ? acceptanceFailureMessage(result.acceptance) : undefined) ?? "acceptance rejected"}`)
@@ -3710,13 +3859,13 @@ async function runSubagent(
3710
3859
  cwd,
3711
3860
  signal: combinedAbortSignal([timeoutAbortController.signal, stopAbortController.signal]),
3712
3861
  abortMessage: stopAbortController.signal.aborted ? stopMessage : timeoutMessage ?? "Subagent timed out.",
3713
- reportOptional: isAgentContractV1(step.agentContract),
3862
+ reportOptional: isAgentContract(step.agentContract),
3714
3863
  }))
3715
3864
  : undefined;
3716
3865
  const groupStopped = stopped || stopAbortController.signal.aborted;
3717
3866
  const groupTimedOut = !groupStopped && (timedOut || timeoutAbortController.signal.aborted);
3718
3867
  const effectiveGroupAcceptance = groupTimedOut || groupStopped ? undefined : groupAcceptance;
3719
- const groupAcceptanceFailure = effectiveDynamicGroupAcceptance.explicit && effectiveGroupAcceptance && (!isAgentContractV1(step.agentContract) || step.gateOn === "acceptance") ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
3868
+ const groupAcceptanceFailure = effectiveDynamicGroupAcceptance.explicit && effectiveGroupAcceptance && (!isAgentContract(step.agentContract) || step.gateOn === "acceptance") ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
3720
3869
  const groupError = groupStopped ? stopMessage : groupTimedOut ? timeoutMessage ?? "Subagent timed out." : groupAcceptanceFailure;
3721
3870
  markDynamicGraphGroup(stepIndex, groupError ? groupStopped ? "stopped" : "failed" : "completed", groupError, effectiveGroupAcceptance);
3722
3871
  if (groupError) {
@@ -3795,7 +3944,9 @@ async function runSubagent(
3795
3944
  break;
3796
3945
  }
3797
3946
  try {
3798
- worktreeSetup = createWorktrees(cwd, `${id}-s${stepIndex}`, group.parallel.length, omitUndefinedProperties({
3947
+ worktreeSetup = await createWorktrees(cwd, `${id}-s${stepIndex}`, group.parallel.length, omitUndefinedProperties({
3948
+ signal: setupSignal,
3949
+ deadlineAt: config.deadlineAt,
3799
3950
  agents: group.parallel.map((task) => task.agent),
3800
3951
  labels: group.parallel.map((task) => task.lane?.key ?? config.workflowKey ?? task.outputName ?? task.label),
3801
3952
  tasks: group.parallel.map((task) => task.task),
@@ -3806,9 +3957,10 @@ async function runSubagent(
3806
3957
  ? omitUndefinedProperties({ hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs })
3807
3958
  : undefined,
3808
3959
  baseDir: config.worktreeBaseDir,
3809
- beforeCreate: (plannedSetup) => {
3810
- for (const worktree of plannedSetup.worktrees) setStatusWorktreeReference(requiredStatusStep(statusPayload, groupStartFlatIndex + worktree.index), worktree);
3811
- const pendingHandoff = writePendingParallelHandoff({
3960
+ onProgress: (progress) => {
3961
+ publishSetupUnknown(progress);
3962
+ for (const worktree of progress.setup.worktrees) setStatusWorktreeReference(requiredStatusStep(statusPayload, groupStartFlatIndex + worktree.index), worktree);
3963
+ const pendingHandoff = writeWorktreeSetupHandoff({
3812
3964
  manifestPath: parallelHandoffPath(asyncDir),
3813
3965
  runId: id,
3814
3966
  mode: (config.resultMode ?? statusPayload.mode) === "parallel" ? "parallel" : "chain",
@@ -3816,16 +3968,25 @@ async function runSubagent(
3816
3968
  cwd,
3817
3969
  stepIndex,
3818
3970
  flatStartIndex: groupStartFlatIndex,
3819
- setup: plannedSetup,
3971
+ progress,
3820
3972
  laneBindings: handoffWorkflowKey || config.lane ? [{ index: groupStartFlatIndex, taskIndex: 0, ...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}), ...(handoffChildRunId ? { runId: handoffChildRunId } : {}), ...(config.lane ? { lane: config.lane } : {}) }] : undefined,
3821
3973
  });
3974
+ if (!pendingHandoff) return;
3822
3975
  statusPayload.parallelHandoff = pendingHandoff;
3823
3976
  statusPayload.lastUpdate = Date.now();
3824
3977
  writeStatusPayload();
3825
3978
  },
3826
3979
  }));
3980
+ if (config.deadlineAt !== undefined && Date.now() >= config.deadlineAt) timeoutRunner();
3981
+ if (setupSignal.aborted) throw new Error(stopped ? stopMessage : timedOut ? timeoutMessage ?? "Subagent timed out." : "Subagent paused during worktree setup.");
3827
3982
  } catch (error) {
3983
+ if (worktreeSetup) await cleanupRemainingWorktree(worktreeSetup, stepIndex, groupStartFlatIndex);
3984
+ if (config.deadlineAt !== undefined && Date.now() >= config.deadlineAt) timeoutRunner();
3828
3985
  const setupError = error instanceof Error ? error.message : String(error);
3986
+ if (error instanceof WorktreeSetupError) publishSetupUnknown(error.snapshot);
3987
+ for (let index = groupStartFlatIndex; index < groupStartFlatIndex + group.parallel.length; index++) {
3988
+ if (childStopRequests.has(index)) markChildStopped(index);
3989
+ }
3829
3990
  const failedAt = Date.now();
3830
3991
  markParallelGroupSetupFailure({
3831
3992
  statusPayload,
@@ -3946,10 +4107,12 @@ async function runSubagent(
3946
4107
  nestedRoute: config.nestedRoute,
3947
4108
  capabilityCeiling: config.capabilityCeiling,
3948
4109
  runFanoutBudget: config.runFanoutBudget,
4110
+ hostAvailableBuiltins: config.hostAvailableBuiltins,
3949
4111
  registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
3950
4112
  registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
3951
4113
  registerStop: (stop) => registerStepStop(fi, stop),
3952
4114
  registerSteer: (steer) => registerStepSteer(fi, steer),
4115
+ onSteerOutcome: (request, delivery) => applySteerDelivery(request.id, fi, delivery),
3953
4116
  timeoutSignal: timeoutAbortController.signal,
3954
4117
  stopSignal: stopAbortController.signal,
3955
4118
  trackedMutationEvidenceForCompletionGuard: Boolean(worktreeSetup),
@@ -3961,7 +4124,8 @@ async function runSubagent(
3961
4124
  onExternalProcess: (process) => updateExternalProcess(fi, process),
3962
4125
  onExternalJob: (externalJob) => updateExternalJob(fi, externalJob),
3963
4126
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(fi),
3964
- usageBudgetExhausted: () => refreshUsageBudget()?.exhausted === true,
4127
+ usageBudgetExhausted: continuationUsageBudgetExhausted,
4128
+ usageBudget: config.usageBudget,
3965
4129
  orcaProgressTab,
3966
4130
  }), config.deadlineAt);
3967
4131
  if (task.sessionFile) {
@@ -4139,47 +4303,50 @@ async function runSubagent(
4139
4303
  })),
4140
4304
  );
4141
4305
  if (worktreeSetup) {
4142
- const captured = captureParallelWorktreeDiffs(worktreeSetup, asyncDir, stepIndex, group);
4143
- if (captured.summary) previousOutput = `${previousOutput}\n\n${captured.summary}`;
4306
+ const setup = worktreeSetup;
4144
4307
  worktreeFinalized = true;
4145
- const manifestPath = parallelHandoffPath(asyncDir);
4146
- const handoff = {
4147
- manifestPath,
4148
- runId: id,
4149
- mode: (config.resultMode ?? statusPayload.mode) === "parallel" ? "parallel" as const : "chain" as const,
4150
- source: "async" as const,
4151
- cwd,
4152
- stepIndex,
4153
- flatStartIndex: groupStartFlatIndex,
4154
- setup: worktreeSetup,
4155
- diffs: captured.diffs,
4156
- results: parallelResults.map((result) => ({
4157
- agent: result.agent,
4158
- ...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}),
4159
- ...(handoffChildRunId ? { runId: handoffChildRunId } : {}),
4160
- ...(config.lane ? { lane: config.lane } : {}),
4161
- status: result.stopped || (result.exitCode !== 0 && isUnexplainedProcessSignal(omitUndefinedProperties({
4162
- processSignal: result.processSignal,
4163
- interrupted: result.interrupted,
4164
- timedOut: result.timedOut,
4165
- stopped: result.stopped,
4166
- }))) ? "stopped" as const : result.interrupted ? "paused" as const : result.exitCode === 0 ? "completed" as const : "failed" as const,
4167
- summary: result.output || result.error || "(no output)",
4168
- ...(result.artifactPaths?.outputPath ? { outputPath: result.artifactPaths.outputPath } : {}),
4169
- ...(result.structuredOutput !== undefined ? { structuredOutput: result.structuredOutput } : {}),
4170
- ...(result.structuredOutputPath ? { structuredOutputPath: result.structuredOutputPath } : {}),
4171
- ...(result.sessionFile ? { sessionPath: result.sessionFile } : {}),
4172
- })),
4173
- };
4174
- try {
4175
- writeParallelHandoffGroup(handoff);
4176
- const cleanup = cleanupWorktrees(worktreeSetup, { kind: "preserve", capturedDiffs: captured.diffs, handoffManifestPath: manifestPath });
4177
- statusPayload.parallelHandoff = writeParallelHandoffGroup({ ...handoff, cleanup });
4178
- previousOutput = `${previousOutput}\n\n${formatParallelHandoffReference(statusPayload.parallelHandoff)}`;
4179
- } catch (error) {
4180
- previousOutput = `${previousOutput}\n\n${formatParallelHandoffError(error)}`;
4181
- }
4182
- writeStatusPayload();
4308
+ await finalizeWorktree(setup, stepIndex, groupStartFlatIndex, () => {
4309
+ const captured = captureParallelWorktreeDiffs(setup, asyncDir, stepIndex, group);
4310
+ if (captured.summary) previousOutput = `${previousOutput}\n\n${captured.summary}`;
4311
+ const manifestPath = parallelHandoffPath(asyncDir);
4312
+ const handoff = {
4313
+ manifestPath,
4314
+ runId: id,
4315
+ mode: (config.resultMode ?? statusPayload.mode) === "parallel" ? "parallel" as const : "chain" as const,
4316
+ source: "async" as const,
4317
+ cwd,
4318
+ stepIndex,
4319
+ flatStartIndex: groupStartFlatIndex,
4320
+ setup,
4321
+ diffs: captured.diffs,
4322
+ results: parallelResults.map((result) => ({
4323
+ agent: result.agent,
4324
+ ...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}),
4325
+ ...(handoffChildRunId ? { runId: handoffChildRunId } : {}),
4326
+ ...(config.lane ? { lane: config.lane } : {}),
4327
+ status: result.stopped || (result.exitCode !== 0 && isUnexplainedProcessSignal(omitUndefinedProperties({
4328
+ processSignal: result.processSignal,
4329
+ interrupted: result.interrupted,
4330
+ timedOut: result.timedOut,
4331
+ stopped: result.stopped,
4332
+ }))) ? "stopped" as const : result.interrupted ? "paused" as const : result.exitCode === 0 ? "completed" as const : "failed" as const,
4333
+ summary: result.output || result.error || "(no output)",
4334
+ ...(result.artifactPaths?.outputPath ? { outputPath: result.artifactPaths.outputPath } : {}),
4335
+ ...(result.structuredOutput !== undefined ? { structuredOutput: result.structuredOutput } : {}),
4336
+ ...(result.structuredOutputPath ? { structuredOutputPath: result.structuredOutputPath } : {}),
4337
+ ...(result.sessionFile ? { sessionPath: result.sessionFile } : {}),
4338
+ })),
4339
+ };
4340
+ try {
4341
+ writeParallelHandoffGroup(handoff);
4342
+ const cleanup = cleanupWorktrees(setup, { kind: "preserve", capturedDiffs: captured.diffs, handoffManifestPath: manifestPath });
4343
+ statusPayload.parallelHandoff = writeParallelHandoffGroup({ ...handoff, cleanup });
4344
+ previousOutput = `${previousOutput}\n\n${formatParallelHandoffReference(statusPayload.parallelHandoff)}`;
4345
+ } catch (error) {
4346
+ previousOutput = `${previousOutput}\n\n${formatParallelHandoffError(error)}`;
4347
+ }
4348
+ writeStatusPayload();
4349
+ });
4183
4350
  }
4184
4351
 
4185
4352
  appendJsonl(eventsPath, JSON.stringify({
@@ -4188,12 +4355,12 @@ async function runSubagent(
4188
4355
  runId: id,
4189
4356
  stepIndex,
4190
4357
  success: parallelResults.every((r) => r.exitCode === 0 || r.exitCode === -1)
4191
- && parallelResults.every((result, index) => !(isAgentContractV1(group.parallel[index]?.agentContract) && group.parallel[index]?.gateOn === "acceptance" && result.acceptance?.status === "rejected")),
4358
+ && parallelResults.every((result, index) => !(isAgentContract(group.parallel[index]?.agentContract) && group.parallel[index]?.gateOn === "acceptance" && result.acceptance?.status === "rejected")),
4192
4359
  }));
4193
4360
 
4194
4361
  const acceptanceGateFailure = parallelResults
4195
4362
  .map((result, index) => ({ result, index, task: group.parallel[index] }))
4196
- .find(({ result, task }) => isAgentContractV1(task?.agentContract) && task?.gateOn === "acceptance" && result.acceptance?.status === "rejected");
4363
+ .find(({ result, task }) => isAgentContract(task?.agentContract) && task?.gateOn === "acceptance" && result.acceptance?.status === "rejected");
4197
4364
  if (acceptanceGateFailure) {
4198
4365
  statusPayload.error = (acceptanceGateFailure.result.acceptance ? acceptanceFailureMessage(acceptanceGateFailure.result.acceptance) : undefined) ?? "Parallel acceptance gate rejected the step.";
4199
4366
  writeStatusPayload();
@@ -4203,7 +4370,7 @@ async function runSubagent(
4203
4370
  break;
4204
4371
  }
4205
4372
  } finally {
4206
- if (worktreeSetup && !worktreeFinalized) cleanupWorktrees(worktreeSetup);
4373
+ if (worktreeSetup && !worktreeFinalized) await cleanupRemainingWorktree(worktreeSetup, stepIndex, groupStartFlatIndex);
4207
4374
  }
4208
4375
  } else {
4209
4376
  const seqStep = step as SubagentStep;
@@ -4230,7 +4397,9 @@ async function runSubagent(
4230
4397
  let singleWorktreeSetup: WorktreeSetup | undefined;
4231
4398
  if (seqStep.worktree) {
4232
4399
  try {
4233
- singleWorktreeSetup = createWorktrees(cwd, `${id}-s${stepIndex}`, 1, omitUndefinedProperties({
4400
+ singleWorktreeSetup = await createWorktrees(cwd, `${id}-s${stepIndex}`, 1, omitUndefinedProperties({
4401
+ signal: setupSignal,
4402
+ deadlineAt: config.deadlineAt,
4234
4403
  agents: [seqStep.agent],
4235
4404
  labels: [seqStep.lane?.key ?? config.workflowKey ?? seqStep.outputName ?? seqStep.label],
4236
4405
  tasks: [seqStep.task],
@@ -4241,12 +4410,13 @@ async function runSubagent(
4241
4410
  ? omitUndefinedProperties({ hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs })
4242
4411
  : undefined,
4243
4412
  baseDir: config.worktreeBaseDir,
4244
- beforeCreate: (plannedSetup) => {
4245
- const worktree = plannedSetup.worktrees[0];
4413
+ onProgress: (progress) => {
4414
+ publishSetupUnknown(progress);
4415
+ const worktree = progress.setup.worktrees[0];
4246
4416
  if (worktree) {
4247
4417
  setStatusWorktreeReference(requiredStatusStep(statusPayload, flatIndex), worktree);
4248
4418
  }
4249
- const pendingHandoff = writePendingParallelHandoff({
4419
+ const pendingHandoff = writeWorktreeSetupHandoff({
4250
4420
  manifestPath: parallelHandoffPath(asyncDir),
4251
4421
  runId: id,
4252
4422
  mode: "single",
@@ -4254,19 +4424,47 @@ async function runSubagent(
4254
4424
  cwd,
4255
4425
  stepIndex,
4256
4426
  flatStartIndex: flatIndex,
4257
- setup: plannedSetup,
4427
+ progress,
4258
4428
  laneBindings: handoffWorkflowKey || config.lane ? [{ index: flatIndex, taskIndex: 0, ...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}), ...(handoffChildRunId ? { runId: handoffChildRunId } : {}), ...(config.lane ? { lane: config.lane } : {}) }] : undefined,
4259
4429
  });
4430
+ if (!pendingHandoff) return;
4260
4431
  statusPayload.parallelHandoff = pendingHandoff;
4261
4432
  statusPayload.lastUpdate = Date.now();
4262
4433
  writeStatusPayload();
4263
4434
  },
4264
4435
  }));
4265
4436
  } catch (error) {
4266
- if (singleWorktreeSetup) cleanupWorktrees(singleWorktreeSetup);
4267
- throw error;
4437
+ if (error instanceof WorktreeSetupError) publishSetupUnknown(error.snapshot);
4438
+ if (config.deadlineAt !== undefined && Date.now() >= config.deadlineAt) timeoutRunner();
4439
+ const message = error instanceof Error ? error.message : String(error);
4440
+ if (childStopRequests.has(flatIndex)) markChildStopped(flatIndex);
4441
+ const statusStep = requiredStatusStep(statusPayload, flatIndex);
4442
+ statusStep.status = stopped || childStopRequests.has(flatIndex) ? "stopped" : interrupted ? "paused" : "failed";
4443
+ statusStep.error ??= message;
4444
+ statusStep.exitCode = interrupted ? 0 : 1;
4445
+ statusStep.endedAt = Date.now();
4446
+ results.push(stopped ? stoppedStepResult(seqStep.agent, seqStep.context, seqStep.sessionName)
4447
+ : childStopRequests.has(flatIndex) ? childStopResult(flatIndex, seqStep.agent, seqStep.context)
4448
+ : timedOut ? timedOutStepResult(seqStep.agent, seqStep.context, seqStep.sessionName)
4449
+ : interrupted ? pausedStepResult(seqStep.agent, seqStep.context, seqStep.sessionName)
4450
+ : { agent: seqStep.agent, output: message, error: message, exitCode: 1, success: false });
4451
+ statusPayload.error ??= message;
4452
+ writeStatusPayload();
4453
+ flatIndex++;
4454
+ break;
4268
4455
  }
4269
4456
  }
4457
+ if (singleWorktreeSetup && config.deadlineAt !== undefined && Date.now() >= config.deadlineAt) timeoutRunner();
4458
+ if (singleWorktreeSetup && (timedOut || stopped || interrupted || childStopRequests.has(flatIndex))) {
4459
+ await cleanupRemainingWorktree(singleWorktreeSetup, stepIndex, flatIndex);
4460
+ results.push(stopped ? stoppedStepResult(seqStep.agent, seqStep.context, seqStep.sessionName)
4461
+ : childStopRequests.has(flatIndex) ? childStopResult(flatIndex, seqStep.agent, seqStep.context)
4462
+ : timedOut ? timedOutStepResult(seqStep.agent, seqStep.context, seqStep.sessionName)
4463
+ : pausedStepResult(seqStep.agent, seqStep.context, seqStep.sessionName));
4464
+ if (interrupted) requiredStatusStep(statusPayload, flatIndex).status = "paused";
4465
+ flatIndex++;
4466
+ continue;
4467
+ }
4270
4468
  const singleCwd = singleWorktreeSetup?.worktrees[0]?.agentCwd ?? cwd;
4271
4469
  const stepStartTime = Date.now();
4272
4470
  statusPayload.currentStep = flatIndex;
@@ -4311,10 +4509,12 @@ async function runSubagent(
4311
4509
  nestedRoute: config.nestedRoute,
4312
4510
  capabilityCeiling: config.capabilityCeiling,
4313
4511
  runFanoutBudget: config.runFanoutBudget,
4512
+ hostAvailableBuiltins: config.hostAvailableBuiltins,
4314
4513
  registerInterrupt: (interrupt) => registerStepInterrupt(flatIndex, interrupt),
4315
4514
  registerTimeout: (interrupt) => registerStepTimeout(flatIndex, interrupt),
4316
4515
  registerStop: (stop) => registerStepStop(flatIndex, stop),
4317
4516
  registerSteer: (steer) => registerStepSteer(flatIndex, steer),
4517
+ onSteerOutcome: (request, delivery) => applySteerDelivery(request.id, flatIndex, delivery),
4318
4518
  timeoutSignal: timeoutAbortController.signal,
4319
4519
  stopSignal: stopAbortController.signal,
4320
4520
  timeoutMessage,
@@ -4325,11 +4525,12 @@ async function runSubagent(
4325
4525
  onExternalProcess: (process) => updateExternalProcess(flatIndex, process),
4326
4526
  onExternalJob: (externalJob) => updateExternalJob(flatIndex, externalJob),
4327
4527
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(flatIndex),
4328
- usageBudgetExhausted: () => refreshUsageBudget()?.exhausted === true,
4528
+ usageBudgetExhausted: continuationUsageBudgetExhausted,
4529
+ usageBudget: config.usageBudget,
4329
4530
  orcaProgressTab,
4330
4531
  }), config.deadlineAt);
4331
4532
  } catch (error) {
4332
- if (singleWorktreeSetup) cleanupWorktrees(singleWorktreeSetup);
4533
+ if (singleWorktreeSetup) await cleanupRemainingWorktree(singleWorktreeSetup, stepIndex, flatIndex);
4333
4534
  throw error;
4334
4535
  }
4335
4536
  if (seqStep.sessionFile) {
@@ -4361,6 +4562,7 @@ async function runSubagent(
4361
4562
  totalCost: singleResult.totalCost,
4362
4563
  usage: singleResult.usage,
4363
4564
  artifactPaths: singleResult.artifactPaths,
4565
+ savedOutputPath: singleResult.savedOutputPath,
4364
4566
  transcriptPath: singleResult.transcriptPath,
4365
4567
  transcriptError: singleResult.transcriptError,
4366
4568
  effects: singleResult.effects,
@@ -4481,48 +4683,51 @@ async function runSubagent(
4481
4683
  }));
4482
4684
  if (stopped || childStopped) appendTerminalChildStatusEvent(flatIndex, stepEndTime);
4483
4685
  if (singleWorktreeSetup && !singleResult.detached) {
4484
- const diffs = diffWorktrees(singleWorktreeSetup, [seqStep.agent], path.join(asyncDir, "worktree-diffs", `step-${stepIndex}`));
4485
- const diffSummary = formatWorktreeDiffSummary(diffs);
4486
- const manifestPath = parallelHandoffPath(asyncDir);
4487
- const handoff = {
4488
- manifestPath,
4489
- runId: id,
4490
- mode: "single" as const,
4491
- source: "async" as const,
4492
- cwd,
4493
- stepIndex,
4494
- flatStartIndex: flatIndex,
4495
- setup: singleWorktreeSetup,
4496
- diffs,
4497
- results: [{
4498
- agent: singleResult.agent,
4499
- ...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}),
4500
- ...(handoffChildRunId ? { runId: handoffChildRunId } : {}),
4501
- ...(config.lane ? { lane: config.lane } : {}),
4502
- status: singleResult.stopped ? "stopped" as const : singleResult.interrupted ? "paused" as const : singleResult.exitCode === 0 ? "completed" as const : "failed" as const,
4503
- summary: singleResult.output || singleResult.error || "(no output)",
4504
- ...(singleResult.artifactPaths?.outputPath ? { outputPath: singleResult.artifactPaths.outputPath } : {}),
4505
- ...(singleResult.structuredOutput !== undefined ? { structuredOutput: singleResult.structuredOutput } : {}),
4506
- ...(singleResult.structuredOutputPath ? { structuredOutputPath: singleResult.structuredOutputPath } : {}),
4507
- ...(singleResult.sessionFile ? { sessionPath: singleResult.sessionFile } : {}),
4508
- }],
4509
- };
4510
- try {
4511
- writeParallelHandoffGroup(handoff);
4512
- const cleanup = cleanupWorktrees(singleWorktreeSetup, {
4513
- kind: "preserve",
4514
- capturedDiffs: diffs,
4515
- handoffManifestPath: manifestPath,
4516
- ...(config.parentWorkflowRunId && singleResult.sessionFile && fs.existsSync(singleResult.sessionFile) && !singleResult.stopped
4517
- ? { cleanupBlocker: "retained child resume requires managed worktree cwd" }
4518
- : {}),
4519
- });
4520
- statusPayload.parallelHandoff = writeParallelHandoffGroup({ ...handoff, cleanup });
4521
- previousOutput = [previousOutput, diffSummary, formatParallelHandoffReference(statusPayload.parallelHandoff)].filter(Boolean).join("\n\n");
4522
- } catch (error) {
4523
- previousOutput = [previousOutput, diffSummary, formatParallelHandoffError(error)].filter(Boolean).join("\n\n");
4524
- }
4525
- writeStatusPayload();
4686
+ const setup = singleWorktreeSetup;
4687
+ await finalizeWorktree(setup, stepIndex, flatIndex, () => {
4688
+ const diffs = diffWorktrees(setup, [seqStep.agent], path.join(asyncDir, "worktree-diffs", `step-${stepIndex}`));
4689
+ const diffSummary = formatWorktreeDiffSummary(diffs);
4690
+ const manifestPath = parallelHandoffPath(asyncDir);
4691
+ const handoff = {
4692
+ manifestPath,
4693
+ runId: id,
4694
+ mode: "single" as const,
4695
+ source: "async" as const,
4696
+ cwd,
4697
+ stepIndex,
4698
+ flatStartIndex: flatIndex,
4699
+ setup,
4700
+ diffs,
4701
+ results: [{
4702
+ agent: singleResult.agent,
4703
+ ...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}),
4704
+ ...(handoffChildRunId ? { runId: handoffChildRunId } : {}),
4705
+ ...(config.lane ? { lane: config.lane } : {}),
4706
+ status: singleResult.stopped ? "stopped" as const : singleResult.interrupted ? "paused" as const : singleResult.exitCode === 0 ? "completed" as const : "failed" as const,
4707
+ summary: singleResult.output || singleResult.error || "(no output)",
4708
+ ...(singleResult.artifactPaths?.outputPath ? { outputPath: singleResult.artifactPaths.outputPath } : {}),
4709
+ ...(singleResult.structuredOutput !== undefined ? { structuredOutput: singleResult.structuredOutput } : {}),
4710
+ ...(singleResult.structuredOutputPath ? { structuredOutputPath: singleResult.structuredOutputPath } : {}),
4711
+ ...(singleResult.sessionFile ? { sessionPath: singleResult.sessionFile } : {}),
4712
+ }],
4713
+ };
4714
+ try {
4715
+ writeParallelHandoffGroup(handoff);
4716
+ const cleanup = cleanupWorktrees(setup, {
4717
+ kind: "preserve",
4718
+ capturedDiffs: diffs,
4719
+ handoffManifestPath: manifestPath,
4720
+ ...(config.parentWorkflowRunId && singleResult.sessionFile && fs.existsSync(singleResult.sessionFile) && !singleResult.stopped
4721
+ ? { cleanupBlocker: "retained child resume requires managed worktree cwd" }
4722
+ : {}),
4723
+ });
4724
+ statusPayload.parallelHandoff = writeParallelHandoffGroup({ ...handoff, cleanup });
4725
+ previousOutput = [previousOutput, diffSummary, formatParallelHandoffReference(statusPayload.parallelHandoff)].filter(Boolean).join("\n\n");
4726
+ } catch (error) {
4727
+ previousOutput = [previousOutput, diffSummary, formatParallelHandoffError(error)].filter(Boolean).join("\n\n");
4728
+ }
4729
+ writeStatusPayload();
4730
+ });
4526
4731
  }
4527
4732
 
4528
4733
  if (singleResult.completionGuardTriggered) {
@@ -4540,7 +4745,7 @@ async function runSubagent(
4540
4745
  }
4541
4746
 
4542
4747
  flatIndex++;
4543
- if (isAgentContractV1(seqStep.agentContract) && seqStep.gateOn === "acceptance" && singleResult.acceptance?.status === "rejected") {
4748
+ if (isAgentContract(seqStep.agentContract) && seqStep.gateOn === "acceptance" && singleResult.acceptance?.status === "rejected") {
4544
4749
  statusPayload.error = acceptanceFailureMessage(singleResult.acceptance) ?? "Chain acceptance gate rejected the step.";
4545
4750
  writeStatusPayload();
4546
4751
  break;
@@ -4626,6 +4831,11 @@ async function runSubagent(
4626
4831
  stopped: result.stopped,
4627
4832
  })));
4628
4833
  const partialWithEvidence = !stopped && !signalTerminated && !timedOut && !usageBudgetExceeded && !interrupted && results.some(partialEvidenceResult) && !results.some(concreteFailureResult);
4834
+ // Flush while still nonterminal; deferred status retries retain that state snapshot.
4835
+ statusWriteCoalescer.flush(statusPath);
4836
+ const publication = new Promise<void>((resolve, reject) => {
4837
+ finalResultPublication = { resolve, reject };
4838
+ });
4629
4839
  statusPayload.state = stopped || signalTerminated ? "stopped" : timedOut || usageBudgetExceeded ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : partialWithEvidence ? "partial" : "failed";
4630
4840
  closeSteerInbox(asyncDir, statusPayload.state, (filePath, payload) => runPersistence.write(filePath, payload));
4631
4841
  disposeControlInbox();
@@ -4637,7 +4847,7 @@ async function runSubagent(
4637
4847
  for (const target of request.targets) {
4638
4848
  if (target.state !== "scheduled" && target.state !== "routed" && target.state !== "queued") continue;
4639
4849
  changed = true;
4640
- const reason = target.state === "queued" ? "run ended before queued follow-up delivery" : "child terminated before steering delivery";
4850
+ const reason = target.state === "queued" ? unconsumedSteerReason() : "child terminated before steering delivery";
4641
4851
  updateSteeringLifecycleTarget(request.id, target.index, "failed", Date.now(), { reason });
4642
4852
  markSteeringAttention(target.index);
4643
4853
  emitSteeringEvent("subagent.steer.failed", { type: "steer", id: request.id, ts: request.requestedAt, message: reason }, target.index, { reason });
@@ -4684,6 +4894,9 @@ async function runSubagent(
4684
4894
  statusPayload.error = `Step failed: ${failedStep.agent}`;
4685
4895
  }
4686
4896
  }
4897
+ let childSessionDisposal: Promise<void> | undefined;
4898
+ const disposeChildSessions = (): Promise<void> => childSessionDisposal ??= childSessions.dispose()
4899
+ .catch((error: unknown) => console.error("Failed to dispose runner child sessions:", error));
4687
4900
  try {
4688
4901
  runPersistence.write(resultPath, {
4689
4902
  lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
@@ -4724,6 +4937,7 @@ async function runSubagent(
4724
4937
  totalCost: r.totalCost,
4725
4938
  usage: r.usage,
4726
4939
  artifactPaths: r.artifactPaths,
4940
+ savedOutputPath: r.savedOutputPath,
4727
4941
  outputSaveError: r.outputSaveError,
4728
4942
  metadataSaveError: r.metadataSaveError,
4729
4943
  truncated: r.truncated,
@@ -4778,13 +4992,16 @@ async function runSubagent(
4778
4992
  ...(taskIndex !== undefined && { taskIndex }),
4779
4993
  ...(totalTasks !== undefined && { totalTasks }),
4780
4994
  }, (filePath, payload) => { writeAsyncResultFile(filePath, payload as Record<string, unknown>); });
4781
- finalResultCommitted = true;
4995
+ // Only capacity deferral releases settled sessions before terminal publication.
4996
+ if (!finalResultCommitted) await Promise.all([publication, disposeChildSessions()]);
4782
4997
  } catch (err) {
4783
4998
  const message = `Failed to write result file ${resultPath}: ${err instanceof Error ? err.message : String(err)}`;
4784
4999
  console.error(message, err);
4785
5000
  statusPayload.state = "failed";
4786
5001
  statusPayload.error = message;
4787
5002
  statusPayload.lastUpdate = Date.now();
5003
+ } finally {
5004
+ finalResultPublication = undefined;
4788
5005
  }
4789
5006
  writeStatusPayload();
4790
5007
  await orcaProgressTab?.finish(statusPayload.state === "complete" ? "completed" : statusPayload.state === "stopped" ? "stopped" : "failed", effectiveSessionFile);
@@ -4822,9 +5039,8 @@ async function runSubagent(
4822
5039
  }), (filePath, content) => runPersistence.write(filePath, { content }, (_path, payload) => {
4823
5040
  fs.writeFileSync(_path, (payload as { content: string }).content, "utf-8");
4824
5041
  }));
4825
- // The run is committed: release the child sessions first, then wait for the
4826
- // storage-capacity retries that may still hold the final status or result.
4827
- await childSessions.dispose().catch((error: unknown) => console.error("Failed to dispose runner child sessions:", error));
5042
+ // Preserve normal-success disposal ordering, then drain remaining persistence retries.
5043
+ await disposeChildSessions();
4828
5044
  while (runPersistence.pendingCount() + indexPersistence.pendingCount() > 0) {
4829
5045
  await new Promise((resolve) => setTimeout(resolve, 200));
4830
5046
  }
@@ -4832,7 +5048,7 @@ async function runSubagent(
4832
5048
  indexPersistence.dispose();
4833
5049
  if (config.runnerProcessInstanceId) {
4834
5050
  // Children run inside this process, so no step has writer processes to prove terminal.
4835
- const writers: Record<string, PiWriterProcessInstanceExitV1[]> = {};
5051
+ const writers: Record<string, PiWriterProcessInstanceExit[]> = {};
4836
5052
  const expectedWriters: Record<string, number> = {};
4837
5053
  for (const index of results.keys()) {
4838
5054
  writers[String(index)] = [];
@@ -4879,7 +5095,8 @@ async function waitForStartupControl(
4879
5095
  throw new Error(`Timed out after ${timeoutMs}ms waiting for runner startup control '${action}'.`);
4880
5096
  }
4881
5097
 
4882
- async function runConfiguredSubagent(config: SubagentRunConfig): Promise<void> {
5098
+ // Both hosts enter here: startup authorization, revival leases and disposal stay shared.
5099
+ export async function runConfiguredSubagent(config: SubagentRunConfig, options?: DefaultChildSessionFactoryOptions): Promise<void> {
4883
5100
  let lease: ReturnType<typeof acquireSessionLease> | undefined;
4884
5101
  let startupCommitted = config.revivalLease === undefined && config.launchBarrierToken === undefined;
4885
5102
  const startupPath = path.join(config.asyncDir, "runner-startup.json");
@@ -4918,7 +5135,7 @@ async function runConfiguredSubagent(config: SubagentRunConfig): Promise<void> {
4918
5135
  }
4919
5136
  }
4920
5137
  }
4921
- const childSessions = await loadRunnerChildSessionFactory(config);
5138
+ const childSessions = await loadRunnerChildSessionFactory(config, options);
4922
5139
  try {
4923
5140
  await runSubagent(config, childSessions);
4924
5141
  } finally {
@@ -4968,6 +5185,7 @@ function startConfiguredSubagent(config: SubagentRunConfig): void {
4968
5185
  );
4969
5186
  }
4970
5187
 
5188
+ if (isRunnerEntrypoint) {
4971
5189
  const configArg = process.argv[2];
4972
5190
  if (configArg) {
4973
5191
  try {
@@ -4999,3 +5217,4 @@ if (configArg) {
4999
5217
  }
5000
5218
  });
5001
5219
  }
5220
+ }