pi-subagents 0.65.0 → 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 (104) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/README.md +1 -1
  3. package/agents/researcher.md +23 -13
  4. package/docs/agents.md +17 -3
  5. package/docs/configuration.md +34 -0
  6. package/docs/extension-api.md +94 -0
  7. package/docs/models.md +58 -1
  8. package/docs/observability.md +42 -2
  9. package/docs/tool-reference.md +11 -5
  10. package/docs/workflows.md +22 -7
  11. package/package.json +4 -1
  12. package/runner-server-preload.mjs +13 -0
  13. package/skills/pi-subagents/SKILL.md +2 -1
  14. package/skills/pi-subagents/references/execution-controls.md +19 -2
  15. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
  16. package/skills/pi-subagents/references/multi-lane-orchestration.md +2 -0
  17. package/src/agents/advertised-agent-prompt.ts +63 -0
  18. package/src/agents/agent-management.ts +14 -1
  19. package/src/agents/agent-serializer.ts +2 -0
  20. package/src/agents/agents.ts +8 -0
  21. package/src/api/preflight.ts +5 -1
  22. package/src/api/shared-types.ts +1 -1
  23. package/src/api/workflow-resources.ts +6 -0
  24. package/src/extension/config.ts +4 -2
  25. package/src/extension/index.ts +71 -4
  26. package/src/extension/public-execution.ts +0 -1
  27. package/src/extension/rpc.ts +4 -21
  28. package/src/extension/schemas.ts +9 -7
  29. package/src/extension/tool-description.ts +10 -5
  30. package/src/integrations/pi-web-session-liveness.ts +73 -0
  31. package/src/intercom/native-supervisor-channel.ts +102 -88
  32. package/src/intercom/supervisor-ui.ts +3 -2
  33. package/src/missions/workflow-state.ts +37 -16
  34. package/src/runs/background/active-async-capacity.ts +18 -18
  35. package/src/runs/background/async-execution.ts +8 -1
  36. package/src/runs/background/async-job-tracker.ts +35 -3
  37. package/src/runs/background/async-resume.ts +3 -1
  38. package/src/runs/background/async-retention.ts +9 -0
  39. package/src/runs/background/async-status-snapshot.ts +10 -12
  40. package/src/runs/background/async-status.ts +17 -9
  41. package/src/runs/background/auto-drain.ts +40 -29
  42. package/src/runs/background/chain-root-attachment.ts +8 -0
  43. package/src/runs/background/control-channel.ts +78 -44
  44. package/src/runs/background/notify.ts +88 -12
  45. package/src/runs/background/owned-process-tree.ts +6 -6
  46. package/src/runs/background/process-terminal.ts +23 -23
  47. package/src/runs/background/retained-nested-route-tracker.ts +96 -0
  48. package/src/runs/background/run-child-session.ts +62 -33
  49. package/src/runs/background/run-status.ts +75 -5
  50. package/src/runs/background/runner-aliases.ts +46 -8
  51. package/src/runs/background/runner-child-launch.ts +86 -0
  52. package/src/runs/background/stale-run-reconciler.ts +3 -1
  53. package/src/runs/background/subagent-runner.ts +430 -208
  54. package/src/runs/background/subagent-wait.ts +3 -0
  55. package/src/runs/background/wait-completions.ts +4 -0
  56. package/src/runs/foreground/async-steering-action.ts +19 -0
  57. package/src/runs/foreground/execution.ts +116 -26
  58. package/src/runs/foreground/foreground-history.ts +3 -1
  59. package/src/runs/foreground/prompt-audit.ts +9 -5
  60. package/src/runs/foreground/subagent-executor.ts +531 -227
  61. package/src/runs/foreground/workflow-detach-reconcile.ts +8 -5
  62. package/src/runs/foreground/workflow-foreground-steering.ts +56 -2
  63. package/src/runs/shared/acceptance.ts +16 -3
  64. package/src/runs/shared/agent-contract.ts +1 -1
  65. package/src/runs/shared/async-status-projection.ts +47 -47
  66. package/src/runs/shared/child-hooks.ts +151 -2
  67. package/src/runs/shared/child-launch.ts +18 -13
  68. package/src/runs/shared/child-session.ts +55 -24
  69. package/src/runs/shared/child-tool-plan.ts +2 -2
  70. package/src/runs/shared/completion-evidence.ts +2 -2
  71. package/src/runs/shared/completion-guard.ts +1 -0
  72. package/src/runs/shared/host-step-status.ts +11 -11
  73. package/src/runs/shared/llm-intent-arbiter.ts +30 -20
  74. package/src/runs/shared/model-exclusions.ts +2 -1
  75. package/src/runs/shared/model-fallback.ts +41 -8
  76. package/src/runs/shared/nested-events.ts +8 -8
  77. package/src/runs/shared/orca-progress-tabs.ts +6 -0
  78. package/src/runs/shared/parallel-handoff.ts +57 -12
  79. package/src/runs/shared/parallel-utils.ts +3 -2
  80. package/src/runs/shared/readonly-drain-observation.ts +42 -0
  81. package/src/runs/shared/readonly-model-continuation.ts +69 -0
  82. package/src/runs/shared/readonly-session-evidence.ts +307 -0
  83. package/src/runs/shared/run-fanout-budget.ts +8 -8
  84. package/src/runs/shared/runtime-acknowledged-extensions.ts +3 -3
  85. package/src/runs/shared/subagent-prompt-runtime.ts +13 -3
  86. package/src/runs/shared/worktree-cleanup-plan.ts +6 -3
  87. package/src/runs/shared/worktree-setup-command.ts +190 -0
  88. package/src/runs/shared/worktree.ts +403 -210
  89. package/src/shared/model-response-aliases.ts +13 -0
  90. package/src/shared/types.ts +89 -60
  91. package/src/shared/utils.ts +10 -2
  92. package/src/shared/watch-strategy.ts +2 -0
  93. package/src/shared/workflow-child-permit.ts +18 -13
  94. package/src/tui/fleet-status.ts +1 -1
  95. package/src/tui/fleet.ts +11 -5
  96. package/src/tui/render.ts +44 -15
  97. package/src/workflows/chat-progress.ts +3 -3
  98. package/src/workflows/scripted-workflow.ts +70 -16
  99. package/src/workflows/workflow-checklist.ts +15 -18
  100. package/src/workflows/workflow-child-summary.ts +57 -8
  101. package/src/workflows/workflow-preflight.ts +19 -19
  102. package/src/workflows/workflow-receipt.ts +3 -3
  103. package/src/workflows/workflow-resources.ts +96 -21
  104. package/src/workflows/workflow-settlement.ts +3 -0
@@ -1,8 +1,28 @@
1
1
  import { spawnSync } from "node:child_process";
2
+ import { EventEmitter } from "node:events";
2
3
  import * as fs from "node:fs";
4
+ import { createRequire } from "node:module";
3
5
  import * as path from "node:path";
4
6
  import { pathToFileURL } from "node:url";
5
7
  import type { Message } from "@earendil-works/pi-ai";
8
+ import { arbitrateCompletionGuardRescue, createTaskMutationArbiter } from "../shared/llm-intent-arbiter.ts";
9
+
10
+ // Detached runners skip Pi's CLI proxy setup. Keep fetch on the same Undici dispatcher.
11
+ function ensureProxyAwareHttpDispatcher(): void {
12
+ try {
13
+ // SAFETY: require loads the pinned direct dependency described by these types.
14
+ const undici = createRequire(import.meta.url)("undici") as typeof import("undici");
15
+ const dispatcher = new undici.EnvHttpProxyAgent({ allowH2: false });
16
+ // Fetch rejects stream errors; the listener prevents an unhandled EventEmitter error.
17
+ EventEmitter.prototype.on.call(dispatcher, "error", () => {});
18
+ undici.setGlobalDispatcher(dispatcher);
19
+ undici.install();
20
+ } catch (error) {
21
+ console.error(`[pi-subagents] proxy-aware HTTP dispatcher not installed: ${error instanceof Error ? error.message : String(error)}`);
22
+ }
23
+ }
24
+ const isRunnerEntrypoint = Boolean(process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href);
25
+ if (isRunnerEntrypoint) ensureProxyAwareHttpDispatcher();
6
26
  import { writeAtomicJson } from "../../shared/atomic-json.ts";
7
27
  import { writeAsyncResultFile, writePendingAsyncResultFile } from "./result-files.ts";
8
28
  import { createFileCoalescer } from "../../shared/file-coalescer.ts";
@@ -27,10 +47,10 @@ import {
27
47
  type AsyncStatus,
28
48
  type ChainOutputMap,
29
49
  type CostSummary,
30
- type LaunchResolvedChildExtensionsV1,
31
- type RuntimeAcknowledgedChildExtensionsV1,
50
+ type LaunchResolvedChildExtensions,
51
+ type RuntimeAcknowledgedChildExtensions,
32
52
  type ModelAttempt,
33
- type PiWriterProcessInstanceExitV1,
53
+ type PiWriterProcessInstanceExit,
34
54
  type NestedRouteInfo,
35
55
  type NestedRunSummary,
36
56
  type ResolvedControlConfig,
@@ -72,10 +92,14 @@ import {
72
92
  DEFAULT_GLOBAL_CONCURRENCY_LIMIT,
73
93
  Semaphore,
74
94
  } from "../shared/parallel-utils.ts";
75
- import { applyThinkingSuffix, deriveForkPromptCacheKey, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/child-tool-plan.ts";
76
- 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";
77
99
  import type { ChildSessionFactory } from "../shared/child-session.ts";
78
- 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";
79
103
  import { loadRunnerChildSessionFactory } from "./runner-child-sessions.ts";
80
104
  import { SUBAGENT_CHILD_ENV } from "../shared/child-runtime-config.ts";
81
105
  import { deriveChildSessionName } from "../../shared/child-session-name.ts";
@@ -89,7 +113,7 @@ import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelSt
89
113
  import { claimRunFanoutBatch, getRunFanoutBudgetSnapshot } from "../shared/run-fanout-budget.ts";
90
114
  import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDir, writeNestedEvent } from "../shared/nested-events.ts";
91
115
  import { formatModelAttemptNote, formatSubagentModelVerificationError, isContextOverflow, isRetryableModelFailureAttempt, recordRetryableModelFailure } from "../shared/model-fallback.ts";
92
- import { markProcessTerminalCandidateLeaseRelease, writeProcessTerminalCandidate, type ProcessTerminalCandidate } from "./process-terminal.ts";
116
+ import { markProcessTerminalCandidateLeaseRelease, processTerminalPath, writeProcessTerminalCandidate, type ProcessTerminalCandidate } from "./process-terminal.ts";
93
117
  import { createSteeringStatus, recordSteeringRequest, steeringStatus, terminalSteeringNoticeState, updateSteeringTarget } from "./steering.ts";
94
118
  import { PROMPT_REDACTED, detectSubagentError, extractTextFromContent, extractToolArgsPreview, formatEmptyTerminalAssistantResponseError, getFinalOutput, hasEmptyTerminalAssistantResponse, readStatus } from "../../shared/utils.ts";
95
119
  import { evaluateCompletionMutationGuard, expectsImplementationMutation, hasMutationToolCapability, validateImplementationToolContract } from "../shared/completion-guard.ts";
@@ -111,6 +135,9 @@ import type { TokenUsage } from "../../shared/types.ts";
111
135
  import {
112
136
  cleanupWorktrees,
113
137
  createWorktrees,
138
+ withWorktreeTransaction,
139
+ WorktreeSetupError,
140
+ type WorktreeSetupProgress,
114
141
  diffWorktrees,
115
142
  findWorktreeTaskCwdConflict,
116
143
  formatWorktreeDiffSummary,
@@ -118,21 +145,20 @@ import {
118
145
  WORKTREE_AGENT_CWD_PLACEHOLDER,
119
146
  type WorktreeSetup,
120
147
  } from "../shared/worktree.ts";
121
- import { findModelInfo, resolveEffectiveThinking } from "../../shared/model-info.ts";
148
+ import { findModelInfo, resolveEffectiveThinking, splitKnownThinkingSuffix } from "../../shared/model-info.ts";
122
149
  import { assertThinkingWithinCeiling } from "../../shared/thinking-ceiling.ts";
123
150
  import { launchBindingDigest } from "../../shared/launch-contract.ts";
124
151
  import { writeInitialProgressFile } from "../../shared/settings.ts";
125
152
  import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
126
153
  import { acceptanceFailureMessage, aggregateAcceptanceReport, buildSkippedAcceptanceLedger, evaluateAcceptance, formatAcceptancePrompt, resolveAcceptanceReportMode, resolveEffectiveAcceptance, stripAcceptanceReport } from "../shared/acceptance.ts";
127
- import { attachContractProjections, isAgentContractV1 } from "../shared/agent-contract.ts";
154
+ import { attachContractProjections, isAgentContract } from "../shared/agent-contract.ts";
128
155
  import { waitForImportedAsyncRoot } from "./chain-root-attachment.ts";
129
- import { normalizeExtensionBindings } from "../shared/extension-bindings.ts";
130
156
  import { appendRunnerStepsToStatus, consumeChainAppendRequests, countPendingChainAppendRequests, statusStepDescription } from "./chain-append.ts";
131
157
  import { asyncStatusChildIdentity } from "../shared/child-identity.ts";
132
158
  import { initialToolBudgetState, toolBudgetState } from "../shared/tool-budget.ts";
133
159
  import { effectiveToolTimeoutMs, formatToolTimeoutMessage, toolTimeoutCallKey } from "../shared/tool-timeout.ts";
134
160
  import { usageBudgetExceededMessage, usageBudgetState } from "../shared/usage-budget.ts";
135
- import { formatParallelHandoffError, formatParallelHandoffReference, parallelHandoffPath, writeParallelHandoffGroup, writePendingParallelHandoff } from "../shared/parallel-handoff.ts";
161
+ import { formatParallelHandoffError, formatParallelHandoffReference, parallelHandoffPath, writeParallelHandoffGroup, writeWorktreeSetupHandoff } from "../shared/parallel-handoff.ts";
136
162
  import { resolveWatchdogConfig } from "../../watchdog/settings.ts";
137
163
  import { acquireSessionLease, type SessionLeaseRequest } from "../shared/session-lease.ts";
138
164
  import { buildExternalCliPrompt, runExternalCli } from "../shared/external-cli-runner.ts";
@@ -207,8 +233,8 @@ interface SubagentRunConfig {
207
233
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
208
234
  runFanoutBudget?: RunFanoutBudgetDescriptor;
209
235
  launchContractDigest?: string;
210
- launchResolvedExtensions?: LaunchResolvedChildExtensionsV1;
211
- runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
236
+ launchResolvedExtensions?: LaunchResolvedChildExtensions;
237
+ runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions;
212
238
  runnerProcessInstanceId?: string;
213
239
  launchBarrierToken?: string;
214
240
  parentWorkflowRunId?: string;
@@ -223,8 +249,8 @@ interface StepResult {
223
249
  context?: "fresh" | "fork";
224
250
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
225
251
  capabilityAudit?: import("../shared/capability-ceiling.ts").SubagentCapabilityAudit;
226
- launchResolvedExtensions?: LaunchResolvedChildExtensionsV1;
227
- runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
252
+ launchResolvedExtensions?: LaunchResolvedChildExtensions;
253
+ runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions;
228
254
  output: string;
229
255
  outputState?: SubagentOutputState;
230
256
  error?: string;
@@ -674,14 +700,17 @@ interface SingleStepContext {
674
700
  onExternalProcess?: (process: ExternalProcessStatus) => void;
675
701
  onExternalJob?: (status: ExternalJobStatus) => void;
676
702
  skipAcceptance?: () => boolean;
677
- 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;
678
707
  /** False when sibling work in the same Git worktree could have caused the tracked diff. */
679
708
  trackedMutationEvidenceForCompletionGuard?: boolean;
680
709
  orcaProgressTab?: OrcaProgressTab;
681
710
  }
682
711
 
683
712
  /** Run a single pi agent step, returning output and metadata */
684
- async function runSingleStepInner(
713
+ export async function runSingleStepInner(
685
714
  step: SubagentStep,
686
715
  ctx: SingleStepContext,
687
716
  ): Promise<StepResult & { completionGuardTriggered?: boolean }> {
@@ -809,8 +838,8 @@ async function runSingleStepInner(
809
838
  // Derive from the pre-acceptance task so internal acceptance/recovery
810
839
  // instructions never leak into the display name.
811
840
  const childSessionName = step.sessionName ?? deriveChildSessionName({ agent: step.agent, task, label: step.label });
812
- if (step.effectiveAcceptance) {
813
- 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) });
814
843
  if (acceptancePrompt) task = `${task}\n${acceptancePrompt}`;
815
844
  }
816
845
  const sessionEnabled = Boolean(step.sessionFile) || ctx.sessionEnabled;
@@ -1017,11 +1046,31 @@ async function runSingleStepInner(
1017
1046
  let modelIndex = 0;
1018
1047
  let contextOverflow = false;
1019
1048
  let launchWarningsEmitted = false;
1020
- 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
+ };
1021
1069
  let nextAttemptTask = task;
1022
1070
  modelAttemptsLoop: while (modelIndex < candidates.length) {
1023
1071
  if (ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break;
1024
- const recoveringAbort = abortRecoveryAttempted;
1072
+ if (readonlyContinuation && !canContinue()) break;
1073
+ const recoveringAbort = recoveryState === "abort-recovery";
1025
1074
  const attemptTask = nextAttemptTask;
1026
1075
  const candidate = candidates[modelIndex];
1027
1076
  const expectedModelForVerification = candidate && !(step.skipPrimaryModelVerification && modelIndex === 0) ? candidate : undefined;
@@ -1029,6 +1078,10 @@ async function runSingleStepInner(
1029
1078
  assertThinkingWithinCeiling({ model: candidate, configThinking: step.thinking, ceiling: step.thinkingCeiling, agent: step.agent, runId: ctx.id });
1030
1079
  } catch (error) {
1031
1080
  const message = error instanceof Error ? error.message : String(error);
1081
+ if (readonlyContinuation) {
1082
+ failContinuationLaunch(candidate, error);
1083
+ break modelAttemptsLoop;
1084
+ }
1032
1085
  return omitUndefinedProperties({ agent: step.agent, output: message, error: message, exitCode: 1, context: step.context, thinkingCeiling: step.thinkingCeiling });
1033
1086
  }
1034
1087
  ctx.onAttemptStart?.(omitUndefinedProperties({
@@ -1054,56 +1107,22 @@ async function runSingleStepInner(
1054
1107
  })
1055
1108
  : undefined;
1056
1109
  let watchdogSink: ((event: ChildWatchdogStatusEvent) => void) | undefined;
1057
- const launch = buildInProcessChildLaunch(omitUndefinedProperties({
1058
- parentSessionId: step.parentSessionId,
1059
- forkCacheKey: step.context === "fork" ? deriveForkPromptCacheKey(step.parentSessionId) : undefined,
1060
- sessionEnabled,
1061
- sessionDir,
1062
- sessionFile: step.sessionFile,
1063
- model: candidate,
1064
- inheritProjectContext: step.inheritProjectContext,
1065
- inheritGlobalContext: step.inheritGlobalContext,
1066
- inheritSkills: step.inheritSkills,
1067
- requireReadTool: Boolean(step.skills?.length),
1068
- tools: step.tools,
1069
- excludeTools: step.excludeTools,
1070
- allowNestedSubagents: step.allowNestedSubagents,
1071
- extensions: step.extensions,
1072
- subagentOnlyExtensions: step.subagentOnlyExtensions,
1073
- fast: step.fast,
1074
- modelCandidates: step.modelCandidates,
1075
- systemPrompt: step.systemPrompt ?? "",
1076
- systemPromptMode: step.systemPromptMode,
1077
- mcpDirectTools: step.mcpDirectTools,
1078
- extensionBindings,
1079
- capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
1080
- cwd: step.cwd ?? ctx.cwd,
1081
- intercomSessionName: ctx.childIntercomTarget,
1082
- sessionName: childSessionName,
1083
- orchestratorIntercomTarget: ctx.orchestratorIntercomTarget,
1084
- runId: ctx.id,
1085
- childAgentName: step.agent,
1086
- childIndex: ctx.flatIndex,
1087
- nestedRoute: ctx.nestedRoute,
1088
- runFanoutBudget: ctx.runFanoutBudget ? {
1089
- ...ctx.runFanoutBudget,
1090
- ...(step.runFanoutPath ? { parentPath: `${ctx.runFanoutBudget.parentPath ? `${ctx.runFanoutBudget.parentPath}/` : ""}${step.runFanoutPath}` } : {}),
1091
- } : undefined,
1092
- structuredOutput: effectiveStructuredOutput,
1093
- toolBudget: step.toolBudget,
1094
- permissionRules: step.permissionRules,
1095
- permissionAuditPath: step.permissionRules && ctx.artifactsDir
1096
- ? path.join(ctx.artifactsDir, "permission-audit", `${ctx.id}-${ctx.flatIndex}.jsonl`)
1097
- : undefined,
1098
- childWatchdog,
1099
- watchdogStatus: (event) => watchdogSink?.(event),
1100
- waitToolEnabled: step.waitToolEnabled,
1101
- waitToolDefaultTimeoutMs: step.waitToolDefaultTimeoutMs,
1102
- thinkingCeiling: step.thinkingCeiling,
1103
- maxSubagentDepth: step.maxSubagentDepth,
1104
- inherited: ctx.inheritedChildRuntime,
1105
- host: "runner",
1106
- }));
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
+ }
1107
1126
  if (effectiveStructuredOutput && launch.config.structuredOutput) {
1108
1127
  // The runner reads the value back from the runtime's files after the run.
1109
1128
  launch.config.structuredOutput.capture = createStructuredOutputFileCapture(effectiveStructuredOutput);
@@ -1163,6 +1182,9 @@ async function runSingleStepInner(
1163
1182
  const run = await runChildSession(omitUndefinedProperties({
1164
1183
  factory: ctx.childSessions,
1165
1184
  launch,
1185
+ collectReadonlyEvidence: true,
1186
+ readonlyContinuation,
1187
+ canContinue,
1166
1188
  prompt: `Task: ${attemptTask}`,
1167
1189
  childWatchdog,
1168
1190
  childEventContext: { runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
@@ -1188,6 +1210,7 @@ async function runSingleStepInner(
1188
1210
  runDeadlineAt: ctx.deadlineAt,
1189
1211
  expectedModelForVerification,
1190
1212
  modelVerificationRegistry: step.modelVerificationRegistry,
1213
+ modelResponseAliases: step.modelResponseAliases,
1191
1214
  mutationTools: step.mutationTools,
1192
1215
  }));
1193
1216
  const toolDiagnostic = run.exitCode === 0 && !run.error ? launch.capture.toolDiagnostic() : undefined;
@@ -1242,7 +1265,7 @@ async function runSingleStepInner(
1242
1265
  && (!hiddenError?.hasError || hasEmptyTerminalAssistantResponse(run.messages))
1243
1266
  ? formatEmptyTerminalAssistantResponseError(run.messages)
1244
1267
  : undefined;
1245
- const completionGuardEnabled = isAgentContractV1(step.agentContract) ? step.completionGuard === true : step.completionGuard !== false;
1268
+ const completionGuardEnabled = isAgentContract(step.agentContract) ? step.completionGuard === true : step.completionGuard !== false;
1246
1269
  const completionToolPlan = resolvedTaskToolPlan;
1247
1270
  const completionTools = completionToolPlan ? (completionToolPlan.explicitToolAllowlist ? completionToolPlan.effectiveToolAllowlist : undefined) : step.tools;
1248
1271
  const mutationEvidence = collectTrackedMutationEvidence(mutationSnapshot, step.cwd ?? ctx.cwd);
@@ -1261,14 +1284,27 @@ async function runSingleStepInner(
1261
1284
  }))
1262
1285
  : undefined;
1263
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
+ }
1264
1298
  const completionEvidence = planCompletionEvidence({
1265
1299
  guard: completionGuard,
1300
+ guardTriggered: arbitration.triggered,
1301
+ arbiterRescued: arbitration.rescued,
1266
1302
  completionGuardEnabled,
1267
1303
  mutationCapable: hasMutationToolCapability(completionTools, completionToolPlan?.effectiveMcpTools ?? step.mcpDirectTools),
1268
1304
  implementationMutationExpected: expectsImplementationMutation(step.agent, taskForCompletionGuard),
1269
1305
  mutationAttemptObserved,
1270
1306
  mutationEvidence: completionMutationEvidence,
1271
- agentContractV1: isAgentContractV1(step.agentContract),
1307
+ agentContractEnabled: isAgentContract(step.agentContract),
1272
1308
  });
1273
1309
  const finalOutputHasPersistableFileContent = run.exitCode === 0 && !run.error && !emptyOutputError && Boolean(stripAcceptanceReport(run.finalOutput).trim());
1274
1310
  const requiredOutput = step.outputMode === "file-only" && step.outputPath
@@ -1328,7 +1364,7 @@ async function runSingleStepInner(
1328
1364
  messages: run.messages,
1329
1365
  error,
1330
1366
  sessionAvailable: Boolean(step.sessionFile && fs.existsSync(step.sessionFile)),
1331
- alreadyResumed: abortRecoveryAttempted,
1367
+ alreadyResumed: recoveryState !== "unused",
1332
1368
  stopped: run.stopped || ctx.stopSignal?.aborted || ctx.skipAcceptance?.(),
1333
1369
  interrupted: run.interrupted,
1334
1370
  timedOut: run.timedOut || ctx.timeoutSignal?.aborted,
@@ -1348,15 +1384,59 @@ async function runSingleStepInner(
1348
1384
  if (run.stopped || run.timedOut || ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break modelAttemptsLoop;
1349
1385
  if (abortRecovery?.action === "settle" && abortRecovery.diagnostic) break modelAttemptsLoop;
1350
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;
1351
1389
  if (recoveringAbort) break modelAttemptsLoop;
1352
1390
  if (abortRecovery?.action === "resume") {
1353
- abortRecoveryAttempted = true;
1391
+ recoveryState = "abort-recovery";
1354
1392
  nextAttemptTask = abortRecovery.prompt;
1355
1393
  attemptNotes.push("[abort-recovery] provider/transport abort after useful progress; resuming the retained child session once.");
1356
1394
  continue;
1357
1395
  }
1358
1396
  if (completionEvidence.guardTriggered) break modelAttemptsLoop;
1359
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
+
1360
1440
  const retryableModelFailure = isRetryableModelFailureAttempt({ error, messages: run.messages, toolCount: run.toolCount });
1361
1441
  if (retryableModelFailure) recordRetryableModelFailure(candidate ?? run.model ?? step.model, error);
1362
1442
  if (isContextOverflow(error)) {
@@ -1438,7 +1518,7 @@ async function runSingleStepInner(
1438
1518
  cwd: step.cwd ?? ctx.cwd,
1439
1519
  signal: combinedAbortSignal([ctx.timeoutSignal, ctx.stopSignal]),
1440
1520
  abortMessage: ctx.stopSignal?.aborted ? ctx.stopMessage ?? "Subagent stopped by user." : ctx.timeoutMessage ?? "Subagent timed out.",
1441
- reportOptional: isAgentContractV1(step.agentContract),
1521
+ reportOptional: isAgentContract(step.agentContract),
1442
1522
  artifactsDir: ctx.artifactsDir,
1443
1523
  runId: ctx.id,
1444
1524
  watchdog: finalResult?.watchdog,
@@ -1454,7 +1534,7 @@ async function runSingleStepInner(
1454
1534
  : acceptance
1455
1535
  : undefined;
1456
1536
  const acceptanceFailure = effectiveAcceptance ? acceptanceFailureMessage(effectiveAcceptance) : undefined;
1457
- 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);
1458
1538
  const effectiveFinalExitCode = timedOutAfterAcceptance || stoppedAfterAcceptance ? 1 : acceptanceCanFailRun ? 1 : finalResult?.exitCode ?? 1;
1459
1539
  const intercomDetachReceipt = finalResult?.finalOutput === INTERCOM_DETACH_RECEIPT;
1460
1540
  const baseFinalError = stoppedAfterAcceptance
@@ -1496,7 +1576,7 @@ async function runSingleStepInner(
1496
1576
  ...(capabilityAudit ? { capabilityCeiling: capabilityAudit.ceiling, capabilityAudit } : {}),
1497
1577
  launchContractDigest: actualLaunchContractDigest,
1498
1578
  launchResolvedExtensions,
1499
- ...((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 } : {}),
1500
1580
  ...(transcriptWriter ? { transcriptPath: artifactPaths.transcriptPath } : {}),
1501
1581
  transcriptError: transcriptWriter?.getError(),
1502
1582
  skills: step.skills,
@@ -1525,6 +1605,7 @@ async function runSingleStepInner(
1525
1605
  totalCost: costSummaryFromAttempts(modelAttempts),
1526
1606
  usage,
1527
1607
  artifactPaths,
1608
+ savedOutputPath: finalizedOutput.savedPath,
1528
1609
  outputSaveError: [resolvedOutput.saveError, artifactErrors.outputSaveError].filter(Boolean).join("\n") || undefined,
1529
1610
  metadataSaveError: artifactErrors.metadataSaveError,
1530
1611
  transcriptPath: transcriptWriter ? artifactPaths?.transcriptPath : undefined,
@@ -1544,9 +1625,9 @@ async function runSingleStepInner(
1544
1625
  watchdog: finalResult?.watchdog,
1545
1626
  ...(capabilityAudit ? { capabilityCeiling: capabilityAudit.ceiling, capabilityAudit } : {}),
1546
1627
  launchResolvedExtensions,
1547
- ...((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 } : {}),
1548
1629
  });
1549
- 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;
1550
1631
  }
1551
1632
 
1552
1633
  async function runSingleStep(
@@ -1637,12 +1718,15 @@ function markParallelGroupSetupFailure(input: {
1637
1718
  const statusStep = requiredStatusStep(input.statusPayload, flatTaskIndex);
1638
1719
  const task = input.group.parallel[taskIndex];
1639
1720
  if (!task) throw new Error(`Missing parallel task at index ${taskIndex}`);
1640
- 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";
1641
1724
  statusStep.startedAt = input.failedAt;
1642
1725
  statusStep.endedAt = input.failedAt;
1643
1726
  statusStep.durationMs = 0;
1644
- statusStep.exitCode = 1;
1645
- 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 }));
1646
1730
  }
1647
1731
  input.statusPayload.currentStep = input.groupStartFlatIndex;
1648
1732
  input.statusPayload.lastUpdate = input.failedAt;
@@ -1799,7 +1883,10 @@ async function runSingleStepWithTimeout(
1799
1883
  ctx: SingleStepContext,
1800
1884
  parentDeadlineAt?: number,
1801
1885
  ): Promise<SingleStepResult> {
1802
- 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
+ });
1803
1890
 
1804
1891
  const parentRemainingMs = parentDeadlineAt === undefined ? undefined : Math.max(0, parentDeadlineAt - Date.now());
1805
1892
  const timeoutMs = parentRemainingMs === undefined ? step.timeoutMs : Math.min(step.timeoutMs, parentRemainingMs);
@@ -1836,7 +1923,7 @@ async function runSingleStepWithTimeout(
1836
1923
  }
1837
1924
  }
1838
1925
 
1839
- async function runSubagent(
1926
+ export async function runSubagent(
1840
1927
  config: SubagentRunConfig,
1841
1928
  childSessions: ChildSessionFactory,
1842
1929
  ): Promise<void> {
@@ -1874,6 +1961,8 @@ async function runSubagent(
1874
1961
  const stopMessage = "Subagent stopped by user.";
1875
1962
  const timeoutAbortController = new AbortController();
1876
1963
  const stopAbortController = new AbortController();
1964
+ const setupInterruptController = new AbortController();
1965
+ const setupSignal = AbortSignal.any([timeoutAbortController.signal, stopAbortController.signal, setupInterruptController.signal]);
1877
1966
  let previousCumulativeTokens: TokenUsage = { input: 0, output: 0, total: 0 };
1878
1967
  let latestSessionFile: string | undefined;
1879
1968
 
@@ -2040,20 +2129,29 @@ async function runSubagent(
2040
2129
  },
2041
2130
  onError: (error, filePath) => console.error(`Failed to update async run index '${filePath}':`, error),
2042
2131
  });
2043
- const queueActiveRunIndex = (): void => {
2044
- const state = statusPayload.state;
2132
+ const queueActiveRunIndex = (status: AsyncStatus): void => {
2133
+ const state = status.state;
2045
2134
  if (state === lastIndexedStatusState && indexPersistence.pendingCount() === 0) return;
2046
- indexPersistence.write(asyncDir, { state, toolCallId: statusPayload.toolCallId }, (_filePath, payload) => {
2135
+ indexPersistence.write(asyncDir, { state, toolCallId: status.toolCallId }, (_filePath, payload) => {
2047
2136
  const indexPayload = payload as { state: AsyncStatus["state"]; toolCallId?: string };
2048
2137
  updateActiveRunIndex(asyncDir, indexPayload.state, indexPayload.toolCallId, { retryCapacityErrors: true });
2049
2138
  });
2050
2139
  };
2140
+ let finalResultCommitted = false;
2141
+ let finalResultPublication: { resolve(): void; reject(error: unknown): void } | undefined;
2051
2142
  const runPersistence = createCapacityResilientJsonWriter({
2052
2143
  keepAlive: true,
2053
- onSuccess: (filePath) => {
2054
- 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);
2055
2154
  },
2056
- onError: (error, filePath) => console.error(`Failed to persist async run state '${filePath}':`, error),
2057
2155
  });
2058
2156
  try {
2059
2157
  fs.mkdirSync(asyncDir, { recursive: true });
@@ -2061,7 +2159,7 @@ async function runSubagent(
2061
2159
  if (!isStorageCapacityError(error)) throw error;
2062
2160
  console.error(`Failed to prepare async run storage '${asyncDir}' while storage is full:`, error);
2063
2161
  }
2064
- runPersistence.write(statusPath, statusPayload);
2162
+ runPersistence.write(statusPath, { ...statusPayload });
2065
2163
 
2066
2164
  let pendingParallelUsageCost: CostSummary = { inputTokens: 0, outputTokens: 0, costUsd: 0 };
2067
2165
  const currentUsageTotals = (): CostSummary => {
@@ -2080,6 +2178,13 @@ async function runSubagent(
2080
2178
  setOptionalProperty(statusPayload, "usageBudget", usageBudgetState(config.usageBudget, currentUsageTotals()));
2081
2179
  return statusPayload.usageBudget;
2082
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
+ };
2083
2188
  const emitNestedSelfEvent = (type: "subagent.nested.updated" | "subagent.nested.completed"): void => {
2084
2189
  if (!config.nestedRoute || !config.nestedSelf) return;
2085
2190
  try {
@@ -2134,7 +2239,6 @@ async function runSubagent(
2134
2239
  for (const node of graph.nodes) updateNode(node);
2135
2240
  statusPayload.workflowGraph = graph;
2136
2241
  };
2137
- let finalResultCommitted = false;
2138
2242
  const statusResultState = (): AsyncStatus["state"] | undefined => {
2139
2243
  if (statusPayload.state === "running" || statusPayload.state === "queued") return undefined;
2140
2244
  return statusPayload.state;
@@ -2196,14 +2300,15 @@ async function runSubagent(
2196
2300
  }), (filePath, payload) => writePendingAsyncResultFile(filePath, payload as Record<string, unknown>));
2197
2301
  };
2198
2302
  const writeStatusPayloadNow = (): void => {
2303
+ if (finalResultPublication) return;
2199
2304
  refreshWorkflowGraph();
2200
2305
  writeRecoverableStatusResult();
2201
- runPersistence.write(statusPath, statusPayload);
2306
+ runPersistence.write(statusPath, { ...statusPayload });
2202
2307
  emitNestedSelfEvent(statusPayload.state === "running" || statusPayload.state === "queued" ? "subagent.nested.updated" : "subagent.nested.completed");
2203
2308
  };
2204
2309
  const statusWriteCoalescer = createFileCoalescer(writeStatusPayloadNow, 100);
2205
2310
  const writeStatusPayload = (immediate = true): void => {
2206
- if (immediate || statusPayload.state !== "running" || statusPayload.activityState !== undefined) {
2311
+ if (immediate || statusPayload.state !== "running") {
2207
2312
  if (!statusWriteCoalescer.flush(statusPath)) writeStatusPayloadNow();
2208
2313
  return;
2209
2314
  }
@@ -2859,6 +2964,7 @@ async function runSubagent(
2859
2964
  const updateStepFromChildEvent = (flatIndex: number, event: ChildEvent): void => {
2860
2965
  const step = statusPayload.steps[flatIndex];
2861
2966
  if (!step) return;
2967
+ const previousActivityState = step.activityState;
2862
2968
  const now = Date.now();
2863
2969
  statusPayload.currentStep = flatIndex;
2864
2970
  if (isChildWatchdogStatusEvent(event)) {
@@ -2993,6 +3099,10 @@ async function runSubagent(
2993
3099
  appendRecentStepOutput(step, stripAcceptanceReport(extractTextFromContent(event.message.content)).split("\n").slice(-10));
2994
3100
  step.turnCount = (step.turnCount ?? 0) + 1;
2995
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
+ }
2996
3106
  if (usage) {
2997
3107
  const input = usage.input ?? usage.inputTokens ?? 0;
2998
3108
  const output = usage.output ?? usage.outputTokens ?? 0;
@@ -3012,7 +3122,8 @@ async function runSubagent(
3012
3122
  statusPayload.lastActivityAt = now;
3013
3123
  statusPayload.lastUpdate = now;
3014
3124
  maybeEmitActiveLongRunning(flatIndex, now);
3015
- writeStatusPayload(false);
3125
+ // A sibling may keep aggregate attention unchanged; publish this step's transition.
3126
+ writeStatusPayload(step.activityState !== previousActivityState);
3016
3127
  };
3017
3128
  const updateRunnerActivityState = (now: number): boolean => {
3018
3129
  if (!controlConfig.enabled) return false;
@@ -3084,10 +3195,55 @@ async function runSubagent(
3084
3195
  activityTimer.unref?.();
3085
3196
  }
3086
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);
3087
3242
  const interruptRunner = () => {
3088
3243
  consumeInterruptRequest(asyncDir);
3089
3244
  if (interrupted || statusPayload.state !== "running") return;
3090
3245
  interrupted = true;
3246
+ setupInterruptController.abort();
3091
3247
  const now = Date.now();
3092
3248
  statusPayload.state = "paused";
3093
3249
  currentActivityState = undefined;
@@ -3321,14 +3477,14 @@ async function runSubagent(
3321
3477
  cwd,
3322
3478
  signal: combinedAbortSignal([timeoutAbortController.signal, stopAbortController.signal]),
3323
3479
  abortMessage: stopAbortController.signal.aborted ? stopMessage : timeoutMessage ?? "Subagent timed out.",
3324
- reportOptional: isAgentContractV1(step.agentContract),
3480
+ reportOptional: isAgentContract(step.agentContract),
3325
3481
  }))
3326
3482
  : undefined;
3327
3483
  const groupStopped = stopped || stopAbortController.signal.aborted;
3328
3484
  const groupTimedOut = !groupStopped && (timedOut || timeoutAbortController.signal.aborted);
3329
3485
  const effectiveGroupAcceptance = groupTimedOut || groupStopped ? undefined : groupAcceptance;
3330
3486
  if (placeholder && effectiveGroupAcceptance) placeholder.acceptance = effectiveGroupAcceptance;
3331
- 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;
3332
3488
  if (groupTimedOut || groupStopped || groupAcceptanceFailure) {
3333
3489
  const errorMessage = groupStopped ? stopMessage : groupTimedOut ? timeoutMessage ?? "Subagent timed out." : groupAcceptanceFailure!;
3334
3490
  statusPayload.state = groupStopped ? "stopped" : "failed";
@@ -3545,7 +3701,8 @@ async function runSubagent(
3545
3701
  onExternalProcess: (process) => updateExternalProcess(fi, process),
3546
3702
  onExternalJob: (externalJob) => updateExternalJob(fi, externalJob),
3547
3703
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(fi),
3548
- usageBudgetExhausted: () => refreshUsageBudget()?.exhausted === true,
3704
+ usageBudgetExhausted: continuationUsageBudgetExhausted,
3705
+ usageBudget: config.usageBudget,
3549
3706
  orcaProgressTab,
3550
3707
  }), config.deadlineAt);
3551
3708
  const taskEndTime = Date.now();
@@ -3661,7 +3818,7 @@ async function runSubagent(
3661
3818
  const failures = parallelResults.filter((result) => result.exitCode !== 0 && result.exitCode !== -1);
3662
3819
  const acceptanceFailures = parallelResults
3663
3820
  .map((result, originalIndex) => ({ result, originalIndex, task: dynamicSteps[originalIndex] }))
3664
- .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");
3665
3822
  if (acceptanceFailures.length > 0) {
3666
3823
  const message = acceptanceFailures
3667
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"}`)
@@ -3691,13 +3848,13 @@ async function runSubagent(
3691
3848
  cwd,
3692
3849
  signal: combinedAbortSignal([timeoutAbortController.signal, stopAbortController.signal]),
3693
3850
  abortMessage: stopAbortController.signal.aborted ? stopMessage : timeoutMessage ?? "Subagent timed out.",
3694
- reportOptional: isAgentContractV1(step.agentContract),
3851
+ reportOptional: isAgentContract(step.agentContract),
3695
3852
  }))
3696
3853
  : undefined;
3697
3854
  const groupStopped = stopped || stopAbortController.signal.aborted;
3698
3855
  const groupTimedOut = !groupStopped && (timedOut || timeoutAbortController.signal.aborted);
3699
3856
  const effectiveGroupAcceptance = groupTimedOut || groupStopped ? undefined : groupAcceptance;
3700
- 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;
3701
3858
  const groupError = groupStopped ? stopMessage : groupTimedOut ? timeoutMessage ?? "Subagent timed out." : groupAcceptanceFailure;
3702
3859
  markDynamicGraphGroup(stepIndex, groupError ? groupStopped ? "stopped" : "failed" : "completed", groupError, effectiveGroupAcceptance);
3703
3860
  if (groupError) {
@@ -3776,7 +3933,9 @@ async function runSubagent(
3776
3933
  break;
3777
3934
  }
3778
3935
  try {
3779
- 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,
3780
3939
  agents: group.parallel.map((task) => task.agent),
3781
3940
  labels: group.parallel.map((task) => task.lane?.key ?? config.workflowKey ?? task.outputName ?? task.label),
3782
3941
  tasks: group.parallel.map((task) => task.task),
@@ -3787,9 +3946,10 @@ async function runSubagent(
3787
3946
  ? omitUndefinedProperties({ hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs })
3788
3947
  : undefined,
3789
3948
  baseDir: config.worktreeBaseDir,
3790
- beforeCreate: (plannedSetup) => {
3791
- for (const worktree of plannedSetup.worktrees) setStatusWorktreeReference(requiredStatusStep(statusPayload, groupStartFlatIndex + worktree.index), worktree);
3792
- 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({
3793
3953
  manifestPath: parallelHandoffPath(asyncDir),
3794
3954
  runId: id,
3795
3955
  mode: (config.resultMode ?? statusPayload.mode) === "parallel" ? "parallel" : "chain",
@@ -3797,16 +3957,25 @@ async function runSubagent(
3797
3957
  cwd,
3798
3958
  stepIndex,
3799
3959
  flatStartIndex: groupStartFlatIndex,
3800
- setup: plannedSetup,
3960
+ progress,
3801
3961
  laneBindings: handoffWorkflowKey || config.lane ? [{ index: groupStartFlatIndex, taskIndex: 0, ...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}), ...(handoffChildRunId ? { runId: handoffChildRunId } : {}), ...(config.lane ? { lane: config.lane } : {}) }] : undefined,
3802
3962
  });
3963
+ if (!pendingHandoff) return;
3803
3964
  statusPayload.parallelHandoff = pendingHandoff;
3804
3965
  statusPayload.lastUpdate = Date.now();
3805
3966
  writeStatusPayload();
3806
3967
  },
3807
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.");
3808
3971
  } catch (error) {
3972
+ if (worktreeSetup) await cleanupRemainingWorktree(worktreeSetup, stepIndex, groupStartFlatIndex);
3973
+ if (config.deadlineAt !== undefined && Date.now() >= config.deadlineAt) timeoutRunner();
3809
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
+ }
3810
3979
  const failedAt = Date.now();
3811
3980
  markParallelGroupSetupFailure({
3812
3981
  statusPayload,
@@ -3942,7 +4111,8 @@ async function runSubagent(
3942
4111
  onExternalProcess: (process) => updateExternalProcess(fi, process),
3943
4112
  onExternalJob: (externalJob) => updateExternalJob(fi, externalJob),
3944
4113
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(fi),
3945
- usageBudgetExhausted: () => refreshUsageBudget()?.exhausted === true,
4114
+ usageBudgetExhausted: continuationUsageBudgetExhausted,
4115
+ usageBudget: config.usageBudget,
3946
4116
  orcaProgressTab,
3947
4117
  }), config.deadlineAt);
3948
4118
  if (task.sessionFile) {
@@ -4120,47 +4290,50 @@ async function runSubagent(
4120
4290
  })),
4121
4291
  );
4122
4292
  if (worktreeSetup) {
4123
- const captured = captureParallelWorktreeDiffs(worktreeSetup, asyncDir, stepIndex, group);
4124
- if (captured.summary) previousOutput = `${previousOutput}\n\n${captured.summary}`;
4293
+ const setup = worktreeSetup;
4125
4294
  worktreeFinalized = true;
4126
- const manifestPath = parallelHandoffPath(asyncDir);
4127
- const handoff = {
4128
- manifestPath,
4129
- runId: id,
4130
- mode: (config.resultMode ?? statusPayload.mode) === "parallel" ? "parallel" as const : "chain" as const,
4131
- source: "async" as const,
4132
- cwd,
4133
- stepIndex,
4134
- flatStartIndex: groupStartFlatIndex,
4135
- setup: worktreeSetup,
4136
- diffs: captured.diffs,
4137
- results: parallelResults.map((result) => ({
4138
- agent: result.agent,
4139
- ...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}),
4140
- ...(handoffChildRunId ? { runId: handoffChildRunId } : {}),
4141
- ...(config.lane ? { lane: config.lane } : {}),
4142
- status: result.stopped || (result.exitCode !== 0 && isUnexplainedProcessSignal(omitUndefinedProperties({
4143
- processSignal: result.processSignal,
4144
- interrupted: result.interrupted,
4145
- timedOut: result.timedOut,
4146
- stopped: result.stopped,
4147
- }))) ? "stopped" as const : result.interrupted ? "paused" as const : result.exitCode === 0 ? "completed" as const : "failed" as const,
4148
- summary: result.output || result.error || "(no output)",
4149
- ...(result.artifactPaths?.outputPath ? { outputPath: result.artifactPaths.outputPath } : {}),
4150
- ...(result.structuredOutput !== undefined ? { structuredOutput: result.structuredOutput } : {}),
4151
- ...(result.structuredOutputPath ? { structuredOutputPath: result.structuredOutputPath } : {}),
4152
- ...(result.sessionFile ? { sessionPath: result.sessionFile } : {}),
4153
- })),
4154
- };
4155
- try {
4156
- writeParallelHandoffGroup(handoff);
4157
- const cleanup = cleanupWorktrees(worktreeSetup, { kind: "preserve", capturedDiffs: captured.diffs, handoffManifestPath: manifestPath });
4158
- statusPayload.parallelHandoff = writeParallelHandoffGroup({ ...handoff, cleanup });
4159
- previousOutput = `${previousOutput}\n\n${formatParallelHandoffReference(statusPayload.parallelHandoff)}`;
4160
- } catch (error) {
4161
- previousOutput = `${previousOutput}\n\n${formatParallelHandoffError(error)}`;
4162
- }
4163
- 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
+ });
4164
4337
  }
4165
4338
 
4166
4339
  appendJsonl(eventsPath, JSON.stringify({
@@ -4169,12 +4342,12 @@ async function runSubagent(
4169
4342
  runId: id,
4170
4343
  stepIndex,
4171
4344
  success: parallelResults.every((r) => r.exitCode === 0 || r.exitCode === -1)
4172
- && 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")),
4173
4346
  }));
4174
4347
 
4175
4348
  const acceptanceGateFailure = parallelResults
4176
4349
  .map((result, index) => ({ result, index, task: group.parallel[index] }))
4177
- .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");
4178
4351
  if (acceptanceGateFailure) {
4179
4352
  statusPayload.error = (acceptanceGateFailure.result.acceptance ? acceptanceFailureMessage(acceptanceGateFailure.result.acceptance) : undefined) ?? "Parallel acceptance gate rejected the step.";
4180
4353
  writeStatusPayload();
@@ -4184,7 +4357,7 @@ async function runSubagent(
4184
4357
  break;
4185
4358
  }
4186
4359
  } finally {
4187
- if (worktreeSetup && !worktreeFinalized) cleanupWorktrees(worktreeSetup);
4360
+ if (worktreeSetup && !worktreeFinalized) await cleanupRemainingWorktree(worktreeSetup, stepIndex, groupStartFlatIndex);
4188
4361
  }
4189
4362
  } else {
4190
4363
  const seqStep = step as SubagentStep;
@@ -4211,7 +4384,9 @@ async function runSubagent(
4211
4384
  let singleWorktreeSetup: WorktreeSetup | undefined;
4212
4385
  if (seqStep.worktree) {
4213
4386
  try {
4214
- 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,
4215
4390
  agents: [seqStep.agent],
4216
4391
  labels: [seqStep.lane?.key ?? config.workflowKey ?? seqStep.outputName ?? seqStep.label],
4217
4392
  tasks: [seqStep.task],
@@ -4222,12 +4397,13 @@ async function runSubagent(
4222
4397
  ? omitUndefinedProperties({ hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs })
4223
4398
  : undefined,
4224
4399
  baseDir: config.worktreeBaseDir,
4225
- beforeCreate: (plannedSetup) => {
4226
- const worktree = plannedSetup.worktrees[0];
4400
+ onProgress: (progress) => {
4401
+ publishSetupUnknown(progress);
4402
+ const worktree = progress.setup.worktrees[0];
4227
4403
  if (worktree) {
4228
4404
  setStatusWorktreeReference(requiredStatusStep(statusPayload, flatIndex), worktree);
4229
4405
  }
4230
- const pendingHandoff = writePendingParallelHandoff({
4406
+ const pendingHandoff = writeWorktreeSetupHandoff({
4231
4407
  manifestPath: parallelHandoffPath(asyncDir),
4232
4408
  runId: id,
4233
4409
  mode: "single",
@@ -4235,19 +4411,47 @@ async function runSubagent(
4235
4411
  cwd,
4236
4412
  stepIndex,
4237
4413
  flatStartIndex: flatIndex,
4238
- setup: plannedSetup,
4414
+ progress,
4239
4415
  laneBindings: handoffWorkflowKey || config.lane ? [{ index: flatIndex, taskIndex: 0, ...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}), ...(handoffChildRunId ? { runId: handoffChildRunId } : {}), ...(config.lane ? { lane: config.lane } : {}) }] : undefined,
4240
4416
  });
4417
+ if (!pendingHandoff) return;
4241
4418
  statusPayload.parallelHandoff = pendingHandoff;
4242
4419
  statusPayload.lastUpdate = Date.now();
4243
4420
  writeStatusPayload();
4244
4421
  },
4245
4422
  }));
4246
4423
  } catch (error) {
4247
- if (singleWorktreeSetup) cleanupWorktrees(singleWorktreeSetup);
4248
- 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;
4249
4442
  }
4250
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
+ }
4251
4455
  const singleCwd = singleWorktreeSetup?.worktrees[0]?.agentCwd ?? cwd;
4252
4456
  const stepStartTime = Date.now();
4253
4457
  statusPayload.currentStep = flatIndex;
@@ -4306,11 +4510,12 @@ async function runSubagent(
4306
4510
  onExternalProcess: (process) => updateExternalProcess(flatIndex, process),
4307
4511
  onExternalJob: (externalJob) => updateExternalJob(flatIndex, externalJob),
4308
4512
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(flatIndex),
4309
- usageBudgetExhausted: () => refreshUsageBudget()?.exhausted === true,
4513
+ usageBudgetExhausted: continuationUsageBudgetExhausted,
4514
+ usageBudget: config.usageBudget,
4310
4515
  orcaProgressTab,
4311
4516
  }), config.deadlineAt);
4312
4517
  } catch (error) {
4313
- if (singleWorktreeSetup) cleanupWorktrees(singleWorktreeSetup);
4518
+ if (singleWorktreeSetup) await cleanupRemainingWorktree(singleWorktreeSetup, stepIndex, flatIndex);
4314
4519
  throw error;
4315
4520
  }
4316
4521
  if (seqStep.sessionFile) {
@@ -4342,6 +4547,7 @@ async function runSubagent(
4342
4547
  totalCost: singleResult.totalCost,
4343
4548
  usage: singleResult.usage,
4344
4549
  artifactPaths: singleResult.artifactPaths,
4550
+ savedOutputPath: singleResult.savedOutputPath,
4345
4551
  transcriptPath: singleResult.transcriptPath,
4346
4552
  transcriptError: singleResult.transcriptError,
4347
4553
  effects: singleResult.effects,
@@ -4462,48 +4668,51 @@ async function runSubagent(
4462
4668
  }));
4463
4669
  if (stopped || childStopped) appendTerminalChildStatusEvent(flatIndex, stepEndTime);
4464
4670
  if (singleWorktreeSetup && !singleResult.detached) {
4465
- const diffs = diffWorktrees(singleWorktreeSetup, [seqStep.agent], path.join(asyncDir, "worktree-diffs", `step-${stepIndex}`));
4466
- const diffSummary = formatWorktreeDiffSummary(diffs);
4467
- const manifestPath = parallelHandoffPath(asyncDir);
4468
- const handoff = {
4469
- manifestPath,
4470
- runId: id,
4471
- mode: "single" as const,
4472
- source: "async" as const,
4473
- cwd,
4474
- stepIndex,
4475
- flatStartIndex: flatIndex,
4476
- setup: singleWorktreeSetup,
4477
- diffs,
4478
- results: [{
4479
- agent: singleResult.agent,
4480
- ...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}),
4481
- ...(handoffChildRunId ? { runId: handoffChildRunId } : {}),
4482
- ...(config.lane ? { lane: config.lane } : {}),
4483
- status: singleResult.stopped ? "stopped" as const : singleResult.interrupted ? "paused" as const : singleResult.exitCode === 0 ? "completed" as const : "failed" as const,
4484
- summary: singleResult.output || singleResult.error || "(no output)",
4485
- ...(singleResult.artifactPaths?.outputPath ? { outputPath: singleResult.artifactPaths.outputPath } : {}),
4486
- ...(singleResult.structuredOutput !== undefined ? { structuredOutput: singleResult.structuredOutput } : {}),
4487
- ...(singleResult.structuredOutputPath ? { structuredOutputPath: singleResult.structuredOutputPath } : {}),
4488
- ...(singleResult.sessionFile ? { sessionPath: singleResult.sessionFile } : {}),
4489
- }],
4490
- };
4491
- try {
4492
- writeParallelHandoffGroup(handoff);
4493
- const cleanup = cleanupWorktrees(singleWorktreeSetup, {
4494
- kind: "preserve",
4495
- capturedDiffs: diffs,
4496
- handoffManifestPath: manifestPath,
4497
- ...(config.parentWorkflowRunId && singleResult.sessionFile && fs.existsSync(singleResult.sessionFile) && !singleResult.stopped
4498
- ? { cleanupBlocker: "retained child resume requires managed worktree cwd" }
4499
- : {}),
4500
- });
4501
- statusPayload.parallelHandoff = writeParallelHandoffGroup({ ...handoff, cleanup });
4502
- previousOutput = [previousOutput, diffSummary, formatParallelHandoffReference(statusPayload.parallelHandoff)].filter(Boolean).join("\n\n");
4503
- } catch (error) {
4504
- previousOutput = [previousOutput, diffSummary, formatParallelHandoffError(error)].filter(Boolean).join("\n\n");
4505
- }
4506
- 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
+ });
4507
4716
  }
4508
4717
 
4509
4718
  if (singleResult.completionGuardTriggered) {
@@ -4521,7 +4730,7 @@ async function runSubagent(
4521
4730
  }
4522
4731
 
4523
4732
  flatIndex++;
4524
- if (isAgentContractV1(seqStep.agentContract) && seqStep.gateOn === "acceptance" && singleResult.acceptance?.status === "rejected") {
4733
+ if (isAgentContract(seqStep.agentContract) && seqStep.gateOn === "acceptance" && singleResult.acceptance?.status === "rejected") {
4525
4734
  statusPayload.error = acceptanceFailureMessage(singleResult.acceptance) ?? "Chain acceptance gate rejected the step.";
4526
4735
  writeStatusPayload();
4527
4736
  break;
@@ -4607,6 +4816,11 @@ async function runSubagent(
4607
4816
  stopped: result.stopped,
4608
4817
  })));
4609
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
+ });
4610
4824
  statusPayload.state = stopped || signalTerminated ? "stopped" : timedOut || usageBudgetExceeded ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : partialWithEvidence ? "partial" : "failed";
4611
4825
  closeSteerInbox(asyncDir, statusPayload.state, (filePath, payload) => runPersistence.write(filePath, payload));
4612
4826
  disposeControlInbox();
@@ -4665,6 +4879,9 @@ async function runSubagent(
4665
4879
  statusPayload.error = `Step failed: ${failedStep.agent}`;
4666
4880
  }
4667
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));
4668
4885
  try {
4669
4886
  runPersistence.write(resultPath, {
4670
4887
  lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
@@ -4705,6 +4922,7 @@ async function runSubagent(
4705
4922
  totalCost: r.totalCost,
4706
4923
  usage: r.usage,
4707
4924
  artifactPaths: r.artifactPaths,
4925
+ savedOutputPath: r.savedOutputPath,
4708
4926
  outputSaveError: r.outputSaveError,
4709
4927
  metadataSaveError: r.metadataSaveError,
4710
4928
  truncated: r.truncated,
@@ -4759,13 +4977,16 @@ async function runSubagent(
4759
4977
  ...(taskIndex !== undefined && { taskIndex }),
4760
4978
  ...(totalTasks !== undefined && { totalTasks }),
4761
4979
  }, (filePath, payload) => { writeAsyncResultFile(filePath, payload as Record<string, unknown>); });
4762
- finalResultCommitted = true;
4980
+ // Only capacity deferral releases settled sessions before terminal publication.
4981
+ if (!finalResultCommitted) await Promise.all([publication, disposeChildSessions()]);
4763
4982
  } catch (err) {
4764
4983
  const message = `Failed to write result file ${resultPath}: ${err instanceof Error ? err.message : String(err)}`;
4765
4984
  console.error(message, err);
4766
4985
  statusPayload.state = "failed";
4767
4986
  statusPayload.error = message;
4768
4987
  statusPayload.lastUpdate = Date.now();
4988
+ } finally {
4989
+ finalResultPublication = undefined;
4769
4990
  }
4770
4991
  writeStatusPayload();
4771
4992
  await orcaProgressTab?.finish(statusPayload.state === "complete" ? "completed" : statusPayload.state === "stopped" ? "stopped" : "failed", effectiveSessionFile);
@@ -4803,9 +5024,8 @@ async function runSubagent(
4803
5024
  }), (filePath, content) => runPersistence.write(filePath, { content }, (_path, payload) => {
4804
5025
  fs.writeFileSync(_path, (payload as { content: string }).content, "utf-8");
4805
5026
  }));
4806
- // The run is committed: release the child sessions first, then wait for the
4807
- // storage-capacity retries that may still hold the final status or result.
4808
- 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();
4809
5029
  while (runPersistence.pendingCount() + indexPersistence.pendingCount() > 0) {
4810
5030
  await new Promise((resolve) => setTimeout(resolve, 200));
4811
5031
  }
@@ -4813,7 +5033,7 @@ async function runSubagent(
4813
5033
  indexPersistence.dispose();
4814
5034
  if (config.runnerProcessInstanceId) {
4815
5035
  // Children run inside this process, so no step has writer processes to prove terminal.
4816
- const writers: Record<string, PiWriterProcessInstanceExitV1[]> = {};
5036
+ const writers: Record<string, PiWriterProcessInstanceExit[]> = {};
4817
5037
  const expectedWriters: Record<string, number> = {};
4818
5038
  for (const index of results.keys()) {
4819
5039
  writers[String(index)] = [];
@@ -4949,6 +5169,7 @@ function startConfiguredSubagent(config: SubagentRunConfig): void {
4949
5169
  );
4950
5170
  }
4951
5171
 
5172
+ if (isRunnerEntrypoint) {
4952
5173
  const configArg = process.argv[2];
4953
5174
  if (configArg) {
4954
5175
  try {
@@ -4980,3 +5201,4 @@ if (configArg) {
4980
5201
  }
4981
5202
  });
4982
5203
  }
5204
+ }