pi-subagents 0.65.1 → 0.66.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 (85) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +1 -1
  3. package/agents/researcher.md +23 -13
  4. package/docs/agents.md +16 -2
  5. package/docs/configuration.md +18 -0
  6. package/docs/extension-api.md +91 -0
  7. package/docs/models.md +58 -1
  8. package/docs/observability.md +42 -2
  9. package/docs/tool-reference.md +3 -3
  10. package/docs/workflows.md +14 -7
  11. package/package.json +2 -1
  12. package/skills/pi-subagents/references/execution-controls.md +14 -1
  13. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
  14. package/src/agents/advertised-agent-prompt.ts +63 -0
  15. package/src/agents/agent-management.ts +14 -1
  16. package/src/agents/agent-serializer.ts +2 -0
  17. package/src/agents/agents.ts +8 -0
  18. package/src/api/shared-types.ts +1 -1
  19. package/src/api/workflow-resources.ts +6 -0
  20. package/src/extension/index.ts +40 -2
  21. package/src/extension/public-execution.ts +0 -1
  22. package/src/extension/rpc.ts +4 -21
  23. package/src/extension/schemas.ts +8 -6
  24. package/src/extension/tool-description.ts +6 -5
  25. package/src/intercom/native-supervisor-channel.ts +82 -54
  26. package/src/runs/background/active-async-capacity.ts +18 -18
  27. package/src/runs/background/async-job-tracker.ts +35 -3
  28. package/src/runs/background/async-status-snapshot.ts +10 -12
  29. package/src/runs/background/async-status.ts +17 -9
  30. package/src/runs/background/auto-drain.ts +40 -29
  31. package/src/runs/background/chain-root-attachment.ts +8 -0
  32. package/src/runs/background/control-channel.ts +78 -44
  33. package/src/runs/background/notify.ts +86 -12
  34. package/src/runs/background/owned-process-tree.ts +6 -6
  35. package/src/runs/background/process-terminal.ts +23 -23
  36. package/src/runs/background/run-child-session.ts +60 -32
  37. package/src/runs/background/run-status.ts +75 -5
  38. package/src/runs/background/runner-aliases.ts +18 -7
  39. package/src/runs/background/runner-child-launch.ts +86 -0
  40. package/src/runs/background/stale-run-reconciler.ts +3 -1
  41. package/src/runs/background/subagent-runner.ts +412 -209
  42. package/src/runs/background/subagent-wait.ts +3 -0
  43. package/src/runs/background/wait-completions.ts +4 -0
  44. package/src/runs/foreground/async-steering-action.ts +19 -0
  45. package/src/runs/foreground/execution.ts +101 -25
  46. package/src/runs/foreground/subagent-executor.ts +474 -197
  47. package/src/runs/foreground/workflow-detach-reconcile.ts +8 -5
  48. package/src/runs/foreground/workflow-foreground-steering.ts +56 -2
  49. package/src/runs/shared/acceptance.ts +2 -2
  50. package/src/runs/shared/agent-contract.ts +1 -1
  51. package/src/runs/shared/async-status-projection.ts +47 -47
  52. package/src/runs/shared/child-hooks.ts +151 -2
  53. package/src/runs/shared/child-launch.ts +18 -13
  54. package/src/runs/shared/child-session.ts +42 -6
  55. package/src/runs/shared/child-tool-plan.ts +2 -2
  56. package/src/runs/shared/completion-evidence.ts +2 -2
  57. package/src/runs/shared/completion-guard.ts +1 -0
  58. package/src/runs/shared/host-step-status.ts +11 -11
  59. package/src/runs/shared/llm-intent-arbiter.ts +10 -9
  60. package/src/runs/shared/model-fallback.ts +10 -6
  61. package/src/runs/shared/nested-events.ts +5 -5
  62. package/src/runs/shared/orca-progress-tabs.ts +6 -0
  63. package/src/runs/shared/parallel-handoff.ts +57 -12
  64. package/src/runs/shared/parallel-utils.ts +2 -2
  65. package/src/runs/shared/readonly-drain-observation.ts +42 -0
  66. package/src/runs/shared/readonly-model-continuation.ts +69 -0
  67. package/src/runs/shared/readonly-session-evidence.ts +307 -0
  68. package/src/runs/shared/run-fanout-budget.ts +8 -8
  69. package/src/runs/shared/runtime-acknowledged-extensions.ts +3 -3
  70. package/src/runs/shared/subagent-prompt-runtime.ts +13 -3
  71. package/src/runs/shared/worktree-setup-command.ts +190 -0
  72. package/src/runs/shared/worktree.ts +332 -204
  73. package/src/shared/types.ts +81 -60
  74. package/src/shared/utils.ts +7 -2
  75. package/src/shared/workflow-child-permit.ts +18 -13
  76. package/src/tui/fleet.ts +11 -5
  77. package/src/tui/render.ts +23 -5
  78. package/src/workflows/chat-progress.ts +3 -3
  79. package/src/workflows/scripted-workflow.ts +38 -10
  80. package/src/workflows/workflow-checklist.ts +11 -15
  81. package/src/workflows/workflow-child-summary.ts +57 -8
  82. package/src/workflows/workflow-preflight.ts +19 -19
  83. package/src/workflows/workflow-receipt.ts +3 -3
  84. package/src/workflows/workflow-resources.ts +96 -21
  85. package/src/workflows/workflow-settlement.ts +3 -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 { 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";
95
99
  import type { ChildSessionFactory } from "../shared/child-session.ts";
96
- import { runChildSession, type ChildEvent, type RunChildSessionResult, type StepSteerHandler } from "./run-child-session.ts";
100
+ import { getSettledReadonlyChild, runChildSession, type ChildEvent, type RunChildSessionInput, type RunChildSessionResult, 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,7 +113,7 @@ 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";
116
+ import { markProcessTerminalCandidateLeaseRelease, processTerminalPath, writeProcessTerminalCandidate, type ProcessTerminalCandidate } from "./process-terminal.ts";
111
117
  import { createSteeringStatus, recordSteeringRequest, steeringStatus, terminalSteeringNoticeState, 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";
@@ -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
150
  import { launchBindingDigest } 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";
@@ -225,8 +233,8 @@ interface SubagentRunConfig {
225
233
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
226
234
  runFanoutBudget?: RunFanoutBudgetDescriptor;
227
235
  launchContractDigest?: string;
228
- launchResolvedExtensions?: LaunchResolvedChildExtensionsV1;
229
- runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
236
+ launchResolvedExtensions?: LaunchResolvedChildExtensions;
237
+ runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions;
230
238
  runnerProcessInstanceId?: string;
231
239
  launchBarrierToken?: string;
232
240
  parentWorkflowRunId?: string;
@@ -241,8 +249,8 @@ interface StepResult {
241
249
  context?: "fresh" | "fork";
242
250
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
243
251
  capabilityAudit?: import("../shared/capability-ceiling.ts").SubagentCapabilityAudit;
244
- launchResolvedExtensions?: LaunchResolvedChildExtensionsV1;
245
- runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
252
+ launchResolvedExtensions?: LaunchResolvedChildExtensions;
253
+ runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions;
246
254
  output: string;
247
255
  outputState?: SubagentOutputState;
248
256
  error?: string;
@@ -692,14 +700,17 @@ interface SingleStepContext {
692
700
  onExternalProcess?: (process: ExternalProcessStatus) => void;
693
701
  onExternalJob?: (status: ExternalJobStatus) => void;
694
702
  skipAcceptance?: () => boolean;
695
- usageBudgetExhausted?: () => boolean;
703
+ /** Authoritative owner decision after event delivery; undefined includes incomplete run-wide usage. */
704
+ usageBudgetExhausted?: () => boolean | undefined;
705
+ /** Existing run-owned budget configuration; cost allowance is not settled by the live token ledger. */
706
+ usageBudget?: UsageBudgetConfig;
696
707
  /** False when sibling work in the same Git worktree could have caused the tracked diff. */
697
708
  trackedMutationEvidenceForCompletionGuard?: boolean;
698
709
  orcaProgressTab?: OrcaProgressTab;
699
710
  }
700
711
 
701
712
  /** Run a single pi agent step, returning output and metadata */
702
- async function runSingleStepInner(
713
+ export async function runSingleStepInner(
703
714
  step: SubagentStep,
704
715
  ctx: SingleStepContext,
705
716
  ): Promise<StepResult & { completionGuardTriggered?: boolean }> {
@@ -827,8 +838,8 @@ async function runSingleStepInner(
827
838
  // Derive from the pre-acceptance task so internal acceptance/recovery
828
839
  // instructions never leak into the display name.
829
840
  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) });
841
+ if (step.effectiveAcceptance && (step.runner?.type === "external-cli" || step.runner?.type === "external-job")) {
842
+ const acceptancePrompt = formatAcceptancePrompt(step.effectiveAcceptance, { reportOptional: isAgentContract(step.agentContract), structuredOutput: Boolean(step.structuredOutput?.acceptanceReportPath) });
832
843
  if (acceptancePrompt) task = `${task}\n${acceptancePrompt}`;
833
844
  }
834
845
  const sessionEnabled = Boolean(step.sessionFile) || ctx.sessionEnabled;
@@ -1035,11 +1046,31 @@ async function runSingleStepInner(
1035
1046
  let modelIndex = 0;
1036
1047
  let contextOverflow = false;
1037
1048
  let launchWarningsEmitted = false;
1038
- let abortRecoveryAttempted = false;
1049
+ let recoveryState: LogicalRecoveryState = "unused";
1050
+ let readonlyContinuation: RunChildSessionInput["readonlyContinuation"];
1051
+ const continuationBudget = () => {
1052
+ if (step.toolBudget) return "tool-budget-configured" as const;
1053
+ // Refresh the authoritative run ledger, already fed synchronously by onChildEvent.
1054
+ const exhausted = ctx.usageBudgetExhausted?.();
1055
+ if (exhausted === true) return "exhausted" as const;
1056
+ if (!ctx.usageBudget) return "unconfigured" as const;
1057
+ if (ctx.usageBudget.costUsd || !ctx.onChildEvent || exhausted !== false) return "unknown" as const;
1058
+ return "available" as const;
1059
+ };
1060
+ const lifecycleAllowsContinuation = () => !ctx.timeoutSignal?.aborted && !ctx.stopSignal?.aborted
1061
+ && !ctx.skipAcceptance?.() && (ctx.deadlineAt === undefined || Date.now() < ctx.deadlineAt);
1062
+ const canContinue = () => lifecycleAllowsContinuation() && ["available", "unconfigured"].includes(continuationBudget());
1063
+ const failContinuationLaunch = (candidate: string | undefined, error: unknown) => {
1064
+ const message = error instanceof Error ? error.message : String(error);
1065
+ modelAttempts.push({ model: candidate ?? "default", success: false, exitCode: 1, error: message });
1066
+ if (candidate) attemptedModels.push(candidate);
1067
+ if (finalResult) finalResult = { ...finalResult, exitCode: 1, error: message };
1068
+ };
1039
1069
  let nextAttemptTask = task;
1040
1070
  modelAttemptsLoop: while (modelIndex < candidates.length) {
1041
1071
  if (ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break;
1042
- const recoveringAbort = abortRecoveryAttempted;
1072
+ if (readonlyContinuation && !canContinue()) break;
1073
+ const recoveringAbort = recoveryState === "abort-recovery";
1043
1074
  const attemptTask = nextAttemptTask;
1044
1075
  const candidate = candidates[modelIndex];
1045
1076
  const expectedModelForVerification = candidate && !(step.skipPrimaryModelVerification && modelIndex === 0) ? candidate : undefined;
@@ -1047,6 +1078,10 @@ async function runSingleStepInner(
1047
1078
  assertThinkingWithinCeiling({ model: candidate, configThinking: step.thinking, ceiling: step.thinkingCeiling, agent: step.agent, runId: ctx.id });
1048
1079
  } catch (error) {
1049
1080
  const message = error instanceof Error ? error.message : String(error);
1081
+ if (readonlyContinuation) {
1082
+ failContinuationLaunch(candidate, error);
1083
+ break modelAttemptsLoop;
1084
+ }
1050
1085
  return omitUndefinedProperties({ agent: step.agent, output: message, error: message, exitCode: 1, context: step.context, thinkingCeiling: step.thinkingCeiling });
1051
1086
  }
1052
1087
  ctx.onAttemptStart?.(omitUndefinedProperties({
@@ -1072,56 +1107,22 @@ async function runSingleStepInner(
1072
1107
  })
1073
1108
  : undefined;
1074
1109
  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
- }));
1110
+ let launch: ReturnType<typeof buildRunnerChildLaunch>;
1111
+ try {
1112
+ launch = buildRunnerChildLaunch(step, ctx, {
1113
+ sessionEnabled,
1114
+ sessionDir,
1115
+ model: candidate,
1116
+ sessionName: childSessionName,
1117
+ structuredOutput: effectiveStructuredOutput,
1118
+ childWatchdog,
1119
+ watchdogStatus: (event) => watchdogSink?.(event),
1120
+ });
1121
+ } catch (error) {
1122
+ if (!readonlyContinuation) throw error;
1123
+ failContinuationLaunch(candidate, error);
1124
+ break modelAttemptsLoop;
1125
+ }
1125
1126
  if (effectiveStructuredOutput && launch.config.structuredOutput) {
1126
1127
  // The runner reads the value back from the runtime's files after the run.
1127
1128
  launch.config.structuredOutput.capture = createStructuredOutputFileCapture(effectiveStructuredOutput);
@@ -1181,6 +1182,9 @@ async function runSingleStepInner(
1181
1182
  const run = await runChildSession(omitUndefinedProperties({
1182
1183
  factory: ctx.childSessions,
1183
1184
  launch,
1185
+ collectReadonlyEvidence: true,
1186
+ readonlyContinuation,
1187
+ canContinue,
1184
1188
  prompt: `Task: ${attemptTask}`,
1185
1189
  childWatchdog,
1186
1190
  childEventContext: { runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
@@ -1261,7 +1265,7 @@ async function runSingleStepInner(
1261
1265
  && (!hiddenError?.hasError || hasEmptyTerminalAssistantResponse(run.messages))
1262
1266
  ? formatEmptyTerminalAssistantResponseError(run.messages)
1263
1267
  : undefined;
1264
- const completionGuardEnabled = isAgentContractV1(step.agentContract) ? step.completionGuard === true : step.completionGuard !== false;
1268
+ const completionGuardEnabled = isAgentContract(step.agentContract) ? step.completionGuard === true : step.completionGuard !== false;
1265
1269
  const completionToolPlan = resolvedTaskToolPlan;
1266
1270
  const completionTools = completionToolPlan ? (completionToolPlan.explicitToolAllowlist ? completionToolPlan.effectiveToolAllowlist : undefined) : step.tools;
1267
1271
  const mutationEvidence = collectTrackedMutationEvidence(mutationSnapshot, step.cwd ?? ctx.cwd);
@@ -1280,14 +1284,27 @@ async function runSingleStepInner(
1280
1284
  }))
1281
1285
  : undefined;
1282
1286
  const mutationAttemptObserved = run.observedMutationAttempt === true || completionMutationEvidence?.attemptedMutation === true;
1287
+ let arbitration = { triggered: completionGuard?.triggered === true && !mutationAttemptObserved, rescued: false };
1288
+ if (arbitration.triggered) {
1289
+ const modelContext = launch.capture.completionIntentContext?.();
1290
+ arbitration = await arbitrateCompletionGuardRescue({
1291
+ guardTriggered: true,
1292
+ task: taskForCompletionGuard,
1293
+ // Construct lazily too: the shared gate refuses overlength tasks before
1294
+ // registry/auth/model work. The child has already shut down normally.
1295
+ arbiter: modelContext ? async (task) => createTaskMutationArbiter(modelContext)?.(task) ?? "unavailable" : undefined,
1296
+ });
1297
+ }
1283
1298
  const completionEvidence = planCompletionEvidence({
1284
1299
  guard: completionGuard,
1300
+ guardTriggered: arbitration.triggered,
1301
+ arbiterRescued: arbitration.rescued,
1285
1302
  completionGuardEnabled,
1286
1303
  mutationCapable: hasMutationToolCapability(completionTools, completionToolPlan?.effectiveMcpTools ?? step.mcpDirectTools),
1287
1304
  implementationMutationExpected: expectsImplementationMutation(step.agent, taskForCompletionGuard),
1288
1305
  mutationAttemptObserved,
1289
1306
  mutationEvidence: completionMutationEvidence,
1290
- agentContractV1: isAgentContractV1(step.agentContract),
1307
+ agentContractEnabled: isAgentContract(step.agentContract),
1291
1308
  });
1292
1309
  const finalOutputHasPersistableFileContent = run.exitCode === 0 && !run.error && !emptyOutputError && Boolean(stripAcceptanceReport(run.finalOutput).trim());
1293
1310
  const requiredOutput = step.outputMode === "file-only" && step.outputPath
@@ -1347,7 +1364,7 @@ async function runSingleStepInner(
1347
1364
  messages: run.messages,
1348
1365
  error,
1349
1366
  sessionAvailable: Boolean(step.sessionFile && fs.existsSync(step.sessionFile)),
1350
- alreadyResumed: abortRecoveryAttempted,
1367
+ alreadyResumed: recoveryState !== "unused",
1351
1368
  stopped: run.stopped || ctx.stopSignal?.aborted || ctx.skipAcceptance?.(),
1352
1369
  interrupted: run.interrupted,
1353
1370
  timedOut: run.timedOut || ctx.timeoutSignal?.aborted,
@@ -1367,15 +1384,59 @@ async function runSingleStepInner(
1367
1384
  if (run.stopped || run.timedOut || ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break modelAttemptsLoop;
1368
1385
  if (abortRecovery?.action === "settle" && abortRecovery.diagnostic) break modelAttemptsLoop;
1369
1386
  if (attempt.success) break modelAttemptsLoop;
1387
+ // The shared token is consumed before sibling creation; no third dispatch of any kind.
1388
+ if (recoveryState === "readonly-continuation") break modelAttemptsLoop;
1370
1389
  if (recoveringAbort) break modelAttemptsLoop;
1371
1390
  if (abortRecovery?.action === "resume") {
1372
- abortRecoveryAttempted = true;
1391
+ recoveryState = "abort-recovery";
1373
1392
  nextAttemptTask = abortRecovery.prompt;
1374
1393
  attemptNotes.push("[abort-recovery] provider/transport abort after useful progress; resuming the retained child session once.");
1375
1394
  continue;
1376
1395
  }
1377
1396
  if (completionEvidence.guardTriggered) break modelAttemptsLoop;
1378
1397
 
1398
+ const source = getSettledReadonlyChild(run);
1399
+ if (source) {
1400
+ const captured = launch.capture.completionIntentContext?.();
1401
+ const sourceModel = captured?.model;
1402
+ const retained = getReadonlySessionEvidence(source)!;
1403
+ // Modality/capacity assessment, not another history/provenance validator.
1404
+ // Images and unknown content have no certified token bound in this host slice.
1405
+ const textOnly = (JSON.parse(retained.contextJson) as Message[]).every((message) => typeof message.content === "string"
1406
+ || (Array.isArray(message.content) && message.content.every((block) => ["text", "thinking", "toolCall"].includes(block.type))));
1407
+ const retainedBytes = Buffer.byteLength(retained.contextJson, "utf8");
1408
+ const resolvedCandidates = candidates.map((reference, index) => {
1409
+ // Only exact registry identities qualify; aliases/default guesses remain ineligible.
1410
+ const base = reference ? splitKnownThinkingSuffix(reference).baseModel : "";
1411
+ const slash = base.indexOf("/");
1412
+ const model = slash > 0 ? captured?.modelRegistry.find(base.slice(0, slash), base.slice(slash + 1)) : undefined;
1413
+ // Reserve the source window for runtime/system/tool overhead, plus a conservative
1414
+ // byte bound for the complete retained text and new prompt; never assume a fit
1415
+ // merely because a 429 request was sent. Equal/smaller windows remain denied.
1416
+ const compatible = textOnly && model && sourceModel && model.input?.includes("text")
1417
+ && Number.isFinite(sourceModel.contextWindow) && sourceModel.contextWindow > 0
1418
+ && model.contextWindow >= sourceModel.contextWindow + retainedBytes + Buffer.byteLength(READONLY_CONTINUATION_PROMPT, "utf8")
1419
+ && model.maxTokens > 0 && model.maxTokens <= sourceModel.maxTokens;
1420
+ return { resolved: model ? { provider: model.provider, model: model.id, api: model.api } : undefined,
1421
+ tried: index <= modelIndex, compatibility: compatible ? "compatible" as const : "unknown" as const };
1422
+ });
1423
+ const plan = planReadonlyModelContinuation({ source, recoveryState, candidates: resolvedCandidates, currentIndex: modelIndex,
1424
+ lifecycleAllowsContinuation: lifecycleAllowsContinuation() && !run.interrupted && !run.stopped && !run.timedOut,
1425
+ effectsAllowContinuation: !run.currentTool && !run.observedMutationAttempt && !mutationEvidence.attemptedMutation
1426
+ && !structuredError && !effectiveStructuredOutput && !missingRequiredOutputError && !toolAvailabilityError
1427
+ && !completionEvidence.guardTriggered && !midToolExitError && !hiddenError?.hasError,
1428
+ budget: continuationBudget(), knownContextOverflow: isContextOverflow(error) });
1429
+ if (plan.kind === "continue" && canContinue()) {
1430
+ recoveryState = plan.recoveryState;
1431
+ const selected = resolvedCandidates[plan.candidateIndex]!.resolved!;
1432
+ readonlyContinuation = { source, expected: plan.expected, modelId: `${selected.provider}/${selected.model}` };
1433
+ nextAttemptTask = plan.prompt;
1434
+ modelIndex = plan.candidateIndex;
1435
+ attemptNotes.push(`[readonly-continuation] ${attempt.model} returned HTTP 429 after read-only progress; continuing the retained session once with ${candidates[modelIndex]}.`);
1436
+ continue;
1437
+ }
1438
+ }
1439
+
1379
1440
  const retryableModelFailure = isRetryableModelFailureAttempt({ error, messages: run.messages, toolCount: run.toolCount });
1380
1441
  if (retryableModelFailure) recordRetryableModelFailure(candidate ?? run.model ?? step.model, error);
1381
1442
  if (isContextOverflow(error)) {
@@ -1457,7 +1518,7 @@ async function runSingleStepInner(
1457
1518
  cwd: step.cwd ?? ctx.cwd,
1458
1519
  signal: combinedAbortSignal([ctx.timeoutSignal, ctx.stopSignal]),
1459
1520
  abortMessage: ctx.stopSignal?.aborted ? ctx.stopMessage ?? "Subagent stopped by user." : ctx.timeoutMessage ?? "Subagent timed out.",
1460
- reportOptional: isAgentContractV1(step.agentContract),
1521
+ reportOptional: isAgentContract(step.agentContract),
1461
1522
  artifactsDir: ctx.artifactsDir,
1462
1523
  runId: ctx.id,
1463
1524
  watchdog: finalResult?.watchdog,
@@ -1473,7 +1534,7 @@ async function runSingleStepInner(
1473
1534
  : acceptance
1474
1535
  : undefined;
1475
1536
  const acceptanceFailure = effectiveAcceptance ? acceptanceFailureMessage(effectiveAcceptance) : undefined;
1476
- const acceptanceCanFailRun = acceptanceFailure && effectiveAcceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted && !timedOutAfterAcceptance && !stoppedAfterAcceptance && !isAgentContractV1(step.agentContract);
1537
+ const acceptanceCanFailRun = acceptanceFailure && effectiveAcceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted && !timedOutAfterAcceptance && !stoppedAfterAcceptance && !isAgentContract(step.agentContract);
1477
1538
  const effectiveFinalExitCode = timedOutAfterAcceptance || stoppedAfterAcceptance ? 1 : acceptanceCanFailRun ? 1 : finalResult?.exitCode ?? 1;
1478
1539
  const intercomDetachReceipt = finalResult?.finalOutput === INTERCOM_DETACH_RECEIPT;
1479
1540
  const baseFinalError = stoppedAfterAcceptance
@@ -1515,7 +1576,7 @@ async function runSingleStepInner(
1515
1576
  ...(capabilityAudit ? { capabilityCeiling: capabilityAudit.ceiling, capabilityAudit } : {}),
1516
1577
  launchContractDigest: actualLaunchContractDigest,
1517
1578
  launchResolvedExtensions,
1518
- ...((finalResult as (RunChildSessionResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }) | undefined)?.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: (finalResult as RunChildSessionResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }).runtimeAcknowledgedExtensions } : {}),
1579
+ ...((finalResult as (RunChildSessionResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions }) | undefined)?.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: (finalResult as RunChildSessionResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions }).runtimeAcknowledgedExtensions } : {}),
1519
1580
  ...(transcriptWriter ? { transcriptPath: artifactPaths.transcriptPath } : {}),
1520
1581
  transcriptError: transcriptWriter?.getError(),
1521
1582
  skills: step.skills,
@@ -1544,6 +1605,7 @@ async function runSingleStepInner(
1544
1605
  totalCost: costSummaryFromAttempts(modelAttempts),
1545
1606
  usage,
1546
1607
  artifactPaths,
1608
+ savedOutputPath: finalizedOutput.savedPath,
1547
1609
  outputSaveError: [resolvedOutput.saveError, artifactErrors.outputSaveError].filter(Boolean).join("\n") || undefined,
1548
1610
  metadataSaveError: artifactErrors.metadataSaveError,
1549
1611
  transcriptPath: transcriptWriter ? artifactPaths?.transcriptPath : undefined,
@@ -1563,9 +1625,9 @@ async function runSingleStepInner(
1563
1625
  watchdog: finalResult?.watchdog,
1564
1626
  ...(capabilityAudit ? { capabilityCeiling: capabilityAudit.ceiling, capabilityAudit } : {}),
1565
1627
  launchResolvedExtensions,
1566
- ...((finalResult as (RunChildSessionResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }) | undefined)?.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: (finalResult as RunChildSessionResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }).runtimeAcknowledgedExtensions } : {}),
1628
+ ...((finalResult as (RunChildSessionResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions }) | undefined)?.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: (finalResult as RunChildSessionResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions }).runtimeAcknowledgedExtensions } : {}),
1567
1629
  });
1568
- return isAgentContractV1(step.agentContract) ? attachContractProjections(result as unknown as import("../../shared/types.ts").SingleResult) as unknown as typeof result : result;
1630
+ return isAgentContract(step.agentContract) ? attachContractProjections(result as unknown as import("../../shared/types.ts").SingleResult) as unknown as typeof result : result;
1569
1631
  }
1570
1632
 
1571
1633
  async function runSingleStep(
@@ -1656,12 +1718,15 @@ function markParallelGroupSetupFailure(input: {
1656
1718
  const statusStep = requiredStatusStep(input.statusPayload, flatTaskIndex);
1657
1719
  const task = input.group.parallel[taskIndex];
1658
1720
  if (!task) throw new Error(`Missing parallel task at index ${taskIndex}`);
1659
- statusStep.status = "failed";
1721
+ const stopped = statusStep.stopped || statusStep.stopRequested || input.statusPayload.stopped;
1722
+ const paused = !stopped && input.statusPayload.state === "paused";
1723
+ statusStep.status = stopped ? "stopped" : paused ? "paused" : "failed";
1660
1724
  statusStep.startedAt = input.failedAt;
1661
1725
  statusStep.endedAt = input.failedAt;
1662
1726
  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 }));
1727
+ statusStep.exitCode = paused ? 0 : 1;
1728
+ statusStep.error ??= input.setupError;
1729
+ 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
1730
  }
1666
1731
  input.statusPayload.currentStep = input.groupStartFlatIndex;
1667
1732
  input.statusPayload.lastUpdate = input.failedAt;
@@ -1818,7 +1883,10 @@ async function runSingleStepWithTimeout(
1818
1883
  ctx: SingleStepContext,
1819
1884
  parentDeadlineAt?: number,
1820
1885
  ): Promise<SingleStepResult> {
1821
- if (step.timeoutMs === undefined) return runSingleStep(step, ctx);
1886
+ if (step.timeoutMs === undefined) return runSingleStep(step, parentDeadlineAt === undefined ? ctx : {
1887
+ ...ctx,
1888
+ deadlineAt: ctx.deadlineAt === undefined ? parentDeadlineAt : Math.min(ctx.deadlineAt, parentDeadlineAt),
1889
+ });
1822
1890
 
1823
1891
  const parentRemainingMs = parentDeadlineAt === undefined ? undefined : Math.max(0, parentDeadlineAt - Date.now());
1824
1892
  const timeoutMs = parentRemainingMs === undefined ? step.timeoutMs : Math.min(step.timeoutMs, parentRemainingMs);
@@ -1855,7 +1923,7 @@ async function runSingleStepWithTimeout(
1855
1923
  }
1856
1924
  }
1857
1925
 
1858
- async function runSubagent(
1926
+ export async function runSubagent(
1859
1927
  config: SubagentRunConfig,
1860
1928
  childSessions: ChildSessionFactory,
1861
1929
  ): Promise<void> {
@@ -1893,6 +1961,8 @@ async function runSubagent(
1893
1961
  const stopMessage = "Subagent stopped by user.";
1894
1962
  const timeoutAbortController = new AbortController();
1895
1963
  const stopAbortController = new AbortController();
1964
+ const setupInterruptController = new AbortController();
1965
+ const setupSignal = AbortSignal.any([timeoutAbortController.signal, stopAbortController.signal, setupInterruptController.signal]);
1896
1966
  let previousCumulativeTokens: TokenUsage = { input: 0, output: 0, total: 0 };
1897
1967
  let latestSessionFile: string | undefined;
1898
1968
 
@@ -2059,20 +2129,29 @@ async function runSubagent(
2059
2129
  },
2060
2130
  onError: (error, filePath) => console.error(`Failed to update async run index '${filePath}':`, error),
2061
2131
  });
2062
- const queueActiveRunIndex = (): void => {
2063
- const state = statusPayload.state;
2132
+ const queueActiveRunIndex = (status: AsyncStatus): void => {
2133
+ const state = status.state;
2064
2134
  if (state === lastIndexedStatusState && indexPersistence.pendingCount() === 0) return;
2065
- indexPersistence.write(asyncDir, { state, toolCallId: statusPayload.toolCallId }, (_filePath, payload) => {
2135
+ indexPersistence.write(asyncDir, { state, toolCallId: status.toolCallId }, (_filePath, payload) => {
2066
2136
  const indexPayload = payload as { state: AsyncStatus["state"]; toolCallId?: string };
2067
2137
  updateActiveRunIndex(asyncDir, indexPayload.state, indexPayload.toolCallId, { retryCapacityErrors: true });
2068
2138
  });
2069
2139
  };
2140
+ let finalResultCommitted = false;
2141
+ let finalResultPublication: { resolve(): void; reject(error: unknown): void } | undefined;
2070
2142
  const runPersistence = createCapacityResilientJsonWriter({
2071
2143
  keepAlive: true,
2072
- onSuccess: (filePath) => {
2073
- if (filePath === statusPath) queueActiveRunIndex();
2144
+ onSuccess: (filePath, payload) => {
2145
+ if (filePath === statusPath) queueActiveRunIndex(payload as AsyncStatus);
2146
+ if (filePath === resultPath && finalResultPublication) {
2147
+ finalResultCommitted = true;
2148
+ finalResultPublication.resolve();
2149
+ }
2150
+ },
2151
+ onError: (error, filePath) => {
2152
+ console.error(`Failed to persist async run state '${filePath}':`, error);
2153
+ if (filePath === resultPath) finalResultPublication?.reject(error);
2074
2154
  },
2075
- onError: (error, filePath) => console.error(`Failed to persist async run state '${filePath}':`, error),
2076
2155
  });
2077
2156
  try {
2078
2157
  fs.mkdirSync(asyncDir, { recursive: true });
@@ -2080,7 +2159,7 @@ async function runSubagent(
2080
2159
  if (!isStorageCapacityError(error)) throw error;
2081
2160
  console.error(`Failed to prepare async run storage '${asyncDir}' while storage is full:`, error);
2082
2161
  }
2083
- runPersistence.write(statusPath, statusPayload);
2162
+ runPersistence.write(statusPath, { ...statusPayload });
2084
2163
 
2085
2164
  let pendingParallelUsageCost: CostSummary = { inputTokens: 0, outputTokens: 0, costUsd: 0 };
2086
2165
  const currentUsageTotals = (): CostSummary => {
@@ -2099,6 +2178,13 @@ async function runSubagent(
2099
2178
  setOptionalProperty(statusPayload, "usageBudget", usageBudgetState(config.usageBudget, currentUsageTotals()));
2100
2179
  return statusPayload.usageBudget;
2101
2180
  };
2181
+ // Continuation admission only: the existing ledger has no in-flight cost or
2182
+ // external/import usage coverage. Never change ordinary budget enforcement.
2183
+ let continuationUsageUncertain = config.steps.some(isDynamicRunnerGroup) || flatSteps.some((step) => Boolean(step.runner || step.importAsyncRoot));
2184
+ const continuationUsageBudgetExhausted = (): boolean | undefined => {
2185
+ const exhausted = refreshUsageBudget()?.exhausted === true;
2186
+ return exhausted ? true : config.usageBudget && continuationUsageUncertain ? undefined : false;
2187
+ };
2102
2188
  const emitNestedSelfEvent = (type: "subagent.nested.updated" | "subagent.nested.completed"): void => {
2103
2189
  if (!config.nestedRoute || !config.nestedSelf) return;
2104
2190
  try {
@@ -2153,7 +2239,6 @@ async function runSubagent(
2153
2239
  for (const node of graph.nodes) updateNode(node);
2154
2240
  statusPayload.workflowGraph = graph;
2155
2241
  };
2156
- let finalResultCommitted = false;
2157
2242
  const statusResultState = (): AsyncStatus["state"] | undefined => {
2158
2243
  if (statusPayload.state === "running" || statusPayload.state === "queued") return undefined;
2159
2244
  return statusPayload.state;
@@ -2215,14 +2300,15 @@ async function runSubagent(
2215
2300
  }), (filePath, payload) => writePendingAsyncResultFile(filePath, payload as Record<string, unknown>));
2216
2301
  };
2217
2302
  const writeStatusPayloadNow = (): void => {
2303
+ if (finalResultPublication) return;
2218
2304
  refreshWorkflowGraph();
2219
2305
  writeRecoverableStatusResult();
2220
- runPersistence.write(statusPath, statusPayload);
2306
+ runPersistence.write(statusPath, { ...statusPayload });
2221
2307
  emitNestedSelfEvent(statusPayload.state === "running" || statusPayload.state === "queued" ? "subagent.nested.updated" : "subagent.nested.completed");
2222
2308
  };
2223
2309
  const statusWriteCoalescer = createFileCoalescer(writeStatusPayloadNow, 100);
2224
2310
  const writeStatusPayload = (immediate = true): void => {
2225
- if (immediate || statusPayload.state !== "running" || statusPayload.activityState !== undefined) {
2311
+ if (immediate || statusPayload.state !== "running") {
2226
2312
  if (!statusWriteCoalescer.flush(statusPath)) writeStatusPayloadNow();
2227
2313
  return;
2228
2314
  }
@@ -2878,6 +2964,7 @@ async function runSubagent(
2878
2964
  const updateStepFromChildEvent = (flatIndex: number, event: ChildEvent): void => {
2879
2965
  const step = statusPayload.steps[flatIndex];
2880
2966
  if (!step) return;
2967
+ const previousActivityState = step.activityState;
2881
2968
  const now = Date.now();
2882
2969
  statusPayload.currentStep = flatIndex;
2883
2970
  if (isChildWatchdogStatusEvent(event)) {
@@ -3012,6 +3099,10 @@ async function runSubagent(
3012
3099
  appendRecentStepOutput(step, stripAcceptanceReport(extractTextFromContent(event.message.content)).split("\n").slice(-10));
3013
3100
  step.turnCount = (step.turnCount ?? 0) + 1;
3014
3101
  const usage = event.message.usage;
3102
+ if (config.usageBudget) {
3103
+ const known = (value: unknown) => typeof value === "number" && Number.isFinite(value) && value >= 0;
3104
+ if (!known(usage?.input ?? usage?.inputTokens) || !known(usage?.output ?? usage?.outputTokens)) continuationUsageUncertain = true;
3105
+ }
3015
3106
  if (usage) {
3016
3107
  const input = usage.input ?? usage.inputTokens ?? 0;
3017
3108
  const output = usage.output ?? usage.outputTokens ?? 0;
@@ -3031,7 +3122,8 @@ async function runSubagent(
3031
3122
  statusPayload.lastActivityAt = now;
3032
3123
  statusPayload.lastUpdate = now;
3033
3124
  maybeEmitActiveLongRunning(flatIndex, now);
3034
- writeStatusPayload(false);
3125
+ // A sibling may keep aggregate attention unchanged; publish this step's transition.
3126
+ writeStatusPayload(step.activityState !== previousActivityState);
3035
3127
  };
3036
3128
  const updateRunnerActivityState = (now: number): boolean => {
3037
3129
  if (!controlConfig.enabled) return false;
@@ -3103,10 +3195,55 @@ async function runSubagent(
3103
3195
  activityTimer.unref?.();
3104
3196
  }
3105
3197
 
3198
+ const publishSetupUnknown = (progress: WorktreeSetupProgress): void => {
3199
+ if (!progress.unknown) return;
3200
+ const proof = {
3201
+ version: 1 as const, state: "unknown" as const, runId: id,
3202
+ runnerProcessInstanceId: config.runnerProcessInstanceId ?? "unknown",
3203
+ reason: "process-tree-unverified" as const,
3204
+ diagnostic: `Worktree setup settlement unknown: ${progress.unknown}; manual reconciliation required; ${parallelHandoffPath(asyncDir)}`,
3205
+ };
3206
+ statusPayload.processTerminal = proof;
3207
+ // Publish the sticky proof independently of in-process child writer counts.
3208
+ writeAtomicJson(processTerminalPath(asyncDir), proof);
3209
+ };
3210
+ const finalizeWorktree = async (setup: WorktreeSetup, stepIndex: number, flatStartIndex: number, action: () => void, deadlineAt?: number): Promise<void> => {
3211
+ let admitted = false;
3212
+ try { await withWorktreeTransaction(() => {
3213
+ if (deadlineAt !== undefined && Date.now() >= deadlineAt) throw new Error("Run deadline expired before worktree cleanup");
3214
+ admitted = true;
3215
+ action();
3216
+ }); }
3217
+ catch (error) {
3218
+ if (admitted) throw error;
3219
+ const reason = `Worktree finalization retained; manual reconciliation required: ${error instanceof Error ? error.message : String(error)}`;
3220
+ statusPayload.parallelHandoff = writeParallelHandoffGroup({
3221
+ manifestPath: parallelHandoffPath(asyncDir), runId: id,
3222
+ mode: (config.resultMode ?? statusPayload.mode) === "parallel" ? "parallel" : (config.resultMode ?? statusPayload.mode) === "single" ? "single" : "chain",
3223
+ source: "async", cwd, stepIndex, flatStartIndex, setup, diffs: [], results: [],
3224
+ cleanup: { state: "partial", pruned: false, errors: [reason], tasks: setup.worktrees.map((worktree) => ({
3225
+ index: worktree.index, path: worktree.path, branch: worktree.branch, provider: worktree.provider, naming: worktree.naming,
3226
+ worktreeRemoved: false, branchRemoved: false, preserved: true, reason,
3227
+ })) },
3228
+ });
3229
+ previousOutput = [previousOutput, reason, formatParallelHandoffReference(statusPayload.parallelHandoff)].filter(Boolean).join("\n\n");
3230
+ writeStatusPayload();
3231
+ }
3232
+ };
3233
+ const cleanupRemainingWorktree = (setup: WorktreeSetup, stepIndex: number, flatStartIndex: number) => finalizeWorktree(setup, stepIndex, flatStartIndex, () => {
3234
+ const cleanup = cleanupWorktrees(setup);
3235
+ statusPayload.parallelHandoff = writeParallelHandoffGroup({
3236
+ manifestPath: parallelHandoffPath(asyncDir), runId: id,
3237
+ mode: (config.resultMode ?? statusPayload.mode) === "parallel" ? "parallel" : (config.resultMode ?? statusPayload.mode) === "single" ? "single" : "chain",
3238
+ source: "async", cwd, stepIndex, flatStartIndex, setup, diffs: [], results: [], cleanup,
3239
+ });
3240
+ writeStatusPayload();
3241
+ }, config.deadlineAt);
3106
3242
  const interruptRunner = () => {
3107
3243
  consumeInterruptRequest(asyncDir);
3108
3244
  if (interrupted || statusPayload.state !== "running") return;
3109
3245
  interrupted = true;
3246
+ setupInterruptController.abort();
3110
3247
  const now = Date.now();
3111
3248
  statusPayload.state = "paused";
3112
3249
  currentActivityState = undefined;
@@ -3340,14 +3477,14 @@ async function runSubagent(
3340
3477
  cwd,
3341
3478
  signal: combinedAbortSignal([timeoutAbortController.signal, stopAbortController.signal]),
3342
3479
  abortMessage: stopAbortController.signal.aborted ? stopMessage : timeoutMessage ?? "Subagent timed out.",
3343
- reportOptional: isAgentContractV1(step.agentContract),
3480
+ reportOptional: isAgentContract(step.agentContract),
3344
3481
  }))
3345
3482
  : undefined;
3346
3483
  const groupStopped = stopped || stopAbortController.signal.aborted;
3347
3484
  const groupTimedOut = !groupStopped && (timedOut || timeoutAbortController.signal.aborted);
3348
3485
  const effectiveGroupAcceptance = groupTimedOut || groupStopped ? undefined : groupAcceptance;
3349
3486
  if (placeholder && effectiveGroupAcceptance) placeholder.acceptance = effectiveGroupAcceptance;
3350
- const groupAcceptanceFailure = effectiveGroupAcceptance && (!isAgentContractV1(step.agentContract) || step.gateOn === "acceptance") ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
3487
+ const groupAcceptanceFailure = effectiveGroupAcceptance && (!isAgentContract(step.agentContract) || step.gateOn === "acceptance") ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
3351
3488
  if (groupTimedOut || groupStopped || groupAcceptanceFailure) {
3352
3489
  const errorMessage = groupStopped ? stopMessage : groupTimedOut ? timeoutMessage ?? "Subagent timed out." : groupAcceptanceFailure!;
3353
3490
  statusPayload.state = groupStopped ? "stopped" : "failed";
@@ -3564,7 +3701,8 @@ async function runSubagent(
3564
3701
  onExternalProcess: (process) => updateExternalProcess(fi, process),
3565
3702
  onExternalJob: (externalJob) => updateExternalJob(fi, externalJob),
3566
3703
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(fi),
3567
- usageBudgetExhausted: () => refreshUsageBudget()?.exhausted === true,
3704
+ usageBudgetExhausted: continuationUsageBudgetExhausted,
3705
+ usageBudget: config.usageBudget,
3568
3706
  orcaProgressTab,
3569
3707
  }), config.deadlineAt);
3570
3708
  const taskEndTime = Date.now();
@@ -3680,7 +3818,7 @@ async function runSubagent(
3680
3818
  const failures = parallelResults.filter((result) => result.exitCode !== 0 && result.exitCode !== -1);
3681
3819
  const acceptanceFailures = parallelResults
3682
3820
  .map((result, originalIndex) => ({ result, originalIndex, task: dynamicSteps[originalIndex] }))
3683
- .filter(({ result, task }) => isAgentContractV1(task?.agentContract ?? step.agentContract) && task?.gateOn === "acceptance" && result.acceptance?.status === "rejected");
3821
+ .filter(({ result, task }) => isAgentContract(task?.agentContract ?? step.agentContract) && task?.gateOn === "acceptance" && result.acceptance?.status === "rejected");
3684
3822
  if (acceptanceFailures.length > 0) {
3685
3823
  const message = acceptanceFailures
3686
3824
  .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 +3848,13 @@ async function runSubagent(
3710
3848
  cwd,
3711
3849
  signal: combinedAbortSignal([timeoutAbortController.signal, stopAbortController.signal]),
3712
3850
  abortMessage: stopAbortController.signal.aborted ? stopMessage : timeoutMessage ?? "Subagent timed out.",
3713
- reportOptional: isAgentContractV1(step.agentContract),
3851
+ reportOptional: isAgentContract(step.agentContract),
3714
3852
  }))
3715
3853
  : undefined;
3716
3854
  const groupStopped = stopped || stopAbortController.signal.aborted;
3717
3855
  const groupTimedOut = !groupStopped && (timedOut || timeoutAbortController.signal.aborted);
3718
3856
  const effectiveGroupAcceptance = groupTimedOut || groupStopped ? undefined : groupAcceptance;
3719
- const groupAcceptanceFailure = effectiveDynamicGroupAcceptance.explicit && effectiveGroupAcceptance && (!isAgentContractV1(step.agentContract) || step.gateOn === "acceptance") ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
3857
+ const groupAcceptanceFailure = effectiveDynamicGroupAcceptance.explicit && effectiveGroupAcceptance && (!isAgentContract(step.agentContract) || step.gateOn === "acceptance") ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
3720
3858
  const groupError = groupStopped ? stopMessage : groupTimedOut ? timeoutMessage ?? "Subagent timed out." : groupAcceptanceFailure;
3721
3859
  markDynamicGraphGroup(stepIndex, groupError ? groupStopped ? "stopped" : "failed" : "completed", groupError, effectiveGroupAcceptance);
3722
3860
  if (groupError) {
@@ -3795,7 +3933,9 @@ async function runSubagent(
3795
3933
  break;
3796
3934
  }
3797
3935
  try {
3798
- worktreeSetup = createWorktrees(cwd, `${id}-s${stepIndex}`, group.parallel.length, omitUndefinedProperties({
3936
+ worktreeSetup = await createWorktrees(cwd, `${id}-s${stepIndex}`, group.parallel.length, omitUndefinedProperties({
3937
+ signal: setupSignal,
3938
+ deadlineAt: config.deadlineAt,
3799
3939
  agents: group.parallel.map((task) => task.agent),
3800
3940
  labels: group.parallel.map((task) => task.lane?.key ?? config.workflowKey ?? task.outputName ?? task.label),
3801
3941
  tasks: group.parallel.map((task) => task.task),
@@ -3806,9 +3946,10 @@ async function runSubagent(
3806
3946
  ? omitUndefinedProperties({ hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs })
3807
3947
  : undefined,
3808
3948
  baseDir: config.worktreeBaseDir,
3809
- beforeCreate: (plannedSetup) => {
3810
- for (const worktree of plannedSetup.worktrees) setStatusWorktreeReference(requiredStatusStep(statusPayload, groupStartFlatIndex + worktree.index), worktree);
3811
- const pendingHandoff = writePendingParallelHandoff({
3949
+ onProgress: (progress) => {
3950
+ publishSetupUnknown(progress);
3951
+ for (const worktree of progress.setup.worktrees) setStatusWorktreeReference(requiredStatusStep(statusPayload, groupStartFlatIndex + worktree.index), worktree);
3952
+ const pendingHandoff = writeWorktreeSetupHandoff({
3812
3953
  manifestPath: parallelHandoffPath(asyncDir),
3813
3954
  runId: id,
3814
3955
  mode: (config.resultMode ?? statusPayload.mode) === "parallel" ? "parallel" : "chain",
@@ -3816,16 +3957,25 @@ async function runSubagent(
3816
3957
  cwd,
3817
3958
  stepIndex,
3818
3959
  flatStartIndex: groupStartFlatIndex,
3819
- setup: plannedSetup,
3960
+ progress,
3820
3961
  laneBindings: handoffWorkflowKey || config.lane ? [{ index: groupStartFlatIndex, taskIndex: 0, ...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}), ...(handoffChildRunId ? { runId: handoffChildRunId } : {}), ...(config.lane ? { lane: config.lane } : {}) }] : undefined,
3821
3962
  });
3963
+ if (!pendingHandoff) return;
3822
3964
  statusPayload.parallelHandoff = pendingHandoff;
3823
3965
  statusPayload.lastUpdate = Date.now();
3824
3966
  writeStatusPayload();
3825
3967
  },
3826
3968
  }));
3969
+ if (config.deadlineAt !== undefined && Date.now() >= config.deadlineAt) timeoutRunner();
3970
+ if (setupSignal.aborted) throw new Error(stopped ? stopMessage : timedOut ? timeoutMessage ?? "Subagent timed out." : "Subagent paused during worktree setup.");
3827
3971
  } catch (error) {
3972
+ if (worktreeSetup) await cleanupRemainingWorktree(worktreeSetup, stepIndex, groupStartFlatIndex);
3973
+ if (config.deadlineAt !== undefined && Date.now() >= config.deadlineAt) timeoutRunner();
3828
3974
  const setupError = error instanceof Error ? error.message : String(error);
3975
+ if (error instanceof WorktreeSetupError) publishSetupUnknown(error.snapshot);
3976
+ for (let index = groupStartFlatIndex; index < groupStartFlatIndex + group.parallel.length; index++) {
3977
+ if (childStopRequests.has(index)) markChildStopped(index);
3978
+ }
3829
3979
  const failedAt = Date.now();
3830
3980
  markParallelGroupSetupFailure({
3831
3981
  statusPayload,
@@ -3961,7 +4111,8 @@ async function runSubagent(
3961
4111
  onExternalProcess: (process) => updateExternalProcess(fi, process),
3962
4112
  onExternalJob: (externalJob) => updateExternalJob(fi, externalJob),
3963
4113
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(fi),
3964
- usageBudgetExhausted: () => refreshUsageBudget()?.exhausted === true,
4114
+ usageBudgetExhausted: continuationUsageBudgetExhausted,
4115
+ usageBudget: config.usageBudget,
3965
4116
  orcaProgressTab,
3966
4117
  }), config.deadlineAt);
3967
4118
  if (task.sessionFile) {
@@ -4139,47 +4290,50 @@ async function runSubagent(
4139
4290
  })),
4140
4291
  );
4141
4292
  if (worktreeSetup) {
4142
- const captured = captureParallelWorktreeDiffs(worktreeSetup, asyncDir, stepIndex, group);
4143
- if (captured.summary) previousOutput = `${previousOutput}\n\n${captured.summary}`;
4293
+ const setup = worktreeSetup;
4144
4294
  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();
4295
+ await finalizeWorktree(setup, stepIndex, groupStartFlatIndex, () => {
4296
+ const captured = captureParallelWorktreeDiffs(setup, asyncDir, stepIndex, group);
4297
+ if (captured.summary) previousOutput = `${previousOutput}\n\n${captured.summary}`;
4298
+ const manifestPath = parallelHandoffPath(asyncDir);
4299
+ const handoff = {
4300
+ manifestPath,
4301
+ runId: id,
4302
+ mode: (config.resultMode ?? statusPayload.mode) === "parallel" ? "parallel" as const : "chain" as const,
4303
+ source: "async" as const,
4304
+ cwd,
4305
+ stepIndex,
4306
+ flatStartIndex: groupStartFlatIndex,
4307
+ setup,
4308
+ diffs: captured.diffs,
4309
+ results: parallelResults.map((result) => ({
4310
+ agent: result.agent,
4311
+ ...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}),
4312
+ ...(handoffChildRunId ? { runId: handoffChildRunId } : {}),
4313
+ ...(config.lane ? { lane: config.lane } : {}),
4314
+ status: result.stopped || (result.exitCode !== 0 && isUnexplainedProcessSignal(omitUndefinedProperties({
4315
+ processSignal: result.processSignal,
4316
+ interrupted: result.interrupted,
4317
+ timedOut: result.timedOut,
4318
+ stopped: result.stopped,
4319
+ }))) ? "stopped" as const : result.interrupted ? "paused" as const : result.exitCode === 0 ? "completed" as const : "failed" as const,
4320
+ summary: result.output || result.error || "(no output)",
4321
+ ...(result.artifactPaths?.outputPath ? { outputPath: result.artifactPaths.outputPath } : {}),
4322
+ ...(result.structuredOutput !== undefined ? { structuredOutput: result.structuredOutput } : {}),
4323
+ ...(result.structuredOutputPath ? { structuredOutputPath: result.structuredOutputPath } : {}),
4324
+ ...(result.sessionFile ? { sessionPath: result.sessionFile } : {}),
4325
+ })),
4326
+ };
4327
+ try {
4328
+ writeParallelHandoffGroup(handoff);
4329
+ const cleanup = cleanupWorktrees(setup, { kind: "preserve", capturedDiffs: captured.diffs, handoffManifestPath: manifestPath });
4330
+ statusPayload.parallelHandoff = writeParallelHandoffGroup({ ...handoff, cleanup });
4331
+ previousOutput = `${previousOutput}\n\n${formatParallelHandoffReference(statusPayload.parallelHandoff)}`;
4332
+ } catch (error) {
4333
+ previousOutput = `${previousOutput}\n\n${formatParallelHandoffError(error)}`;
4334
+ }
4335
+ writeStatusPayload();
4336
+ });
4183
4337
  }
4184
4338
 
4185
4339
  appendJsonl(eventsPath, JSON.stringify({
@@ -4188,12 +4342,12 @@ async function runSubagent(
4188
4342
  runId: id,
4189
4343
  stepIndex,
4190
4344
  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")),
4345
+ && parallelResults.every((result, index) => !(isAgentContract(group.parallel[index]?.agentContract) && group.parallel[index]?.gateOn === "acceptance" && result.acceptance?.status === "rejected")),
4192
4346
  }));
4193
4347
 
4194
4348
  const acceptanceGateFailure = parallelResults
4195
4349
  .map((result, index) => ({ result, index, task: group.parallel[index] }))
4196
- .find(({ result, task }) => isAgentContractV1(task?.agentContract) && task?.gateOn === "acceptance" && result.acceptance?.status === "rejected");
4350
+ .find(({ result, task }) => isAgentContract(task?.agentContract) && task?.gateOn === "acceptance" && result.acceptance?.status === "rejected");
4197
4351
  if (acceptanceGateFailure) {
4198
4352
  statusPayload.error = (acceptanceGateFailure.result.acceptance ? acceptanceFailureMessage(acceptanceGateFailure.result.acceptance) : undefined) ?? "Parallel acceptance gate rejected the step.";
4199
4353
  writeStatusPayload();
@@ -4203,7 +4357,7 @@ async function runSubagent(
4203
4357
  break;
4204
4358
  }
4205
4359
  } finally {
4206
- if (worktreeSetup && !worktreeFinalized) cleanupWorktrees(worktreeSetup);
4360
+ if (worktreeSetup && !worktreeFinalized) await cleanupRemainingWorktree(worktreeSetup, stepIndex, groupStartFlatIndex);
4207
4361
  }
4208
4362
  } else {
4209
4363
  const seqStep = step as SubagentStep;
@@ -4230,7 +4384,9 @@ async function runSubagent(
4230
4384
  let singleWorktreeSetup: WorktreeSetup | undefined;
4231
4385
  if (seqStep.worktree) {
4232
4386
  try {
4233
- singleWorktreeSetup = createWorktrees(cwd, `${id}-s${stepIndex}`, 1, omitUndefinedProperties({
4387
+ singleWorktreeSetup = await createWorktrees(cwd, `${id}-s${stepIndex}`, 1, omitUndefinedProperties({
4388
+ signal: setupSignal,
4389
+ deadlineAt: config.deadlineAt,
4234
4390
  agents: [seqStep.agent],
4235
4391
  labels: [seqStep.lane?.key ?? config.workflowKey ?? seqStep.outputName ?? seqStep.label],
4236
4392
  tasks: [seqStep.task],
@@ -4241,12 +4397,13 @@ async function runSubagent(
4241
4397
  ? omitUndefinedProperties({ hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs })
4242
4398
  : undefined,
4243
4399
  baseDir: config.worktreeBaseDir,
4244
- beforeCreate: (plannedSetup) => {
4245
- const worktree = plannedSetup.worktrees[0];
4400
+ onProgress: (progress) => {
4401
+ publishSetupUnknown(progress);
4402
+ const worktree = progress.setup.worktrees[0];
4246
4403
  if (worktree) {
4247
4404
  setStatusWorktreeReference(requiredStatusStep(statusPayload, flatIndex), worktree);
4248
4405
  }
4249
- const pendingHandoff = writePendingParallelHandoff({
4406
+ const pendingHandoff = writeWorktreeSetupHandoff({
4250
4407
  manifestPath: parallelHandoffPath(asyncDir),
4251
4408
  runId: id,
4252
4409
  mode: "single",
@@ -4254,19 +4411,47 @@ async function runSubagent(
4254
4411
  cwd,
4255
4412
  stepIndex,
4256
4413
  flatStartIndex: flatIndex,
4257
- setup: plannedSetup,
4414
+ progress,
4258
4415
  laneBindings: handoffWorkflowKey || config.lane ? [{ index: flatIndex, taskIndex: 0, ...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}), ...(handoffChildRunId ? { runId: handoffChildRunId } : {}), ...(config.lane ? { lane: config.lane } : {}) }] : undefined,
4259
4416
  });
4417
+ if (!pendingHandoff) return;
4260
4418
  statusPayload.parallelHandoff = pendingHandoff;
4261
4419
  statusPayload.lastUpdate = Date.now();
4262
4420
  writeStatusPayload();
4263
4421
  },
4264
4422
  }));
4265
4423
  } catch (error) {
4266
- if (singleWorktreeSetup) cleanupWorktrees(singleWorktreeSetup);
4267
- throw error;
4424
+ if (error instanceof WorktreeSetupError) publishSetupUnknown(error.snapshot);
4425
+ if (config.deadlineAt !== undefined && Date.now() >= config.deadlineAt) timeoutRunner();
4426
+ const message = error instanceof Error ? error.message : String(error);
4427
+ if (childStopRequests.has(flatIndex)) markChildStopped(flatIndex);
4428
+ const statusStep = requiredStatusStep(statusPayload, flatIndex);
4429
+ statusStep.status = stopped || childStopRequests.has(flatIndex) ? "stopped" : interrupted ? "paused" : "failed";
4430
+ statusStep.error ??= message;
4431
+ statusStep.exitCode = interrupted ? 0 : 1;
4432
+ statusStep.endedAt = Date.now();
4433
+ results.push(stopped ? stoppedStepResult(seqStep.agent, seqStep.context, seqStep.sessionName)
4434
+ : childStopRequests.has(flatIndex) ? childStopResult(flatIndex, seqStep.agent, seqStep.context)
4435
+ : timedOut ? timedOutStepResult(seqStep.agent, seqStep.context, seqStep.sessionName)
4436
+ : interrupted ? pausedStepResult(seqStep.agent, seqStep.context, seqStep.sessionName)
4437
+ : { agent: seqStep.agent, output: message, error: message, exitCode: 1, success: false });
4438
+ statusPayload.error ??= message;
4439
+ writeStatusPayload();
4440
+ flatIndex++;
4441
+ break;
4268
4442
  }
4269
4443
  }
4444
+ if (singleWorktreeSetup && config.deadlineAt !== undefined && Date.now() >= config.deadlineAt) timeoutRunner();
4445
+ if (singleWorktreeSetup && (timedOut || stopped || interrupted || childStopRequests.has(flatIndex))) {
4446
+ await cleanupRemainingWorktree(singleWorktreeSetup, stepIndex, flatIndex);
4447
+ results.push(stopped ? stoppedStepResult(seqStep.agent, seqStep.context, seqStep.sessionName)
4448
+ : childStopRequests.has(flatIndex) ? childStopResult(flatIndex, seqStep.agent, seqStep.context)
4449
+ : timedOut ? timedOutStepResult(seqStep.agent, seqStep.context, seqStep.sessionName)
4450
+ : pausedStepResult(seqStep.agent, seqStep.context, seqStep.sessionName));
4451
+ if (interrupted) requiredStatusStep(statusPayload, flatIndex).status = "paused";
4452
+ flatIndex++;
4453
+ continue;
4454
+ }
4270
4455
  const singleCwd = singleWorktreeSetup?.worktrees[0]?.agentCwd ?? cwd;
4271
4456
  const stepStartTime = Date.now();
4272
4457
  statusPayload.currentStep = flatIndex;
@@ -4325,11 +4510,12 @@ async function runSubagent(
4325
4510
  onExternalProcess: (process) => updateExternalProcess(flatIndex, process),
4326
4511
  onExternalJob: (externalJob) => updateExternalJob(flatIndex, externalJob),
4327
4512
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(flatIndex),
4328
- usageBudgetExhausted: () => refreshUsageBudget()?.exhausted === true,
4513
+ usageBudgetExhausted: continuationUsageBudgetExhausted,
4514
+ usageBudget: config.usageBudget,
4329
4515
  orcaProgressTab,
4330
4516
  }), config.deadlineAt);
4331
4517
  } catch (error) {
4332
- if (singleWorktreeSetup) cleanupWorktrees(singleWorktreeSetup);
4518
+ if (singleWorktreeSetup) await cleanupRemainingWorktree(singleWorktreeSetup, stepIndex, flatIndex);
4333
4519
  throw error;
4334
4520
  }
4335
4521
  if (seqStep.sessionFile) {
@@ -4361,6 +4547,7 @@ async function runSubagent(
4361
4547
  totalCost: singleResult.totalCost,
4362
4548
  usage: singleResult.usage,
4363
4549
  artifactPaths: singleResult.artifactPaths,
4550
+ savedOutputPath: singleResult.savedOutputPath,
4364
4551
  transcriptPath: singleResult.transcriptPath,
4365
4552
  transcriptError: singleResult.transcriptError,
4366
4553
  effects: singleResult.effects,
@@ -4481,48 +4668,51 @@ async function runSubagent(
4481
4668
  }));
4482
4669
  if (stopped || childStopped) appendTerminalChildStatusEvent(flatIndex, stepEndTime);
4483
4670
  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();
4671
+ const setup = singleWorktreeSetup;
4672
+ await finalizeWorktree(setup, stepIndex, flatIndex, () => {
4673
+ const diffs = diffWorktrees(setup, [seqStep.agent], path.join(asyncDir, "worktree-diffs", `step-${stepIndex}`));
4674
+ const diffSummary = formatWorktreeDiffSummary(diffs);
4675
+ const manifestPath = parallelHandoffPath(asyncDir);
4676
+ const handoff = {
4677
+ manifestPath,
4678
+ runId: id,
4679
+ mode: "single" as const,
4680
+ source: "async" as const,
4681
+ cwd,
4682
+ stepIndex,
4683
+ flatStartIndex: flatIndex,
4684
+ setup,
4685
+ diffs,
4686
+ results: [{
4687
+ agent: singleResult.agent,
4688
+ ...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}),
4689
+ ...(handoffChildRunId ? { runId: handoffChildRunId } : {}),
4690
+ ...(config.lane ? { lane: config.lane } : {}),
4691
+ status: singleResult.stopped ? "stopped" as const : singleResult.interrupted ? "paused" as const : singleResult.exitCode === 0 ? "completed" as const : "failed" as const,
4692
+ summary: singleResult.output || singleResult.error || "(no output)",
4693
+ ...(singleResult.artifactPaths?.outputPath ? { outputPath: singleResult.artifactPaths.outputPath } : {}),
4694
+ ...(singleResult.structuredOutput !== undefined ? { structuredOutput: singleResult.structuredOutput } : {}),
4695
+ ...(singleResult.structuredOutputPath ? { structuredOutputPath: singleResult.structuredOutputPath } : {}),
4696
+ ...(singleResult.sessionFile ? { sessionPath: singleResult.sessionFile } : {}),
4697
+ }],
4698
+ };
4699
+ try {
4700
+ writeParallelHandoffGroup(handoff);
4701
+ const cleanup = cleanupWorktrees(setup, {
4702
+ kind: "preserve",
4703
+ capturedDiffs: diffs,
4704
+ handoffManifestPath: manifestPath,
4705
+ ...(config.parentWorkflowRunId && singleResult.sessionFile && fs.existsSync(singleResult.sessionFile) && !singleResult.stopped
4706
+ ? { cleanupBlocker: "retained child resume requires managed worktree cwd" }
4707
+ : {}),
4708
+ });
4709
+ statusPayload.parallelHandoff = writeParallelHandoffGroup({ ...handoff, cleanup });
4710
+ previousOutput = [previousOutput, diffSummary, formatParallelHandoffReference(statusPayload.parallelHandoff)].filter(Boolean).join("\n\n");
4711
+ } catch (error) {
4712
+ previousOutput = [previousOutput, diffSummary, formatParallelHandoffError(error)].filter(Boolean).join("\n\n");
4713
+ }
4714
+ writeStatusPayload();
4715
+ });
4526
4716
  }
4527
4717
 
4528
4718
  if (singleResult.completionGuardTriggered) {
@@ -4540,7 +4730,7 @@ async function runSubagent(
4540
4730
  }
4541
4731
 
4542
4732
  flatIndex++;
4543
- if (isAgentContractV1(seqStep.agentContract) && seqStep.gateOn === "acceptance" && singleResult.acceptance?.status === "rejected") {
4733
+ if (isAgentContract(seqStep.agentContract) && seqStep.gateOn === "acceptance" && singleResult.acceptance?.status === "rejected") {
4544
4734
  statusPayload.error = acceptanceFailureMessage(singleResult.acceptance) ?? "Chain acceptance gate rejected the step.";
4545
4735
  writeStatusPayload();
4546
4736
  break;
@@ -4626,6 +4816,11 @@ async function runSubagent(
4626
4816
  stopped: result.stopped,
4627
4817
  })));
4628
4818
  const partialWithEvidence = !stopped && !signalTerminated && !timedOut && !usageBudgetExceeded && !interrupted && results.some(partialEvidenceResult) && !results.some(concreteFailureResult);
4819
+ // Flush while still nonterminal; deferred status retries retain that state snapshot.
4820
+ statusWriteCoalescer.flush(statusPath);
4821
+ const publication = new Promise<void>((resolve, reject) => {
4822
+ finalResultPublication = { resolve, reject };
4823
+ });
4629
4824
  statusPayload.state = stopped || signalTerminated ? "stopped" : timedOut || usageBudgetExceeded ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : partialWithEvidence ? "partial" : "failed";
4630
4825
  closeSteerInbox(asyncDir, statusPayload.state, (filePath, payload) => runPersistence.write(filePath, payload));
4631
4826
  disposeControlInbox();
@@ -4684,6 +4879,9 @@ async function runSubagent(
4684
4879
  statusPayload.error = `Step failed: ${failedStep.agent}`;
4685
4880
  }
4686
4881
  }
4882
+ let childSessionDisposal: Promise<void> | undefined;
4883
+ const disposeChildSessions = (): Promise<void> => childSessionDisposal ??= childSessions.dispose()
4884
+ .catch((error: unknown) => console.error("Failed to dispose runner child sessions:", error));
4687
4885
  try {
4688
4886
  runPersistence.write(resultPath, {
4689
4887
  lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
@@ -4724,6 +4922,7 @@ async function runSubagent(
4724
4922
  totalCost: r.totalCost,
4725
4923
  usage: r.usage,
4726
4924
  artifactPaths: r.artifactPaths,
4925
+ savedOutputPath: r.savedOutputPath,
4727
4926
  outputSaveError: r.outputSaveError,
4728
4927
  metadataSaveError: r.metadataSaveError,
4729
4928
  truncated: r.truncated,
@@ -4778,13 +4977,16 @@ async function runSubagent(
4778
4977
  ...(taskIndex !== undefined && { taskIndex }),
4779
4978
  ...(totalTasks !== undefined && { totalTasks }),
4780
4979
  }, (filePath, payload) => { writeAsyncResultFile(filePath, payload as Record<string, unknown>); });
4781
- finalResultCommitted = true;
4980
+ // Only capacity deferral releases settled sessions before terminal publication.
4981
+ if (!finalResultCommitted) await Promise.all([publication, disposeChildSessions()]);
4782
4982
  } catch (err) {
4783
4983
  const message = `Failed to write result file ${resultPath}: ${err instanceof Error ? err.message : String(err)}`;
4784
4984
  console.error(message, err);
4785
4985
  statusPayload.state = "failed";
4786
4986
  statusPayload.error = message;
4787
4987
  statusPayload.lastUpdate = Date.now();
4988
+ } finally {
4989
+ finalResultPublication = undefined;
4788
4990
  }
4789
4991
  writeStatusPayload();
4790
4992
  await orcaProgressTab?.finish(statusPayload.state === "complete" ? "completed" : statusPayload.state === "stopped" ? "stopped" : "failed", effectiveSessionFile);
@@ -4822,9 +5024,8 @@ async function runSubagent(
4822
5024
  }), (filePath, content) => runPersistence.write(filePath, { content }, (_path, payload) => {
4823
5025
  fs.writeFileSync(_path, (payload as { content: string }).content, "utf-8");
4824
5026
  }));
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));
5027
+ // Preserve normal-success disposal ordering, then drain remaining persistence retries.
5028
+ await disposeChildSessions();
4828
5029
  while (runPersistence.pendingCount() + indexPersistence.pendingCount() > 0) {
4829
5030
  await new Promise((resolve) => setTimeout(resolve, 200));
4830
5031
  }
@@ -4832,7 +5033,7 @@ async function runSubagent(
4832
5033
  indexPersistence.dispose();
4833
5034
  if (config.runnerProcessInstanceId) {
4834
5035
  // Children run inside this process, so no step has writer processes to prove terminal.
4835
- const writers: Record<string, PiWriterProcessInstanceExitV1[]> = {};
5036
+ const writers: Record<string, PiWriterProcessInstanceExit[]> = {};
4836
5037
  const expectedWriters: Record<string, number> = {};
4837
5038
  for (const index of results.keys()) {
4838
5039
  writers[String(index)] = [];
@@ -4968,6 +5169,7 @@ function startConfiguredSubagent(config: SubagentRunConfig): void {
4968
5169
  );
4969
5170
  }
4970
5171
 
5172
+ if (isRunnerEntrypoint) {
4971
5173
  const configArg = process.argv[2];
4972
5174
  if (configArg) {
4973
5175
  try {
@@ -4999,3 +5201,4 @@ if (configArg) {
4999
5201
  }
5000
5202
  });
5001
5203
  }
5204
+ }