pi-subagents 0.58.0 → 0.59.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 (105) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/docs/agents.md +5 -3
  3. package/docs/configuration.md +4 -4
  4. package/docs/extension-api.md +1 -1
  5. package/docs/models.md +24 -1
  6. package/docs/observability.md +1 -1
  7. package/docs/tool-reference.md +89 -4
  8. package/docs/workflows.md +93 -2
  9. package/package.json +3 -1
  10. package/prompts/review-loop.md +2 -2
  11. package/skills/council-mode/SKILL.md +1 -1
  12. package/skills/pi-subagents/SKILL.md +1 -1
  13. package/skills/pi-subagents/references/execution-controls.md +5 -1
  14. package/skills/pi-subagents/references/management-authoring-rpc.md +1 -2
  15. package/skills/pi-subagents/references/prompting-and-roles.md +24 -3
  16. package/src/agents/agent-management.ts +5 -17
  17. package/src/agents/agent-serializer.ts +4 -2
  18. package/src/agents/agents.ts +67 -26
  19. package/src/agents/runtime-agent-registry.ts +9 -16
  20. package/src/api/background-work.ts +5 -1
  21. package/src/api/delegation.ts +0 -7
  22. package/src/api/preflight.ts +6 -9
  23. package/src/extension/fanout-child.ts +5 -3
  24. package/src/extension/index.ts +51 -23
  25. package/src/extension/public-execution.ts +15 -2
  26. package/src/extension/schemas.ts +35 -10
  27. package/src/extension/tool-description.ts +18 -3
  28. package/src/intercom/result-intercom.ts +2 -0
  29. package/src/profiles/profiles.ts +5 -6
  30. package/src/runs/background/active-async-capacity.ts +2 -2
  31. package/src/runs/background/async-execution.ts +50 -35
  32. package/src/runs/background/async-job-tracker.ts +14 -12
  33. package/src/runs/background/async-resume.ts +23 -25
  34. package/src/runs/background/async-status-snapshot.ts +23 -261
  35. package/src/runs/background/async-status.ts +66 -7
  36. package/src/runs/background/chain-append.ts +6 -3
  37. package/src/runs/background/chain-root-attachment.ts +60 -8
  38. package/src/runs/background/fleet-view.ts +21 -11
  39. package/src/runs/background/notify.ts +158 -6
  40. package/src/runs/background/result-files.ts +2 -1
  41. package/src/runs/background/result-watcher.ts +2 -0
  42. package/src/runs/background/resume-guidance.ts +1 -1
  43. package/src/runs/background/retained-children.ts +1 -1
  44. package/src/runs/background/run-status.ts +18 -8
  45. package/src/runs/background/scheduled-runs.ts +86 -7
  46. package/src/runs/background/stale-run-reconciler.ts +10 -4
  47. package/src/runs/background/steering.ts +4 -14
  48. package/src/runs/background/subagent-runner.ts +346 -357
  49. package/src/runs/background/subagent-wait.ts +58 -10
  50. package/src/runs/background/terminal-run-index.ts +1 -1
  51. package/src/runs/background/wait-completions.ts +22 -1
  52. package/src/runs/background/wait-config.ts +23 -9
  53. package/src/runs/background/wait-tool.ts +9 -2
  54. package/src/runs/foreground/async-steering-action.ts +2 -2
  55. package/src/runs/foreground/execution.ts +114 -120
  56. package/src/runs/foreground/foreground-control.ts +3 -0
  57. package/src/runs/foreground/foreground-history.ts +1 -0
  58. package/src/runs/foreground/subagent-executor.ts +478 -239
  59. package/src/runs/foreground/workflow-detach-reconcile.ts +99 -200
  60. package/src/runs/shared/abort-recovery.ts +119 -0
  61. package/src/runs/shared/async-status-projection.ts +463 -0
  62. package/src/runs/shared/child-identity.ts +19 -4
  63. package/src/runs/shared/child-launch-plan.ts +151 -0
  64. package/src/runs/shared/completion-evidence.ts +89 -0
  65. package/src/runs/shared/completion-guard.ts +1 -1
  66. package/src/runs/shared/dynamic-fanout.ts +2 -2
  67. package/src/runs/shared/host-step-status.ts +230 -0
  68. package/src/runs/shared/lane-metadata.ts +105 -0
  69. package/src/runs/shared/mcp-config-sources.ts +42 -6
  70. package/src/runs/shared/mcp-direct-tool-allowlist.ts +93 -143
  71. package/src/runs/shared/mcp-direct-tool-grant.ts +197 -0
  72. package/src/runs/shared/model-fallback.ts +9 -2
  73. package/src/runs/shared/nested-events.ts +6 -2
  74. package/src/runs/shared/nested-render.ts +7 -3
  75. package/src/runs/shared/parallel-handoff.ts +419 -7
  76. package/src/runs/shared/parallel-utils.ts +7 -0
  77. package/src/runs/shared/pi-args.ts +20 -2
  78. package/src/runs/shared/single-output.ts +27 -4
  79. package/src/runs/shared/subagent-prompt-runtime.ts +13 -4
  80. package/src/runs/shared/worktree-cleanup-plan.ts +847 -0
  81. package/src/runs/shared/worktree.ts +18 -0
  82. package/src/shared/child-session-name.ts +46 -0
  83. package/src/shared/extension-context.ts +24 -0
  84. package/src/shared/formatters.ts +5 -2
  85. package/src/shared/launch-contract.ts +1 -1
  86. package/src/shared/settings.ts +9 -103
  87. package/src/shared/types.ts +198 -29
  88. package/src/shared/utils.ts +35 -55
  89. package/src/slash/delegation-adapters.ts +1 -8
  90. package/src/slash/delegation-request.ts +0 -4
  91. package/src/slash/slash-bridge.ts +1 -2
  92. package/src/slash/slash-commands.ts +369 -90
  93. package/src/slash/slash-live-state.ts +22 -11
  94. package/src/tui/fleet-status.ts +125 -74
  95. package/src/tui/fleet.ts +11 -5
  96. package/src/tui/render.ts +316 -27
  97. package/src/watchdog/turn-delta.ts +1 -1
  98. package/src/workflows/chat-progress.ts +6 -3
  99. package/src/workflows/host-command.ts +230 -0
  100. package/src/workflows/scripted-workflow.ts +452 -38
  101. package/src/workflows/workflow-child-summary.ts +9 -5
  102. package/src/workflows/workflow-preflight.ts +270 -0
  103. package/src/workflows/workflow-receipt.ts +43 -4
  104. package/src/workflows/workflow-settlement.ts +246 -0
  105. package/src/runs/shared/turn-budget.ts +0 -98
@@ -36,25 +36,21 @@ import {
36
36
  type NestedRouteInfo,
37
37
  type NestedRunSummary,
38
38
  type ResolvedControlConfig,
39
- type ResolvedTurnBudget,
40
39
  type ResolvedToolBudget,
41
40
  type RunFanoutBudgetDescriptor,
42
41
  type SubagentRunMode,
43
42
  type SubagentOutputState,
44
43
  type UsageBudgetConfig,
45
44
  type ToolBudgetState,
46
- type TurnBudgetState,
47
45
  type Usage,
48
46
  type WorkflowGraphSnapshot,
49
- type SteeringStatus,
50
47
  type SteeringTargetState,
51
48
  type SteeringTargetStatus,
52
49
  type SubagentChildStatusEvent,
53
- type SettlementDiagnostic,
50
+ type WorkflowLaneMetadata,
54
51
  DEFAULT_MAX_OUTPUT,
55
52
  type MaxOutputConfig,
56
53
  SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
57
- POLL_INTERVAL_MS,
58
54
  truncateOutput,
59
55
  getSubagentDepthEnv,
60
56
  } from "../../shared/types.ts";
@@ -80,6 +76,7 @@ import {
80
76
  Semaphore,
81
77
  } from "../shared/parallel-utils.ts";
82
78
  import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan, type SubagentTaskDelivery } from "../shared/pi-args.ts";
79
+ import { deriveChildSessionName } from "../../shared/child-session-name.ts";
83
80
  import { readRuntimeAcknowledgedExtensions } from "../shared/runtime-acknowledged-extensions.ts";
84
81
  import { outputEntryFromAsyncResult, resolveOutputReferences } from "../shared/chain-outputs.ts";
85
82
  import { createStructuredOutputRuntime, MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput, readStructuredOutputAcceptanceReport } from "../shared/structured-output.ts";
@@ -102,8 +99,10 @@ import { markProcessTerminalCandidateLeaseRelease, writeProcessTerminalCandidate
102
99
  import { createOwnedProcessTreeController, type OwnedProcessTreeController } from "./owned-process-tree.ts";
103
100
  import { createSteeringStatus, recordSteeringRequest, steeringStatus, terminalSteeringNoticeState, updateSteeringTarget } from "./steering.ts";
104
101
  import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
105
- import { PROMPT_REDACTED, detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput, hasEmptyTerminalAssistantResponse, readStatus } from "../../shared/utils.ts";
102
+ import { PROMPT_REDACTED, detectSubagentError, extractTextFromContent, extractToolArgsPreview, formatEmptyTerminalAssistantResponseError, getFinalOutput, hasEmptyTerminalAssistantResponse, readStatus } from "../../shared/utils.ts";
106
103
  import { evaluateCompletionMutationGuard, expectsImplementationMutation, hasMutationToolCapability, validateImplementationToolContract } from "../shared/completion-guard.ts";
104
+ import { planCompletionEvidence, projectSettlementDiagnostic } from "../shared/completion-evidence.ts";
105
+ import { planAbortRecovery } from "../shared/abort-recovery.ts";
107
106
  import {
108
107
  createMutatingFailureState,
109
108
  didMutatingToolFail,
@@ -137,7 +136,6 @@ import { waitForImportedAsyncRoot } from "./chain-root-attachment.ts";
137
136
  import { normalizeExtensionBindings } from "../shared/extension-bindings.ts";
138
137
  import { appendRunnerStepsToStatus, consumeChainAppendRequests, countPendingChainAppendRequests, statusStepDescription } from "./chain-append.ts";
139
138
  import { asyncStatusChildIdentity } from "../shared/child-identity.ts";
140
- import { appendTurnBudgetSystemPrompt, formatTurnBudgetOutput, initialTurnBudgetState, turnBudgetDecision, turnBudgetDeferredNote, turnBudgetDeferredState, turnBudgetExceededMessage, turnBudgetSoftNote, turnBudgetState } from "../shared/turn-budget.ts";
141
139
  import { initialToolBudgetState, toolBudgetState } from "../shared/tool-budget.ts";
142
140
  import { effectiveToolTimeoutMs, formatToolTimeoutMessage, toolTimeoutCallKey } from "../shared/tool-timeout.ts";
143
141
  import { usageBudgetExceededMessage, usageBudgetState } from "../shared/usage-budget.ts";
@@ -199,7 +197,6 @@ interface SubagentRunConfig {
199
197
  deadlineAt?: number;
200
198
  /** Resolved configured hard per-tool-call timeout (ms); fast tools still have a default when undefined. */
201
199
  toolTimeoutMs?: number;
202
- turnBudget?: ResolvedTurnBudget;
203
200
  toolBudget?: ResolvedToolBudget;
204
201
  usageBudget?: UsageBudgetConfig;
205
202
  revivalLease?: SessionLeaseRequest;
@@ -215,10 +212,13 @@ interface SubagentRunConfig {
215
212
  launchBarrierToken?: string;
216
213
  parentWorkflowRunId?: string;
217
214
  workflowKey?: string;
215
+ lane?: WorkflowLaneMetadata;
218
216
  }
219
217
 
220
218
  interface StepResult {
221
219
  agent: string;
220
+ /** Human-readable display name for the child session, when derived at launch. */
221
+ sessionName?: string;
222
222
  context?: "fresh" | "fork";
223
223
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
224
224
  capabilityAudit?: import("../shared/capability-ceiling.ts").SubagentCapabilityAudit;
@@ -239,9 +239,6 @@ interface StepResult {
239
239
  stopped?: boolean;
240
240
  processSignal?: string | null;
241
241
  timeoutRecovery?: import("../../shared/types.ts").TimeoutRecoverySummary;
242
- turnBudget?: TurnBudgetState;
243
- turnBudgetExceeded?: boolean;
244
- wrapUpRequested?: boolean;
245
242
  toolBudget?: ToolBudgetState;
246
243
  toolBudgetBlocked?: boolean;
247
244
  sessionFile?: string;
@@ -429,6 +426,23 @@ function costSummaryFromAttempts(attempts: ModelAttempt[] | undefined): CostSumm
429
426
  : undefined;
430
427
  }
431
428
 
429
+ function usageFromAttempts(attempts: ModelAttempt[] | undefined): Usage | undefined {
430
+ if (!attempts || attempts.length === 0) return undefined;
431
+ const usage = emptyUsage();
432
+ for (const attempt of attempts) {
433
+ if (!attempt.usage) continue;
434
+ usage.input += attempt.usage.input;
435
+ usage.output += attempt.usage.output;
436
+ usage.cacheRead += attempt.usage.cacheRead;
437
+ usage.cacheWrite += attempt.usage.cacheWrite;
438
+ usage.cost += attempt.usage.cost;
439
+ usage.turns += attempt.usage.turns;
440
+ }
441
+ return usage.input !== 0 || usage.output !== 0 || usage.cacheRead !== 0 || usage.cacheWrite !== 0 || usage.cost !== 0 || usage.turns !== 0
442
+ ? usage
443
+ : undefined;
444
+ }
445
+
432
446
  function appendRecentStepOutput(step: RunnerStatusStep, lines: string[]): void {
433
447
  const nonEmpty = lines.filter((line) => line.trim());
434
448
  if (nonEmpty.length === 0) return;
@@ -537,9 +551,6 @@ interface RunPiStreamingResult {
537
551
  interrupted?: boolean;
538
552
  timedOut?: boolean;
539
553
  stopped?: boolean;
540
- turnBudget?: TurnBudgetState;
541
- turnBudgetExceeded?: boolean;
542
- wrapUpRequested?: boolean;
543
554
  toolBudget?: ToolBudgetState;
544
555
  toolBudgetBlocked?: boolean;
545
556
  observedMutationAttempt?: boolean;
@@ -556,21 +567,36 @@ interface RunPiStreamingResult {
556
567
  currentToolArgs?: string;
557
568
  currentPath?: string;
558
569
  afterCompactionSettlement?: boolean;
570
+ abortRecoveryDiagnostic?: string;
559
571
  effects?: import("../../shared/types.ts").EffectsProjection;
560
572
  }
561
573
 
562
574
  const MAX_CHILD_FAILURE_DIAGNOSTIC_CHARS = 8_192;
563
575
 
576
+ function formatRequiredOutputError(requiredOutput: {
577
+ kind: "file-only" | "structured";
578
+ path: string;
579
+ missing: boolean;
580
+ } | undefined): string | undefined {
581
+ if (!requiredOutput?.missing) return undefined;
582
+ return `Required ${requiredOutput.kind} output was not produced: ${requiredOutput.path.slice(0, 2_048)}`;
583
+ }
584
+
564
585
  function formatChildFailureDiagnostic(input: {
565
586
  error: string | undefined;
566
587
  afterCompactionSettlement?: boolean;
567
- missingFileOnlyOutput?: string;
568
- missingStructuredOutput?: string;
588
+ abortRecoveryDiagnostic?: string;
589
+ requiredOutput?: {
590
+ kind: "file-only" | "structured";
591
+ path: string;
592
+ missing: boolean;
593
+ };
569
594
  }): string | undefined {
595
+ const missingOutput = formatRequiredOutputError(input.requiredOutput);
570
596
  const notes = [
597
+ input.abortRecoveryDiagnostic,
571
598
  input.afterCompactionSettlement ? "Child failure followed session compaction and agent settlement." : undefined,
572
- input.missingFileOnlyOutput ? `Required file-only output was not produced: ${input.missingFileOnlyOutput.slice(0, 2_048)}` : undefined,
573
- input.missingStructuredOutput ? `Required structured output was not produced: ${input.missingStructuredOutput.slice(0, 2_048)}` : undefined,
599
+ missingOutput && input.error !== missingOutput ? missingOutput : undefined,
574
600
  ].filter((note): note is string => Boolean(note));
575
601
  if (notes.length === 0) return input.error;
576
602
  const context = notes.join("\n");
@@ -595,7 +621,6 @@ function runPiStreaming(
595
621
  timeoutMessage?: string,
596
622
  registerStop?: (stop: (() => void) | undefined) => void,
597
623
  stopMessage?: string,
598
- registerTurnBudgetAbort?: (abort: ((message: string, state?: TurnBudgetState) => void) | undefined) => void,
599
624
  onWriterProcess?: (writer: { state: "none" | "spawning" } | { state: "running"; pid: number }) => void,
600
625
  toolTimeoutMs?: number,
601
626
  runDeadlineAt?: number,
@@ -642,9 +667,6 @@ function runPiStreaming(
642
667
  let interrupted = false;
643
668
  let timedOut = false;
644
669
  let stopped = false;
645
- let turnBudgetExceeded = false;
646
- let turnBudgetMessage: string | undefined;
647
- let turnBudget: TurnBudgetState | undefined;
648
670
  let observedMutationAttempt = false;
649
671
  let structuredOutputToolInvoked = false;
650
672
  let structuredOutputMessageStartIndex: number | undefined;
@@ -747,8 +769,19 @@ function runPiStreaming(
747
769
  appendChildEvent(event as unknown as Record<string, unknown>);
748
770
  transcriptWriter?.writeChildEvent(event);
749
771
  if (event.type === "compaction_start") compactionStartedReceived = true;
772
+ if (event.type === "compaction_end" && event.willRetry === true) {
773
+ compactionStartedReceived = false;
774
+ afterCompactionSettlement = false;
775
+ }
776
+ if (event.type === "agent_start" || event.type === "auto_retry_start") {
777
+ compactionStartedReceived = false;
778
+ afterCompactionSettlement = false;
779
+ }
750
780
  const lifecycleAction = projectChildLifecycle(event, false, childLifecycleState);
751
- if (event.type === "agent_settled" && lifecycleAction === "start-drain") agentSettledReceived = true;
781
+ if (event.type === "agent_settled" && lifecycleAction === "start-drain") {
782
+ agentSettledReceived = true;
783
+ afterCompactionSettlement = compactionStartedReceived;
784
+ }
752
785
  applyChildLifecycle(lifecycleAction);
753
786
 
754
787
  if (isChildWatchdogStatusEvent(event)) {
@@ -856,11 +889,10 @@ function runPiStreaming(
856
889
  let cleanTerminalAssistantStopReceived = false;
857
890
  let agentSettledReceived = false;
858
891
  let compactionStartedReceived = false;
892
+ let afterCompactionSettlement = false;
859
893
  let finalDrainTimer: NodeJS.Timeout | undefined;
860
894
  let finalHardKillTimer: NodeJS.Timeout | undefined;
861
895
  let watchdogTailTimer: NodeJS.Timeout | undefined;
862
- let turnBudgetTerminationTimer: NodeJS.Timeout | undefined;
863
- let turnBudgetHardKillTimer: NodeJS.Timeout | undefined;
864
896
  let protocolHardKillTimer: NodeJS.Timeout | undefined;
865
897
  let protocolError: ProtocolOutputLimit | undefined;
866
898
  let settled = false;
@@ -981,23 +1013,6 @@ function runPiStreaming(
981
1013
  if (processTreeController) void processTreeController.terminate();
982
1014
  else trySignalChild(child, "SIGTERM");
983
1015
  });
984
- registerTurnBudgetAbort?.((message, state) => {
985
- if (settled || timedOut || stopped || turnBudgetExceeded) return;
986
- turnBudgetExceeded = true;
987
- turnBudgetMessage = message;
988
- turnBudget = state;
989
- interrupted = false;
990
- error = message;
991
- trySignalChild(child, "SIGINT");
992
- turnBudgetTerminationTimer = setTimeout(() => {
993
- if (!settled && !timedOut && !stopped) trySignalChild(child, "SIGTERM");
994
- }, 1000);
995
- turnBudgetTerminationTimer.unref?.();
996
- turnBudgetHardKillTimer = setTimeout(() => {
997
- if (!settled && !timedOut && !stopped) trySignalChild(child, "SIGKILL");
998
- }, 4000);
999
- turnBudgetHardKillTimer.unref?.();
1000
- });
1001
1016
  const clearDrainTimers = () => {
1002
1017
  clearAllToolTimeouts();
1003
1018
  if (finalDrainTimer) {
@@ -1009,14 +1024,6 @@ function runPiStreaming(
1009
1024
  finalHardKillTimer = undefined;
1010
1025
  }
1011
1026
  clearWatchdogTailTimer();
1012
- if (turnBudgetTerminationTimer) {
1013
- clearTimeout(turnBudgetTerminationTimer);
1014
- turnBudgetTerminationTimer = undefined;
1015
- }
1016
- if (turnBudgetHardKillTimer) {
1017
- clearTimeout(turnBudgetHardKillTimer);
1018
- turnBudgetHardKillTimer = undefined;
1019
- }
1020
1027
  if (protocolHardKillTimer) {
1021
1028
  clearTimeout(protocolHardKillTimer);
1022
1029
  protocolHardKillTimer = undefined;
@@ -1084,7 +1091,6 @@ function runPiStreaming(
1084
1091
  registerInterrupt?.(undefined);
1085
1092
  registerTimeout?.(undefined);
1086
1093
  registerStop?.(undefined);
1087
- registerTurnBudgetAbort?.(undefined);
1088
1094
  clearDrainTimers();
1089
1095
  clearStdioGuard();
1090
1096
  stdoutReader.end();
@@ -1094,32 +1100,32 @@ function runPiStreaming(
1094
1100
  const finalOutput = getFinalOutput(messages) || rawStdoutTail.text().trim();
1095
1101
  const finalError = error ?? assistantError;
1096
1102
  const forcedDrainAfterFinalSuccess = Boolean(forcedTerminationSignal || signal) && (cleanTerminalAssistantStopReceived || agentSettledReceived) && !finalError;
1103
+ const forcedDrainAfterEmptyTerminal = forcedDrainAfterFinalSuccess && hasEmptyTerminalAssistantResponse(messages);
1104
+ const forcedDrainError = forcedDrainAfterEmptyTerminal && stderr.trim()
1105
+ ? stderr.trim()
1106
+ : undefined;
1097
1107
  const signalError = isUnexplainedProcessSignal({
1098
1108
  processSignal: signal,
1099
1109
  interrupted,
1100
1110
  timedOut,
1101
1111
  stopped,
1102
- turnBudgetExceeded,
1103
- forcedDrainAfterFinalSuccess,
1112
+ forcedDrainAfterFinalSuccess: forcedDrainAfterFinalSuccess && !forcedDrainAfterEmptyTerminal,
1104
1113
  }) ? formatProcessSignalError(signal!) : undefined;
1105
1114
  resolve(omitUndefinedProperties({
1106
1115
  stderr,
1107
- exitCode: timedOut || stopped ? 1 : turnBudgetExceeded ? 1 : interrupted || forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
1116
+ exitCode: timedOut || stopped ? 1 : interrupted || (forcedDrainAfterFinalSuccess && !forcedDrainAfterEmptyTerminal) ? 0 : forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
1108
1117
  messages,
1109
1118
  usage,
1110
1119
  toolCount,
1111
1120
  durationMs: Date.now() - startedAt,
1112
1121
  model,
1113
- error: stopped ? (stopMessage ?? "Subagent stopped by user.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : interrupted || forcedDrainAfterFinalSuccess ? undefined : finalError ?? signalError,
1122
+ error: stopped ? (stopMessage ?? "Subagent stopped by user.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : interrupted || (forcedDrainAfterFinalSuccess && !forcedDrainAfterEmptyTerminal) ? undefined : finalError ?? forcedDrainError ?? signalError,
1114
1123
  protocolError,
1115
1124
  finalOutput: (timedOut || stopped) && !finalOutput.trim() ? (stopped ? stopMessage ?? "Subagent stopped by user." : timeoutMessage ?? "Subagent timed out.") : finalOutput,
1116
1125
  outputState: finalOutput.trim() ? "present" : "absent",
1117
1126
  interrupted,
1118
1127
  timedOut,
1119
1128
  stopped,
1120
- turnBudget,
1121
- turnBudgetExceeded,
1122
- wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudget?.outcome === "termination-deferred" || turnBudgetExceeded || undefined,
1123
1129
  observedMutationAttempt,
1124
1130
  structuredOutputToolInvoked,
1125
1131
  structuredOutputMessageStartIndex,
@@ -1131,7 +1137,7 @@ function runPiStreaming(
1131
1137
  currentTool,
1132
1138
  currentToolArgs,
1133
1139
  currentPath,
1134
- afterCompactionSettlement: (compactionStartedReceived && agentSettledReceived) || undefined,
1140
+ afterCompactionSettlement: afterCompactionSettlement || undefined,
1135
1141
  }));
1136
1142
  });
1137
1143
 
@@ -1145,7 +1151,6 @@ function runPiStreaming(
1145
1151
  registerInterrupt?.(undefined);
1146
1152
  registerTimeout?.(undefined);
1147
1153
  registerStop?.(undefined);
1148
- registerTurnBudgetAbort?.(undefined);
1149
1154
  clearDrainTimers();
1150
1155
  clearStdioGuard();
1151
1156
  stdoutReader.end();
@@ -1154,7 +1159,7 @@ function runPiStreaming(
1154
1159
  const stderr = stderrTail.text();
1155
1160
  const finalOutput = getFinalOutput(messages) || rawStdoutTail.text().trim();
1156
1161
  const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
1157
- resolve(omitUndefinedProperties({ stderr, exitCode: 1, messages, usage, toolCount, durationMs: Date.now() - startedAt, model, error: stopped ? (stopMessage ?? "Subagent stopped by user.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : error ?? assistantError ?? spawnErrorMessage, protocolError, finalOutput: (timedOut || stopped) && !finalOutput.trim() ? (stopped ? stopMessage ?? "Subagent stopped by user." : timeoutMessage ?? "Subagent timed out.") : finalOutput, outputState: finalOutput.trim() ? "present" : "absent", timedOut, stopped, turnBudget, turnBudgetExceeded, wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudget?.outcome === "termination-deferred" || turnBudgetExceeded || undefined, observedMutationAttempt, structuredOutputToolInvoked, structuredOutputMessageStartIndex, watchdog: childWatchdogState, processInstanceId, processTree: { state: "unknown", reason: "verification-failed", diagnostic: spawnErrorMessage } }));
1162
+ resolve(omitUndefinedProperties({ stderr, exitCode: 1, messages, usage, toolCount, durationMs: Date.now() - startedAt, model, error: stopped ? (stopMessage ?? "Subagent stopped by user.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : error ?? assistantError ?? spawnErrorMessage, protocolError, finalOutput: (timedOut || stopped) && !finalOutput.trim() ? (stopped ? stopMessage ?? "Subagent stopped by user." : timeoutMessage ?? "Subagent timed out.") : finalOutput, outputState: finalOutput.trim() ? "present" : "absent", timedOut, stopped, observedMutationAttempt, structuredOutputToolInvoked, structuredOutputMessageStartIndex, watchdog: childWatchdogState, processInstanceId, processTree: { state: "unknown", reason: "verification-failed", diagnostic: spawnErrorMessage } }));
1158
1163
  });
1159
1164
  });
1160
1165
  }
@@ -1289,7 +1294,6 @@ interface SingleStepContext {
1289
1294
  registerInterrupt?: (interrupt: (() => void) | undefined) => void;
1290
1295
  registerTimeout?: (interrupt: (() => void) | undefined) => void;
1291
1296
  registerStop?: (stop: (() => void) | undefined) => void;
1292
- registerTurnBudgetAbort?: (abort: ((message: string, state?: TurnBudgetState) => void) | undefined) => void;
1293
1297
  timeoutSignal?: AbortSignal;
1294
1298
  stopSignal?: AbortSignal;
1295
1299
  timeoutMessage?: string;
@@ -1298,7 +1302,6 @@ interface SingleStepContext {
1298
1302
  toolTimeoutMs?: number;
1299
1303
  /** Effective step deadline (Date.now() + effective timeout) when a run budget exists. */
1300
1304
  deadlineAt?: number;
1301
- turnBudget?: ResolvedTurnBudget;
1302
1305
  childIntercomTarget?: string;
1303
1306
  orchestratorIntercomTarget?: string;
1304
1307
  nestedRoute?: NestedRouteInfo;
@@ -1310,6 +1313,7 @@ interface SingleStepContext {
1310
1313
  onExternalProcess?: (process: ExternalProcessStatus) => void;
1311
1314
  onExternalJob?: (status: ExternalJobStatus) => void;
1312
1315
  skipAcceptance?: () => boolean;
1316
+ usageBudgetExhausted?: () => boolean;
1313
1317
  /** False when sibling work in the same Git worktree could have caused the tracked diff. */
1314
1318
  trackedMutationEvidenceForCompletionGuard?: boolean;
1315
1319
  orcaProgressTab?: OrcaProgressTab;
@@ -1378,10 +1382,13 @@ async function runSingleStepInner(
1378
1382
  modelAttempts: imported.modelAttempts,
1379
1383
  contextOverflow: imported.contextOverflow,
1380
1384
  totalCost: imported.totalCost,
1385
+ usage: imported.usage,
1381
1386
  structuredOutput: timedOut || stopped ? undefined : imported.structuredOutput,
1382
1387
  structuredOutputPath: timedOut || stopped ? undefined : imported.structuredOutputPath,
1383
1388
  structuredOutputSchemaPath: timedOut || stopped ? undefined : imported.structuredOutputSchemaPath,
1384
1389
  acceptance: timedOut || stopped ? undefined : imported.acceptance,
1390
+ execution: timedOut || stopped ? undefined : imported.execution,
1391
+ effects: timedOut || stopped ? undefined : imported.effects,
1385
1392
  });
1386
1393
  } finally {
1387
1394
  ctx.registerTimeout?.(undefined);
@@ -1400,6 +1407,7 @@ async function runSingleStepInner(
1400
1407
  if (!step.runner) {
1401
1408
  resolvedTaskToolPlan = resolvePiLaunchToolPlan(omitUndefinedProperties({
1402
1409
  tools: step.tools,
1410
+ allowNestedSubagents: step.allowNestedSubagents,
1403
1411
  extensions: step.extensions,
1404
1412
  subagentOnlyExtensions: step.subagentOnlyExtensions,
1405
1413
  fast: step.fast,
@@ -1438,6 +1446,9 @@ async function runSingleStepInner(
1438
1446
  });
1439
1447
  }
1440
1448
  }
1449
+ // Derive from the pre-acceptance task so internal acceptance/recovery
1450
+ // instructions never leak into the display name.
1451
+ const childSessionName = step.sessionName ?? deriveChildSessionName({ agent: step.agent, task, label: step.label });
1441
1452
  if (step.effectiveAcceptance) {
1442
1453
  const acceptancePrompt = formatAcceptancePrompt(step.effectiveAcceptance, { reportOptional: isAgentContractV1(step.agentContract), structuredOutput: Boolean(step.structuredOutput?.acceptanceReportPath) });
1443
1454
  if (acceptancePrompt) task = `${task}\n${acceptancePrompt}`;
@@ -1501,14 +1512,18 @@ async function runSingleStepInner(
1501
1512
  }));
1502
1513
  try { fs.writeFileSync(ctx.outputFile, external.output, "utf-8"); } catch { /* Observability output is best-effort. */ }
1503
1514
  const resolvedOutput = step.outputPath && external.exitCode === 0
1504
- ? resolveSingleOutput(step.outputPath, external.output, outputSnapshot)
1515
+ ? resolveSingleOutput(step.outputPath, external.output, outputSnapshot, step.outputClaimPath)
1505
1516
  : { fullOutput: external.output };
1506
1517
  const outputReference = resolvedOutput.savedPath ? formatSavedOutputReference(resolvedOutput.savedPath, resolvedOutput.fullOutput) : undefined;
1518
+ const exitCode = resolvedOutput.fatalError ? 1 : external.exitCode;
1519
+ const error = resolvedOutput.fatalError && resolvedOutput.saveError
1520
+ ? external.error ? `${external.error}\n${resolvedOutput.saveError}` : resolvedOutput.saveError
1521
+ : external.error;
1507
1522
  const finalizedOutput = finalizeSingleOutput(omitUndefinedProperties({
1508
1523
  fullOutput: resolvedOutput.fullOutput,
1509
1524
  outputPath: step.outputPath,
1510
1525
  outputMode: step.outputMode,
1511
- exitCode: external.exitCode ?? 1,
1526
+ exitCode: exitCode ?? 1,
1512
1527
  savedPath: resolvedOutput.savedPath,
1513
1528
  outputReference,
1514
1529
  saveError: resolvedOutput.saveError,
@@ -1523,16 +1538,17 @@ async function runSingleStepInner(
1523
1538
  : {};
1524
1539
  return omitUndefinedProperties({
1525
1540
  agent: step.agent,
1541
+ ...(childSessionName ? { sessionName: childSessionName } : {}),
1526
1542
  context: step.context,
1527
1543
  output: finalizedOutput.displayOutput,
1528
1544
  outputState: external.output.trim() ? "present" : "absent",
1529
- exitCode: external.exitCode,
1530
- error: external.error,
1545
+ exitCode,
1546
+ error,
1531
1547
  timedOut: external.timedOut,
1532
1548
  stopped: external.stopped,
1533
1549
  processSignal: external.processSignal,
1534
1550
  artifactPaths,
1535
- outputSaveError: artifactErrors.outputSaveError,
1551
+ outputSaveError: [resolvedOutput.saveError, artifactErrors.outputSaveError].filter(Boolean).join("\n") || undefined,
1536
1552
  metadataSaveError: artifactErrors.metadataSaveError,
1537
1553
  runner,
1538
1554
  externalProcess: external.externalProcess,
@@ -1566,14 +1582,18 @@ async function runSingleStepInner(
1566
1582
  }));
1567
1583
  try { fs.writeFileSync(ctx.outputFile, external.output, "utf-8"); } catch { /* Observability output is best-effort. */ }
1568
1584
  const resolvedOutput = step.outputPath && external.exitCode === 0
1569
- ? resolveSingleOutput(step.outputPath, external.output, outputSnapshot)
1585
+ ? resolveSingleOutput(step.outputPath, external.output, outputSnapshot, step.outputClaimPath)
1570
1586
  : { fullOutput: external.output };
1571
1587
  const outputReference = resolvedOutput.savedPath ? formatSavedOutputReference(resolvedOutput.savedPath, resolvedOutput.fullOutput) : undefined;
1588
+ const exitCode = resolvedOutput.fatalError ? 1 : external.exitCode;
1589
+ const error = resolvedOutput.fatalError && resolvedOutput.saveError
1590
+ ? external.error ? `${external.error}\n${resolvedOutput.saveError}` : resolvedOutput.saveError
1591
+ : external.error;
1572
1592
  const finalizedOutput = finalizeSingleOutput(omitUndefinedProperties({
1573
1593
  fullOutput: resolvedOutput.fullOutput,
1574
1594
  outputPath: step.outputPath,
1575
1595
  outputMode: step.outputMode,
1576
- exitCode: external.exitCode,
1596
+ exitCode: exitCode ?? 1,
1577
1597
  savedPath: resolvedOutput.savedPath,
1578
1598
  outputReference,
1579
1599
  saveError: resolvedOutput.saveError,
@@ -1588,15 +1608,16 @@ async function runSingleStepInner(
1588
1608
  : {};
1589
1609
  return omitUndefinedProperties({
1590
1610
  agent: step.agent,
1611
+ ...(childSessionName ? { sessionName: childSessionName } : {}),
1591
1612
  context: step.context,
1592
1613
  output: finalizedOutput.displayOutput,
1593
1614
  outputState: external.output.trim() ? "present" : "absent",
1594
- exitCode: external.exitCode,
1595
- error: external.error,
1615
+ exitCode,
1616
+ error,
1596
1617
  timedOut: external.timedOut,
1597
1618
  stopped: external.stopped,
1598
1619
  artifactPaths,
1599
- outputSaveError: artifactErrors.outputSaveError,
1620
+ outputSaveError: [resolvedOutput.saveError, artifactErrors.outputSaveError].filter(Boolean).join("\n") || undefined,
1600
1621
  metadataSaveError: artifactErrors.metadataSaveError,
1601
1622
  runner,
1602
1623
  externalJob: external.externalJob,
@@ -1625,7 +1646,6 @@ async function runSingleStepInner(
1625
1646
  let structuredAcceptanceReport: unknown;
1626
1647
  let structuredAcceptanceReportError: string | undefined;
1627
1648
  let completionGuardTriggeredFinal = false;
1628
- let turnBudget = ctx.turnBudget ? initialTurnBudgetState(ctx.turnBudget) : undefined;
1629
1649
  let toolBudget = step.toolBudget ? initialToolBudgetState(step.toolBudget) : undefined;
1630
1650
  let toolBudgetBlocked = false;
1631
1651
  let actualLaunchContractDigest = step.launchContractDigest;
@@ -1639,8 +1659,12 @@ async function runSingleStepInner(
1639
1659
  let taskDeliveryOverride: SubagentTaskDelivery | undefined;
1640
1660
  let contextOverflow = false;
1641
1661
  let launchWarningsEmitted = false;
1662
+ let abortRecoveryAttempted = false;
1663
+ let nextAttemptTask = task;
1642
1664
  modelAttemptsLoop: while (modelIndex < candidates.length) {
1643
1665
  if (ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break;
1666
+ const recoveringAbort = abortRecoveryAttempted;
1667
+ const attemptTask = nextAttemptTask;
1644
1668
  const candidate = candidates[modelIndex];
1645
1669
  const expectedModelForVerification = candidate && !(step.skipPrimaryModelVerification && modelIndex === 0) ? candidate : undefined;
1646
1670
  try {
@@ -1675,7 +1699,7 @@ async function runSingleStepInner(
1675
1699
  const { args, env, tempDir, toolDiagnosticPath, runtimeAcknowledgedExtensionsPath, capabilityAudit: attemptCapabilityAudit, warnings } = buildPiArgs(omitUndefinedProperties({
1676
1700
  parentSessionId: step.parentSessionId,
1677
1701
  baseArgs: ["--mode", "json", "-p"],
1678
- task,
1702
+ task: attemptTask,
1679
1703
  taskDelivery: taskDeliveryOverride,
1680
1704
  sessionEnabled,
1681
1705
  sessionDir,
@@ -1686,11 +1710,12 @@ async function runSingleStepInner(
1686
1710
  inheritSkills: step.inheritSkills,
1687
1711
  requireReadTool: Boolean(step.skills?.length),
1688
1712
  tools: step.tools,
1713
+ allowNestedSubagents: step.allowNestedSubagents,
1689
1714
  extensions: step.extensions,
1690
1715
  subagentOnlyExtensions: step.subagentOnlyExtensions,
1691
1716
  fast: step.fast,
1692
1717
  modelCandidates: step.modelCandidates,
1693
- systemPrompt: appendTurnBudgetSystemPrompt(step.systemPrompt ?? "", ctx.turnBudget),
1718
+ systemPrompt: step.systemPrompt ?? "",
1694
1719
  systemPromptMode: step.systemPromptMode,
1695
1720
  mcpDirectTools: step.mcpDirectTools,
1696
1721
  mcpConfig: step.mcpConfig,
@@ -1699,6 +1724,7 @@ async function runSingleStepInner(
1699
1724
  cwd: step.cwd ?? ctx.cwd,
1700
1725
  promptFileStem: step.agent,
1701
1726
  intercomSessionName: ctx.childIntercomTarget,
1727
+ sessionName: childSessionName,
1702
1728
  orchestratorIntercomTarget: ctx.orchestratorIntercomTarget,
1703
1729
  runId: ctx.id,
1704
1730
  childAgentName: step.agent,
@@ -1722,6 +1748,7 @@ async function runSingleStepInner(
1722
1748
  : undefined,
1723
1749
  childWatchdog,
1724
1750
  waitToolEnabled: step.waitToolEnabled,
1751
+ waitToolDefaultTimeoutMs: step.waitToolDefaultTimeoutMs,
1725
1752
  thinkingCeiling: step.thinkingCeiling,
1726
1753
  extensionBindings,
1727
1754
  }));
@@ -1732,6 +1759,7 @@ async function runSingleStepInner(
1732
1759
  if (step.definitionDigest) {
1733
1760
  const toolPlan = resolvedTaskToolPlan ?? resolvePiLaunchToolPlan(omitUndefinedProperties({
1734
1761
  tools: step.tools,
1762
+ allowNestedSubagents: step.allowNestedSubagents,
1735
1763
  extensions: step.extensions,
1736
1764
  subagentOnlyExtensions: step.subagentOnlyExtensions,
1737
1765
  fast: step.fast,
@@ -1756,7 +1784,7 @@ async function runSingleStepInner(
1756
1784
  ...(step.fast !== undefined ? { fast: step.fast } : {}),
1757
1785
  ...(resolveEffectiveThinking(candidate, step.thinking) ? { thinking: resolveEffectiveThinking(candidate, step.thinking) } : {}),
1758
1786
  ...(step.thinkingCeiling ? { thinkingCeiling: step.thinkingCeiling } : {}),
1759
- systemPrompt: appendTurnBudgetSystemPrompt(step.systemPrompt ?? "", ctx.turnBudget),
1787
+ systemPrompt: step.systemPrompt ?? "",
1760
1788
  systemPromptMode: step.systemPromptMode,
1761
1789
  inheritProjectContext: step.inheritProjectContext,
1762
1790
  inheritGlobalContext: step.inheritGlobalContext,
@@ -1789,7 +1817,6 @@ async function runSingleStepInner(
1789
1817
  ctx.timeoutMessage,
1790
1818
  ctx.registerStop,
1791
1819
  ctx.stopMessage,
1792
- ctx.registerTurnBudgetAbort,
1793
1820
  ctx.onWriterProcess,
1794
1821
  ctx.toolTimeoutMs,
1795
1822
  ctx.deadlineAt,
@@ -1809,25 +1836,6 @@ async function runSingleStepInner(
1809
1836
  processTree: run.processTree,
1810
1837
  });
1811
1838
  }
1812
- if (run.turnBudget) turnBudget = run.turnBudget;
1813
- else if (ctx.turnBudget) {
1814
- const assistantMessages = run.messages.filter((message) => message.role === "assistant");
1815
- const turnCount = assistantMessages.length;
1816
- const lastAssistantMessage = assistantMessages.at(-1);
1817
- if (turnCount > 0 && turnCount < ctx.turnBudget.maxTurns) {
1818
- turnBudget = { ...ctx.turnBudget, outcome: "within-budget", turnCount };
1819
- } else if (turnCount >= ctx.turnBudget.maxTurns) {
1820
- const decision = turnBudgetDecision(
1821
- ctx.turnBudget,
1822
- turnCount,
1823
- lastAssistantMessage ? isTerminalAssistantStop(lastAssistantMessage) : false,
1824
- lastAssistantMessage ? assistantStartsToolCall(lastAssistantMessage) : false,
1825
- );
1826
- turnBudget = decision === "defer"
1827
- ? turnBudgetDeferredState(ctx.turnBudget, turnCount)
1828
- : turnBudgetState(ctx.turnBudget, turnCount, decision === "abort");
1829
- }
1830
- }
1831
1839
  const toolAvailabilityError = run.exitCode === 0 && !run.error
1832
1840
  ? readChildToolDiagnosticError(toolDiagnosticPath)
1833
1841
  : undefined;
@@ -1838,7 +1846,6 @@ async function runSingleStepInner(
1838
1846
  && !run.interrupted
1839
1847
  && !run.timedOut
1840
1848
  && !run.stopped
1841
- && !run.turnBudgetExceeded
1842
1849
  && !run.protocolError
1843
1850
  && !toolAvailabilityError
1844
1851
  ? formatMidToolExitError({
@@ -1876,14 +1883,17 @@ async function runSingleStepInner(
1876
1883
  const hiddenError = run.exitCode === 0 && !run.error && !toolAvailabilityError && !structuredError && !midToolExitError
1877
1884
  ? detectSubagentError(errorMessages)
1878
1885
  : null;
1886
+ const terminalEmptyAfterUsefulWork = !validatedStructuredOutput
1887
+ && hasEmptyTerminalAssistantResponse(run.messages)
1888
+ && (run.toolCount > 0 || Boolean(run.finalOutput.trim()));
1879
1889
  const emptyOutputError = run.exitCode === 0
1880
1890
  && !run.error
1881
1891
  && !toolAvailabilityError
1882
1892
  && !structuredError
1883
- && !run.finalOutput.trim()
1884
1893
  && !validatedStructuredOutput
1894
+ && (!run.finalOutput.trim() || terminalEmptyAfterUsefulWork)
1885
1895
  && (!hiddenError?.hasError || hasEmptyTerminalAssistantResponse(run.messages))
1886
- ? "Subagent produced no output (possible model cold-start or empty response)."
1896
+ ? formatEmptyTerminalAssistantResponseError(run.messages)
1887
1897
  : undefined;
1888
1898
  const completionGuardEnabled = isAgentContractV1(step.agentContract) ? step.completionGuard === true : step.completionGuard !== false;
1889
1899
  const completionToolPlan = resolvedTaskToolPlan;
@@ -1904,25 +1914,24 @@ async function runSingleStepInner(
1904
1914
  }))
1905
1915
  : undefined;
1906
1916
  const mutationAttemptObserved = run.observedMutationAttempt === true || completionMutationEvidence?.attemptedMutation === true;
1907
- const completionGuardTriggered = completionGuard?.triggered === true && !mutationAttemptObserved;
1908
- const completionGuardBlocked = completionGuard?.blocked === true;
1909
- const mutationExpected = completionGuard?.expectedMutation ?? (completionGuardEnabled
1910
- && hasMutationToolCapability(completionTools, completionToolPlan?.effectiveMcpTools ?? step.mcpDirectTools)
1911
- && expectsImplementationMutation(step.agent, taskForCompletionGuard));
1912
- const fileMutationEffect = completionGuard
1913
- ? {
1914
- status: completionGuardBlocked ? "blocked" as const : completionGuard.expectedMutation ? completionGuardTriggered ? "missing" as const : "observed" as const : "not-applicable" as const,
1915
- expected: completionGuard.expectedMutation,
1916
- attempted: completionGuardBlocked ? false : completionGuard.attemptedMutation || mutationAttemptObserved,
1917
- ...(completionMutationEvidence ? { evidence: completionMutationEvidence } : {}),
1918
- ...(completionGuardBlocked && completionGuard.message ? { message: completionGuard.message } : {}),
1919
- ...(completionGuardTriggered ? { message: "Subagent completed without making edits for an implementation task." } : {}),
1920
- }
1921
- : undefined;
1922
- const completionGuardError = completionGuardTriggered && !isAgentContractV1(step.agentContract)
1923
- ? "Subagent completed without making edits for an implementation task.\nIt appears to have returned planning or scratchpad output instead of applying changes."
1917
+ const completionEvidence = planCompletionEvidence({
1918
+ guard: completionGuard,
1919
+ completionGuardEnabled,
1920
+ mutationCapable: hasMutationToolCapability(completionTools, completionToolPlan?.effectiveMcpTools ?? step.mcpDirectTools),
1921
+ implementationMutationExpected: expectsImplementationMutation(step.agent, taskForCompletionGuard),
1922
+ mutationAttemptObserved,
1923
+ mutationEvidence: completionMutationEvidence,
1924
+ agentContractV1: isAgentContractV1(step.agentContract),
1925
+ });
1926
+ const finalOutputHasPersistableFileContent = run.exitCode === 0 && !run.error && !emptyOutputError && Boolean(stripAcceptanceReport(run.finalOutput).trim());
1927
+ const requiredOutput = step.outputMode === "file-only" && step.outputPath
1928
+ ? { kind: "file-only" as const, path: step.outputPath, missing: !fs.existsSync(step.outputPath) && !finalOutputHasPersistableFileContent }
1929
+ : effectiveStructuredOutput
1930
+ ? { kind: "structured" as const, path: effectiveStructuredOutput.outputPath, missing: !fs.existsSync(effectiveStructuredOutput.outputPath) }
1924
1931
  : undefined;
1925
- const effectiveExitCode = toolAvailabilityError || (completionGuardTriggered && !isAgentContractV1(step.agentContract)) || midToolExitError || structuredError || emptyOutputError
1932
+ const missingRequiredOutputError = formatRequiredOutputError(requiredOutput);
1933
+ const missingRequiredOutputAfterMutation = Boolean(missingRequiredOutputError) && (mutationAttemptObserved || Boolean(mutationEvidence.changedFiles.length));
1934
+ const effectiveExitCode = toolAvailabilityError || completionEvidence.legacyFailureError || midToolExitError || structuredError || emptyOutputError || missingRequiredOutputError
1926
1935
  ? 1
1927
1936
  : hiddenError?.hasError
1928
1937
  ? (hiddenError.exitCode ?? 1)
@@ -1934,19 +1943,23 @@ async function runSingleStepInner(
1934
1943
  interrupted: run.interrupted,
1935
1944
  timedOut: run.timedOut,
1936
1945
  stopped: run.stopped,
1937
- turnBudgetExceeded: run.turnBudgetExceeded,
1938
1946
  })) ? formatProcessSignalError(run.processSignal!) : undefined;
1947
+ const underlyingError = toolAvailabilityError
1948
+ ?? midToolExitError
1949
+ ?? structuredError
1950
+ ?? run.error
1951
+ ?? signalError
1952
+ ?? (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined)
1953
+ ?? ((emptyOutputError || missingRequiredOutputError) && run.stderr.trim() ? run.stderr.trim() : undefined)
1954
+ ?? emptyOutputError
1955
+ ?? (missingRequiredOutputAfterMutation ? missingRequiredOutputError : undefined)
1956
+ ?? (hiddenError?.hasError
1957
+ ? hiddenError.details
1958
+ ? `${hiddenError.errorType} failed (exit ${effectiveExitCode}): ${hiddenError.details}`
1959
+ : `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
1960
+ : undefined);
1939
1961
  const error = formatSubagentExtensionConflictError(
1940
- toolAvailabilityError
1941
- ?? completionGuardError
1942
- ?? midToolExitError
1943
- ?? structuredError
1944
- ?? emptyOutputError
1945
- ?? (hiddenError?.hasError
1946
- ? hiddenError.details
1947
- ? `${hiddenError.errorType} failed (exit ${effectiveExitCode}): ${hiddenError.details}`
1948
- : `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
1949
- : run.error || signalError || (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined)),
1962
+ underlyingError ?? missingRequiredOutputError ?? completionEvidence.legacyFailureError,
1950
1963
  {
1951
1964
  agent: step.agent,
1952
1965
  ambientExtensionsEnabled: launchResolvedExtensions?.disableAmbientExtensions === false,
@@ -1960,8 +1973,8 @@ async function runSingleStepInner(
1960
1973
  usage: run.usage,
1961
1974
  });
1962
1975
  modelAttempts.push(attempt);
1963
- if (candidate && startupAttemptIndex === 0) attemptedModels.push(candidate);
1964
- completionGuardTriggeredFinal = completionGuardTriggered;
1976
+ if (!recoveringAbort && candidate && startupAttemptIndex === 0) attemptedModels.push(candidate);
1977
+ completionGuardTriggeredFinal = completionEvidence.guardTriggered && !underlyingError && !missingRequiredOutputError;
1965
1978
  finalOutputSnapshot = outputSnapshot;
1966
1979
  if (step.toolBudget) {
1967
1980
  const toolMessages = run.messages.filter((message) => message.role === "toolResult");
@@ -1969,27 +1982,48 @@ async function runSingleStepInner(
1969
1982
  toolBudgetBlocked = Boolean(blockedMessage);
1970
1983
  toolBudget = toolBudgetState(step.toolBudget, toolMessages.length, blockedMessage ? (blockedMessage as { toolName?: string }).toolName : undefined);
1971
1984
  }
1972
- const requiredOutput = step.outputMode === "file-only" && step.outputPath
1973
- ? { kind: "file-only" as const, path: step.outputPath, missing: !fs.existsSync(step.outputPath) }
1974
- : effectiveStructuredOutput
1975
- ? { kind: "structured" as const, path: effectiveStructuredOutput.outputPath, missing: !fs.existsSync(effectiveStructuredOutput.outputPath) }
1976
- : undefined;
1977
- const settlementDiagnostic: SettlementDiagnostic | undefined = effectiveExitCode !== 0 || completionGuardTriggered || completionGuardBlocked
1978
- ? {
1979
- finalTextPresent: Boolean(stripAcceptanceReport(run.finalOutput).trim()),
1980
- mutation: {
1981
- expected: mutationExpected,
1982
- attempted: completionGuard?.attemptedMutation === true || mutationAttemptObserved,
1983
- observed: mutationEvidence.attemptedMutation,
1984
- },
1985
- ...(requiredOutput ? { requiredOutput } : {}),
1986
- afterCompactionSettlement: run.afterCompactionSettlement === true,
1987
- }
1988
- : undefined;
1985
+ const settlementDiagnostic = projectSettlementDiagnostic(completionEvidence, {
1986
+ terminalFailed: effectiveExitCode !== 0,
1987
+ finalTextPresent: Boolean(stripAcceptanceReport(run.finalOutput).trim()),
1988
+ mutationObserved: mutationEvidence.attemptedMutation,
1989
+ requiredOutput,
1990
+ afterCompactionSettlement: run.afterCompactionSettlement === true,
1991
+ });
1992
+ const fileMutationEffect = completionEvidence.fileMutation ?? (missingRequiredOutputAfterMutation ? { status: "observed" as const, expected: completionEvidence.mutationExpected, attempted: true, evidence: mutationEvidence } : undefined);
1989
1993
  finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error, structuredOutput, runtimeAcknowledgedExtensions, ...(step.agentContract ? { agentContract: step.agentContract } : {}), ...(fileMutationEffect || settlementDiagnostic ? { effects: { ...(fileMutationEffect ? { fileMutation: fileMutationEffect } : {}), ...(settlementDiagnostic ? { settlementDiagnostic } : {}) } } : {}) } as RunPiStreamingResult;
1990
- if (run.turnBudgetExceeded) break modelAttemptsLoop;
1994
+ const abortRecovery = !attempt.success ? planAbortRecovery({
1995
+ messages: run.messages,
1996
+ error,
1997
+ processSignal: run.processSignal,
1998
+ sessionAvailable: Boolean(step.sessionFile && fs.existsSync(step.sessionFile)),
1999
+ alreadyResumed: abortRecoveryAttempted,
2000
+ stopped: run.stopped || ctx.stopSignal?.aborted || ctx.skipAcceptance?.(),
2001
+ interrupted: run.interrupted,
2002
+ timedOut: run.timedOut || ctx.timeoutSignal?.aborted,
2003
+ toolBudgetExhausted: run.toolBudgetBlocked || toolBudgetBlocked,
2004
+ usageBudgetExhausted: ctx.usageBudgetExhausted?.(),
2005
+ structuredOutputFailed: Boolean(structuredError),
2006
+ acceptanceFailed: false,
2007
+ currentTool: run.currentTool,
2008
+ afterCompactionSettlement: run.afterCompactionSettlement,
2009
+ }) : undefined;
2010
+ if (abortRecovery?.action === "settle" && abortRecovery.diagnostic) {
2011
+ attempt.error = attempt.error
2012
+ ? `${abortRecovery.diagnostic}\n${attempt.error.slice(0, 8_000)}`
2013
+ : abortRecovery.diagnostic;
2014
+ if (finalResult) finalResult.abortRecoveryDiagnostic = abortRecovery.diagnostic;
2015
+ }
1991
2016
  if (run.stopped || run.timedOut || ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break modelAttemptsLoop;
1992
- if (attempt.success || completionGuardTriggered) break modelAttemptsLoop;
2017
+ if (abortRecovery?.action === "settle" && abortRecovery.diagnostic) break modelAttemptsLoop;
2018
+ if (attempt.success) break modelAttemptsLoop;
2019
+ if (recoveringAbort) break modelAttemptsLoop;
2020
+ if (abortRecovery?.action === "resume") {
2021
+ abortRecoveryAttempted = true;
2022
+ nextAttemptTask = abortRecovery.prompt;
2023
+ attemptNotes.push("[abort-recovery] provider/transport abort after useful progress; resuming the retained child session once.");
2024
+ continue;
2025
+ }
2026
+ if (completionEvidence.guardTriggered) break modelAttemptsLoop;
1993
2027
 
1994
2028
  const startupFailure = isRetryableSubagentStartupFailure(omitUndefinedProperties({
1995
2029
  exitCode: effectiveExitCode,
@@ -2005,7 +2039,6 @@ async function runSingleStepInner(
2005
2039
  interrupted: run.interrupted,
2006
2040
  timedOut: run.timedOut,
2007
2041
  stopped: run.stopped,
2008
- turnBudgetExceeded: run.turnBudgetExceeded,
2009
2042
  }));
2010
2043
  const retryDelayMs = SUBAGENT_STARTUP_RETRY_DELAYS_MS[startupAttemptIndex];
2011
2044
  if (startupFailure && retryDelayMs !== undefined) {
@@ -2052,8 +2085,14 @@ async function runSingleStepInner(
2052
2085
  const rawOutput = finalResult?.finalOutput ?? "";
2053
2086
  const outputForPersistence = stripAcceptanceReport(rawOutput);
2054
2087
  const resolvedOutput = step.outputPath && finalResult?.exitCode === 0
2055
- ? resolveSingleOutput(step.outputPath, outputForPersistence, finalOutputSnapshot)
2088
+ ? resolveSingleOutput(step.outputPath, outputForPersistence, finalOutputSnapshot, step.outputClaimPath)
2056
2089
  : { fullOutput: outputForPersistence };
2090
+ if (resolvedOutput.fatalError) {
2091
+ if (finalResult) {
2092
+ finalResult.exitCode = 1;
2093
+ finalResult.error = finalResult.error ? `${finalResult.error}\n${resolvedOutput.saveError}` : resolvedOutput.saveError;
2094
+ }
2095
+ }
2057
2096
  const output = stripAcceptanceReport(resolvedOutput.fullOutput);
2058
2097
  const outputReference = resolvedOutput.savedPath ? formatSavedOutputReference(resolvedOutput.savedPath, output) : undefined;
2059
2098
  let outputForSummary = output;
@@ -2062,14 +2101,6 @@ async function runSingleStepInner(
2062
2101
  }
2063
2102
  if (finalResult?.stopped && !outputForSummary.trim()) {
2064
2103
  outputForSummary = ctx.stopMessage ?? "Subagent stopped by user.";
2065
- } else if (!finalResult?.timedOut && !finalResult?.stopped && finalResult?.turnBudgetExceeded && turnBudget) {
2066
- outputForSummary = formatTurnBudgetOutput(turnBudgetExceededMessage(turnBudget, turnBudget.turnCount), outputForSummary);
2067
- } else if (!finalResult?.timedOut && !finalResult?.stopped && turnBudget?.outcome === "termination-deferred") {
2068
- const note = turnBudgetDeferredNote(turnBudget, turnBudget.terminationDeferredAtTurn ?? turnBudget.turnCount);
2069
- outputForSummary = outputForSummary.trim() ? `${note}\n\n${outputForSummary}` : note;
2070
- } else if (!finalResult?.timedOut && !finalResult?.stopped && turnBudget?.outcome === "wrap-up-requested") {
2071
- const note = turnBudgetSoftNote(turnBudget, turnBudget.wrapUpRequestedAtTurn ?? turnBudget.turnCount);
2072
- outputForSummary = outputForSummary.trim() ? `${note}\n\n${outputForSummary}` : note;
2073
2104
  }
2074
2105
  const outputForAcceptance = rawOutput;
2075
2106
  const childWrittenOutput = step.outputPath
@@ -2107,7 +2138,7 @@ async function runSingleStepInner(
2107
2138
  saveError: resolvedOutput.saveError,
2108
2139
  }));
2109
2140
  outputForSummary = finalizedOutput.displayOutput;
2110
- const acceptance = step.effectiveAcceptance && !finalResult?.stopped && !finalResult?.turnBudgetExceeded && !ctx.timeoutSignal?.aborted && !ctx.stopSignal?.aborted && !ctx.skipAcceptance?.()
2141
+ const acceptance = step.effectiveAcceptance && !finalResult?.stopped && !ctx.timeoutSignal?.aborted && !ctx.stopSignal?.aborted && !ctx.skipAcceptance?.()
2111
2142
  ? await evaluateAcceptance(omitUndefinedProperties({
2112
2143
  acceptance: step.effectiveAcceptance,
2113
2144
  output: outputForAcceptance,
@@ -2126,39 +2157,31 @@ async function runSingleStepInner(
2126
2157
  : undefined;
2127
2158
  const stoppedAfterAcceptance = finalResult?.stopped === true || ctx.stopSignal?.aborted === true;
2128
2159
  const timedOutAfterAcceptance = !stoppedAfterAcceptance && (finalResult?.timedOut === true || ctx.timeoutSignal?.aborted === true);
2129
- const turnBudgetExceeded = finalResult?.turnBudgetExceeded === true;
2130
2160
  const effectiveAcceptance = step.effectiveAcceptance
2131
2161
  ? stoppedAfterAcceptance
2132
2162
  ? buildSkippedAcceptanceLedger(step.effectiveAcceptance, { id: "stopped", message: "Acceptance was not evaluated because the subagent was stopped." })
2133
2163
  : timedOutAfterAcceptance
2134
2164
  ? buildSkippedAcceptanceLedger(step.effectiveAcceptance, { id: "timeout", message: "Acceptance was not evaluated because the subagent timed out." })
2135
- : turnBudgetExceeded
2136
- ? buildSkippedAcceptanceLedger(step.effectiveAcceptance, { id: "turn-budget", message: "Acceptance was not evaluated because the subagent exceeded its turn budget." })
2137
- : acceptance
2165
+ : acceptance
2138
2166
  : undefined;
2139
2167
  const acceptanceFailure = effectiveAcceptance ? acceptanceFailureMessage(effectiveAcceptance) : undefined;
2140
- const acceptanceCanFailRun = acceptanceFailure && effectiveAcceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted && !timedOutAfterAcceptance && !stoppedAfterAcceptance && !turnBudgetExceeded && !isAgentContractV1(step.agentContract);
2141
- const effectiveFinalExitCode = timedOutAfterAcceptance || stoppedAfterAcceptance || turnBudgetExceeded ? 1 : acceptanceCanFailRun ? 1 : finalResult?.exitCode ?? 1;
2168
+ const acceptanceCanFailRun = acceptanceFailure && effectiveAcceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted && !timedOutAfterAcceptance && !stoppedAfterAcceptance && !isAgentContractV1(step.agentContract);
2169
+ const effectiveFinalExitCode = timedOutAfterAcceptance || stoppedAfterAcceptance ? 1 : acceptanceCanFailRun ? 1 : finalResult?.exitCode ?? 1;
2142
2170
  const intercomDetachReceipt = finalResult?.finalOutput === INTERCOM_DETACH_RECEIPT;
2143
2171
  const baseFinalError = stoppedAfterAcceptance
2144
2172
  ? ctx.stopMessage ?? "Subagent stopped by user."
2145
2173
  : timedOutAfterAcceptance
2146
2174
  ? finalResult?.error ?? ctx.timeoutMessage ?? "Subagent timed out."
2147
- : turnBudgetExceeded
2148
- ? finalResult?.error ?? (turnBudget ? turnBudgetExceededMessage(turnBudget, turnBudget.turnCount) : "Subagent exceeded turn budget.")
2149
- : acceptanceCanFailRun
2175
+ : acceptanceCanFailRun
2150
2176
  ? (finalResult?.error ? `${finalResult.error}\n${acceptanceFailure}` : acceptanceFailure)
2151
2177
  : finalResult?.error ?? (intercomDetachReceipt ? INTERCOM_DETACH_RECEIPT : undefined);
2152
2178
  const effectiveFinalError = formatChildFailureDiagnostic({
2153
2179
  error: baseFinalError,
2154
2180
  afterCompactionSettlement: effectiveFinalExitCode !== 0 ? finalResult?.afterCompactionSettlement : undefined,
2155
- missingFileOnlyOutput: effectiveFinalExitCode !== 0 && finalResult?.effects?.settlementDiagnostic?.requiredOutput?.kind === "file-only" && finalResult.effects.settlementDiagnostic.requiredOutput.missing
2156
- ? finalResult.effects.settlementDiagnostic.requiredOutput.path
2157
- : undefined,
2158
- missingStructuredOutput: effectiveFinalExitCode !== 0 && finalResult?.effects?.settlementDiagnostic?.requiredOutput?.kind === "structured" && finalResult.effects.settlementDiagnostic.requiredOutput.missing
2159
- ? finalResult.effects.settlementDiagnostic.requiredOutput.path
2160
- : undefined,
2181
+ abortRecoveryDiagnostic: effectiveFinalExitCode !== 0 ? finalResult?.abortRecoveryDiagnostic : undefined,
2182
+ requiredOutput: effectiveFinalExitCode !== 0 ? finalResult?.effects?.settlementDiagnostic?.requiredOutput : undefined,
2161
2183
  });
2184
+ const usage = usageFromAttempts(modelAttempts);
2162
2185
 
2163
2186
  const artifactErrors = artifactPaths && ctx.artifactConfig?.enabled !== false
2164
2187
  ? persistStepArtifacts({
@@ -2178,6 +2201,7 @@ async function runSingleStepInner(
2178
2201
  model: finalResult?.model,
2179
2202
  attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
2180
2203
  modelAttempts,
2204
+ usage,
2181
2205
  error: effectiveFinalError,
2182
2206
  acceptance: effectiveAcceptance,
2183
2207
  ...(capabilityAudit ? { capabilityCeiling: capabilityAudit.ceiling, capabilityAudit } : {}),
@@ -2194,6 +2218,7 @@ async function runSingleStepInner(
2194
2218
 
2195
2219
  const result: StepResult & { completionGuardTriggered?: boolean } = omitUndefinedProperties({
2196
2220
  agent: step.agent,
2221
+ ...(childSessionName ? { sessionName: childSessionName } : {}),
2197
2222
  context: step.context,
2198
2223
  ...(step.agentContract ? { agentContract: step.agentContract } : {}),
2199
2224
  launchContractDigest: actualLaunchContractDigest,
@@ -2209,26 +2234,24 @@ async function runSingleStepInner(
2209
2234
  modelAttempts,
2210
2235
  contextOverflow: contextOverflow || undefined,
2211
2236
  totalCost: costSummaryFromAttempts(modelAttempts),
2237
+ usage,
2212
2238
  artifactPaths,
2213
- outputSaveError: artifactErrors.outputSaveError,
2239
+ outputSaveError: [resolvedOutput.saveError, artifactErrors.outputSaveError].filter(Boolean).join("\n") || undefined,
2214
2240
  metadataSaveError: artifactErrors.metadataSaveError,
2215
2241
  transcriptPath: transcriptWriter ? artifactPaths?.transcriptPath : undefined,
2216
2242
  transcriptError: transcriptWriter?.getError(),
2217
- interrupted: timedOutAfterAcceptance || stoppedAfterAcceptance || turnBudgetExceeded ? false : finalResult?.interrupted,
2243
+ interrupted: timedOutAfterAcceptance || stoppedAfterAcceptance ? false : finalResult?.interrupted,
2218
2244
  timedOut: timedOutAfterAcceptance ? true : finalResult?.timedOut,
2219
2245
  stopped: stoppedAfterAcceptance ? true : finalResult?.stopped,
2220
2246
  processSignal: finalResult?.processSignal,
2221
2247
  timeoutRecovery,
2222
- turnBudget,
2223
- turnBudgetExceeded: turnBudgetExceeded || undefined,
2224
- wrapUpRequested: finalResult?.wrapUpRequested || turnBudget?.outcome === "wrap-up-requested" || turnBudget?.outcome === "termination-deferred" || turnBudgetExceeded || undefined,
2225
2248
  toolBudget,
2226
2249
  toolBudgetBlocked: toolBudgetBlocked || undefined,
2227
2250
  completionGuardTriggered: completionGuardTriggeredFinal,
2228
2251
  ...((finalResult as (RunPiStreamingResult & { effects?: import("../../shared/types.ts").EffectsProjection }) | undefined)?.effects ? { effects: (finalResult as RunPiStreamingResult & { effects?: import("../../shared/types.ts").EffectsProjection }).effects } : {}),
2229
- structuredOutput: timedOutAfterAcceptance || stoppedAfterAcceptance || turnBudgetExceeded ? undefined : (finalResult as (RunPiStreamingResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput,
2230
- structuredOutputPath: timedOutAfterAcceptance || stoppedAfterAcceptance || turnBudgetExceeded ? undefined : effectiveStructuredOutput?.outputPath,
2231
- structuredOutputSchemaPath: timedOutAfterAcceptance || stoppedAfterAcceptance || turnBudgetExceeded ? undefined : effectiveStructuredOutput?.schemaPath,
2252
+ structuredOutput: timedOutAfterAcceptance || stoppedAfterAcceptance ? undefined : (finalResult as (RunPiStreamingResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput,
2253
+ structuredOutputPath: timedOutAfterAcceptance || stoppedAfterAcceptance ? undefined : effectiveStructuredOutput?.outputPath,
2254
+ structuredOutputSchemaPath: timedOutAfterAcceptance || stoppedAfterAcceptance ? undefined : effectiveStructuredOutput?.schemaPath,
2232
2255
  acceptance: effectiveAcceptance,
2233
2256
  watchdog: finalResult?.watchdog,
2234
2257
  ...(capabilityAudit ? { capabilityCeiling: capabilityAudit.ceiling, capabilityAudit } : {}),
@@ -2302,6 +2325,11 @@ function requiredStatusStep(statusPayload: RunnerStatusPayload, index: number):
2302
2325
  return step;
2303
2326
  }
2304
2327
 
2328
+ function setStatusWorktreeReference(statusStep: RunnerStatusStep, worktree: WorktreeSetup["worktrees"][number]): void {
2329
+ statusStep.worktreePath = worktree.path;
2330
+ statusStep.branch = worktree.branch;
2331
+ }
2332
+
2305
2333
  function markParallelGroupSetupFailure(input: {
2306
2334
  statusPayload: RunnerStatusPayload;
2307
2335
  results: StepResult[];
@@ -2430,6 +2458,25 @@ function resolveAsyncStepTranscriptPath(input: {
2430
2458
 
2431
2459
  type SingleStepResult = Awaited<ReturnType<typeof runSingleStep>>;
2432
2460
 
2461
+ function missingRequiredOutputAfterUsefulMutation(result: SingleStepResult): boolean {
2462
+ const effects = result.effects;
2463
+ return effects?.settlementDiagnostic?.requiredOutput?.missing === true
2464
+ && (effects.settlementDiagnostic.mutation.attempted || effects.fileMutation?.attempted === true || Boolean(effects.fileMutation?.evidence?.changedFiles.length));
2465
+ }
2466
+
2467
+ function partialExecutionWithUsefulMutation(result: SingleStepResult): boolean {
2468
+ const fileMutation = result.effects?.fileMutation;
2469
+ return result.execution?.status === "partial" && (fileMutation?.attempted === true || Boolean(fileMutation?.evidence?.changedFiles.length));
2470
+ }
2471
+
2472
+ function partialEvidenceResult(result: SingleStepResult): boolean {
2473
+ return missingRequiredOutputAfterUsefulMutation(result) || partialExecutionWithUsefulMutation(result);
2474
+ }
2475
+
2476
+ function concreteFailureResult(result: SingleStepResult): boolean {
2477
+ return result.success === false && !partialEvidenceResult(result);
2478
+ }
2479
+
2433
2480
  function combinedAbortSignal(signals: Array<AbortSignal | undefined>): AbortSignal | undefined {
2434
2481
  const activeSignals = signals.filter((signal): signal is AbortSignal => Boolean(signal));
2435
2482
  if (activeSignals.length === 0) return undefined;
@@ -2501,6 +2548,8 @@ async function runSubagent(
2501
2548
  const overallStartTime = Date.now();
2502
2549
  const shareEnabled = config.share === true;
2503
2550
  const asyncDir = config.asyncDir;
2551
+ const handoffWorkflowKey = config.workflowKey;
2552
+ const handoffChildRunId = handoffWorkflowKey ? id : undefined;
2504
2553
  const statusPath = path.join(asyncDir, "status.json");
2505
2554
  const eventsPath = path.join(asyncDir, "events.jsonl");
2506
2555
  const logPath = path.join(asyncDir, `subagent-log-${id}.md`);
@@ -2509,7 +2558,6 @@ async function runSubagent(
2509
2558
  const activeChildTimeouts = new Map<number, () => void>();
2510
2559
  const activeChildStops = new Map<number, () => void>();
2511
2560
  const childStopRequests = new Map<number, { childId: string; requestedAt: number }>();
2512
- const activeChildTurnBudgetAborts = new Map<number, (message: string, state?: TurnBudgetState) => void>();
2513
2561
  const pendingStepSteers: SteerRequest[] = [];
2514
2562
  const steeringCapabilities = new Map<number, SteerCapability>();
2515
2563
  let interrupted = false;
@@ -2518,7 +2566,6 @@ async function runSubagent(
2518
2566
  let timeoutTimer: NodeJS.Timeout | undefined;
2519
2567
  let timedOut = false;
2520
2568
  let stopped = false;
2521
- let turnBudgetExceeded = false;
2522
2569
  let usageBudgetExceeded = false;
2523
2570
  const timeoutMessage = config.timeoutMs !== undefined ? `Subagent timed out after ${config.timeoutMs}ms.` : undefined;
2524
2571
  const stopMessage = "Subagent stopped by user.";
@@ -2539,8 +2586,11 @@ async function runSubagent(
2539
2586
  for (const task of step.parallel) {
2540
2587
  const taskFlatIndex = flatStepCount;
2541
2588
  const transcriptPath = resolveAsyncStepTranscriptPath(omitUndefinedProperties({ artifactsDir, artifactConfig, runId: id, agent: task.agent, flatIndex: taskFlatIndex, flatStepCount: initialFlatStepCount }));
2589
+ const taskSessionName = task.sessionName ?? deriveChildSessionName({ agent: task.agent, task: task.task, label: task.label });
2542
2590
  initialStatusSteps.push(omitUndefinedProperties({
2543
2591
  agent: task.agent,
2592
+ ...(task.lane ? { lane: task.lane } : config.lane ? { lane: config.lane } : {}),
2593
+ ...(taskSessionName ? { sessionName: taskSessionName } : {}),
2544
2594
  ...(externalRunnerStatus(task.runner) ? { runner: externalRunnerStatus(task.runner) } : {}),
2545
2595
  ...(statusStepDescription(task.task) ? { description: statusStepDescription(task.task) } : {}),
2546
2596
  ...(task.context ? { context: task.context } : {}),
@@ -2590,8 +2640,11 @@ async function runSubagent(
2590
2640
  } else {
2591
2641
  const stepFlatIndex = flatStepCount;
2592
2642
  const transcriptPath = resolveAsyncStepTranscriptPath(omitUndefinedProperties({ artifactsDir, artifactConfig, runId: id, agent: step.agent, flatIndex: stepFlatIndex, flatStepCount: initialFlatStepCount }));
2643
+ const stepSessionName = step.sessionName ?? deriveChildSessionName({ agent: step.agent, task: step.task, label: step.label });
2593
2644
  initialStatusSteps.push(omitUndefinedProperties({
2594
2645
  agent: step.agent,
2646
+ ...(step.lane ? { lane: step.lane } : config.lane ? { lane: config.lane } : {}),
2647
+ ...(stepSessionName ? { sessionName: stepSessionName } : {}),
2595
2648
  ...(externalRunnerStatus(step.runner) ? { runner: externalRunnerStatus(step.runner) } : {}),
2596
2649
  ...(statusStepDescription(step.task) ? { description: statusStepDescription(step.task) } : {}),
2597
2650
  ...(step.context ? { context: step.context } : {}),
@@ -2654,7 +2707,6 @@ async function runSubagent(
2654
2707
  lastUpdate: overallStartTime,
2655
2708
  ...(config.timeoutMs !== undefined ? { timeoutMs: config.timeoutMs } : {}),
2656
2709
  ...(config.deadlineAt !== undefined ? { deadlineAt: config.deadlineAt } : {}),
2657
- ...(config.turnBudget ? { turnBudget: initialTurnBudgetState(config.turnBudget) } : {}),
2658
2710
  ...(config.toolBudget ? { toolBudget: initialToolBudgetState(config.toolBudget) } : {}),
2659
2711
  ...(config.usageBudget ? { usageBudget: usageBudgetState(config.usageBudget, undefined) } : {}),
2660
2712
  pid: process.pid,
@@ -2669,6 +2721,7 @@ async function runSubagent(
2669
2721
  ...(config.runFanoutBudget ? { runFanoutBudget: getRunFanoutBudgetSnapshot(config.runFanoutBudget) } : {}),
2670
2722
  ...(config.parentWorkflowRunId ? { parentWorkflowRunId: config.parentWorkflowRunId } : {}),
2671
2723
  ...(config.workflowKey ? { workflowKey: config.workflowKey } : {}),
2724
+ ...(config.lane ? { lane: config.lane } : {}),
2672
2725
  ...(config.runnerProcessInstanceId ? { processTerminal: { version: 1 as const, state: "pending" as const, runId: id, runnerProcessInstanceId: config.runnerProcessInstanceId } } : {}),
2673
2726
  steps: initialStatusSteps,
2674
2727
  artifactsDir,
@@ -2786,6 +2839,7 @@ async function runSubagent(
2786
2839
  const statusResultSummary = (state: AsyncStatus["state"]): string => {
2787
2840
  if (statusPayload.error) return statusPayload.error;
2788
2841
  if (state === "paused") return "Paused after interrupt. Waiting for explicit next action.";
2842
+ if (state === "partial") return "Subagent needs attention after partial work.";
2789
2843
  if (state === "stopped") return stopMessage;
2790
2844
  if (state === "rejected") return "Subagent rejected.";
2791
2845
  return state === "complete" ? "Subagent completed." : "Subagent failed.";
@@ -2794,12 +2848,13 @@ async function runSubagent(
2794
2848
  if (step.status === "complete" || step.status === "completed") return true;
2795
2849
  if (step.status === "failed" || step.status === "stopped" || step.status === "rejected") return false;
2796
2850
  if (state === "complete") return true;
2797
- if (state === "failed" || state === "stopped" || state === "rejected") return false;
2851
+ if (state === "failed" || state === "partial" || state === "stopped" || state === "rejected") return false;
2798
2852
  return undefined;
2799
2853
  };
2800
2854
  const writeRecoverableStatusResult = (): void => {
2801
2855
  const state = statusResultState();
2802
2856
  if (!state || finalResultCommitted || !config.sessionId) return;
2857
+ if ((state as string) === "complete") return;
2803
2858
  const now = statusPayload.endedAt ?? statusPayload.lastUpdate ?? Date.now();
2804
2859
  const summary = statusResultSummary(state);
2805
2860
  runPersistence.write(resultPath, omitUndefinedProperties({
@@ -2811,10 +2866,11 @@ async function runSubagent(
2811
2866
  success: state === "complete",
2812
2867
  state,
2813
2868
  summary,
2814
- error: state === "failed" || state === "stopped" || state === "rejected" ? summary : undefined,
2869
+ error: state === "failed" || state === "partial" || state === "stopped" || state === "rejected" ? summary : undefined,
2815
2870
  stopped: state === "stopped" ? true : undefined,
2816
2871
  results: statusPayload.steps.map((step) => omitUndefinedProperties({
2817
2872
  agent: step.agent,
2873
+ ...(step.sessionName ? { sessionName: step.sessionName } : {}),
2818
2874
  output: step.status === "complete" || step.status === "completed" ? "" : step.error ?? summary,
2819
2875
  error: step.error,
2820
2876
  success: statusResultSuccess(state, step),
@@ -2822,6 +2878,7 @@ async function runSubagent(
2822
2878
  model: step.model,
2823
2879
  attemptedModels: step.attemptedModels,
2824
2880
  modelAttempts: step.modelAttempts,
2881
+ usage: usageFromAttempts(step.modelAttempts),
2825
2882
  contextOverflow: step.contextOverflow,
2826
2883
  })),
2827
2884
  exitCode: state === "complete" || state === "paused" ? 0 : 1,
@@ -2916,7 +2973,7 @@ async function runSubagent(
2916
2973
  };
2917
2974
  const childStopResult = (index: number, agent: string, context?: "fresh" | "fork"): SingleStepResult => {
2918
2975
  markChildStopped(index);
2919
- return stoppedStepResult(agent, context);
2976
+ return stoppedStepResult(agent, context, requiredStatusStep(statusPayload, index).sessionName);
2920
2977
  };
2921
2978
  const stopChildStep = (request: StopRequest): void => {
2922
2979
  if (request.targetIndex === undefined) {
@@ -2959,13 +3016,6 @@ async function runSubagent(
2959
3016
  activeChildStops.set(flatIndex, stop);
2960
3017
  if (stopped || childStopRequests.has(flatIndex)) stop();
2961
3018
  };
2962
- const registerStepTurnBudgetAbort = (flatIndex: number, abort: ((message: string, state?: TurnBudgetState) => void) | undefined): void => {
2963
- if (!abort) {
2964
- activeChildTurnBudgetAborts.delete(flatIndex);
2965
- return;
2966
- }
2967
- activeChildTurnBudgetAborts.set(flatIndex, abort);
2968
- };
2969
3019
  const interruptActiveChildren = (): void => {
2970
3020
  for (const interrupt of [...activeChildInterrupts.values()]) interrupt();
2971
3021
  };
@@ -3075,23 +3125,26 @@ async function runSubagent(
3075
3125
  }
3076
3126
  }
3077
3127
  };
3078
- const pausedStepResult = (agent: string, context?: "fresh" | "fork"): SingleStepResult => omitUndefinedProperties({
3128
+ const pausedStepResult = (agent: string, context?: "fresh" | "fork", sessionName?: string): SingleStepResult => omitUndefinedProperties({
3079
3129
  agent,
3130
+ sessionName,
3080
3131
  context,
3081
3132
  output: "Paused after interrupt. Waiting for explicit next action.",
3082
3133
  exitCode: 0,
3083
3134
  interrupted: true,
3084
3135
  });
3085
- const timedOutStepResult = (agent: string, context?: "fresh" | "fork"): SingleStepResult => omitUndefinedProperties({
3136
+ const timedOutStepResult = (agent: string, context?: "fresh" | "fork", sessionName?: string): SingleStepResult => omitUndefinedProperties({
3086
3137
  agent,
3138
+ sessionName,
3087
3139
  context,
3088
3140
  output: timeoutMessage ?? "Subagent timed out.",
3089
3141
  error: timeoutMessage ?? "Subagent timed out.",
3090
3142
  exitCode: 1,
3091
3143
  timedOut: true,
3092
3144
  });
3093
- const stoppedStepResult = (agent: string, context?: "fresh" | "fork"): SingleStepResult => omitUndefinedProperties({
3145
+ const stoppedStepResult = (agent: string, context?: "fresh" | "fork", sessionName?: string): SingleStepResult => omitUndefinedProperties({
3094
3146
  agent,
3147
+ sessionName,
3095
3148
  context,
3096
3149
  output: stopMessage,
3097
3150
  error: stopMessage,
@@ -3481,66 +3534,6 @@ async function runSubagent(
3481
3534
  statusPayload.lastUpdate = now;
3482
3535
  writeStatusPayload();
3483
3536
  };
3484
- const updateStepTurnBudget = (
3485
- flatIndex: number,
3486
- turnCount: number,
3487
- now: number,
3488
- terminalAssistantStop: boolean,
3489
- toolWorkActiveOrStarting: boolean,
3490
- ): void => {
3491
- const budget = config.turnBudget;
3492
- const step = statusPayload.steps[flatIndex];
3493
- if (!budget || !step || timedOut || stopped || turnBudgetExceeded || step.turnBudgetExceeded) return;
3494
- if (turnCount < budget.maxTurns) {
3495
- const state: TurnBudgetState = { ...budget, outcome: "within-budget", turnCount };
3496
- step.turnBudget = state;
3497
- statusPayload.turnBudget = state;
3498
- return;
3499
- }
3500
- if (!step.wrapUpRequested) {
3501
- step.wrapUpRequested = true;
3502
- statusPayload.wrapUpRequested = true;
3503
- appendRecentStepOutput(step, [turnBudgetSoftNote(budget, turnCount)]);
3504
- }
3505
- const decision = turnBudgetDecision(budget, turnCount, terminalAssistantStop, toolWorkActiveOrStarting);
3506
- if (decision === "defer") {
3507
- const firstDeferredTurn = step.turnBudget?.terminationDeferredAtTurn;
3508
- const deferredState = turnBudgetDeferredState(budget, turnCount, firstDeferredTurn);
3509
- step.turnBudget = deferredState;
3510
- statusPayload.turnBudget = deferredState;
3511
- if (firstDeferredTurn === undefined) {
3512
- appendJsonl(eventsPath, JSON.stringify({
3513
- type: "subagent.step.turn_budget_deferred",
3514
- ts: now,
3515
- runId: id,
3516
- stepIndex: flatIndex,
3517
- agent: step.agent,
3518
- turnCount,
3519
- maxTurns: budget.maxTurns,
3520
- graceTurns: budget.graceTurns,
3521
- }));
3522
- }
3523
- return;
3524
- }
3525
- const state = turnBudgetState(budget, turnCount, false);
3526
- step.turnBudget = state;
3527
- statusPayload.turnBudget = state;
3528
- if (decision !== "abort") return;
3529
- const exceededState = turnBudgetState(budget, turnCount, true);
3530
- const message = turnBudgetExceededMessage(budget, turnCount);
3531
- step.turnBudget = exceededState;
3532
- step.turnBudgetExceeded = true;
3533
- step.wrapUpRequested = true;
3534
- step.error = message;
3535
- turnBudgetExceeded = true;
3536
- statusPayload.turnBudget = exceededState;
3537
- statusPayload.turnBudgetExceeded = true;
3538
- statusPayload.wrapUpRequested = true;
3539
- statusPayload.error = message;
3540
- statusPayload.lastUpdate = now;
3541
- appendJsonl(eventsPath, JSON.stringify({ type: "subagent.step.turn_budget_exceeded", ts: now, runId: id, stepIndex: flatIndex, agent: step.agent, turnCount, maxTurns: budget.maxTurns, graceTurns: budget.graceTurns, message }));
3542
- activeChildTurnBudgetAborts.get(flatIndex)?.(message, exceededState);
3543
- };
3544
3537
  const updateStepFromChildEvent = (flatIndex: number, event: ChildEvent): void => {
3545
3538
  const step = statusPayload.steps[flatIndex];
3546
3539
  if (!step) return;
@@ -3681,13 +3674,6 @@ async function runSubagent(
3681
3674
  refreshUsageBudget();
3682
3675
  }
3683
3676
  statusPayload.turnCount = Math.max(statusPayload.turnCount ?? 0, step.turnCount);
3684
- updateStepTurnBudget(
3685
- flatIndex,
3686
- step.turnCount,
3687
- now,
3688
- isTerminalAssistantStop(event.message),
3689
- assistantStartsToolCall(event.message) || Boolean(step.currentTool),
3690
- );
3691
3677
  }
3692
3678
  syncTopLevelCurrentTool();
3693
3679
  step.lastActivityAt = now;
@@ -3916,7 +3902,7 @@ async function runSubagent(
3916
3902
  let flatIndex = 0;
3917
3903
  let stepCursor = 0;
3918
3904
  while (true) {
3919
- if (interrupted || timedOut || stopped || turnBudgetExceeded) break;
3905
+ if (interrupted || timedOut || stopped) break;
3920
3906
  consumePendingAppendRequests();
3921
3907
  if (stepCursor >= steps.length) break;
3922
3908
  refreshUsageBudget();
@@ -4061,10 +4047,12 @@ async function runSubagent(
4061
4047
  : step.parallel.outputPath;
4062
4048
  const taskText = task.task ?? step.parallel.task;
4063
4049
  const materializedTask = step.parallel.namespaceOutputPath ? injectSingleOutputInstruction(taskText, outputPath, step.parallel) : taskText;
4050
+ const sessionName = deriveChildSessionName({ agent: step.parallel.agent, task: taskText, label: task.label ?? step.parallel.label });
4064
4051
  return omitUndefinedProperties({
4065
4052
  ...step.parallel,
4066
4053
  runFanoutPath: `chain[${stepIndex}].expand[${itemIndex}]`,
4067
4054
  task: materializedTask,
4055
+ ...(sessionName ? { sessionName } : {}),
4068
4056
  effectiveAcceptance: resolveEffectiveAcceptance(omitUndefinedProperties({
4069
4057
  explicit: step.parallel.acceptanceInput,
4070
4058
  agentName: step.parallel.agent,
@@ -4095,6 +4083,7 @@ async function runSubagent(
4095
4083
  const transcriptPath = resolveAsyncStepTranscriptPath(omitUndefinedProperties({ artifactsDir, artifactConfig, runId: id, agent: task.agent, flatIndex: groupStartFlatIndex + itemIndex, flatStepCount: dynamicFlatStepCount }));
4096
4084
  return omitUndefinedProperties({
4097
4085
  agent: task.agent,
4086
+ ...(task.sessionName ? { sessionName: task.sessionName } : {}),
4098
4087
  ...(statusStepDescription(task.task) ? { description: statusStepDescription(task.task) } : {}),
4099
4088
  ...(task.context ? { context: task.context } : {}),
4100
4089
  ...(task.phase ?? step.phase ? { phase: task.phase ?? step.phase } : {}),
@@ -4178,12 +4167,12 @@ async function runSubagent(
4178
4167
  usageBudgetExceeded = true;
4179
4168
  writeStatusPayload();
4180
4169
  appendJsonl(eventsPath, JSON.stringify({ type: "subagent.step.failed", ts: skippedAt, runId: id, stepIndex: fi, agent: task.agent, exitCode: 1, durationMs: 0 }));
4181
- return omitUndefinedProperties({ agent: task.agent, context: task.context, output: message, error: message, exitCode: 1 as number | null, skipped: true });
4170
+ return omitUndefinedProperties({ agent: task.agent, ...(task.sessionName ? { sessionName: task.sessionName } : {}), context: task.context, output: message, error: message, exitCode: 1 as number | null, skipped: true });
4182
4171
  }
4183
- if (timedOut) return timedOutStepResult(task.agent, task.context);
4184
- if (stopped) return stoppedStepResult(task.agent, task.context);
4172
+ if (timedOut) return timedOutStepResult(task.agent, task.context, task.sessionName);
4173
+ if (stopped) return stoppedStepResult(task.agent, task.context, task.sessionName);
4185
4174
  if (childStopRequests.has(fi)) return childStopResult(fi, task.agent, task.context);
4186
- if (interrupted) return pausedStepResult(task.agent, task.context);
4175
+ if (interrupted) return pausedStepResult(task.agent, task.context, task.sessionName);
4187
4176
  if (aborted && failFast) {
4188
4177
  const skippedAt = Date.now();
4189
4178
  requiredStatusStep(statusPayload, fi).status = "failed";
@@ -4194,7 +4183,7 @@ async function runSubagent(
4194
4183
  requiredStatusStep(statusPayload, fi).exitCode = -1;
4195
4184
  statusPayload.lastUpdate = skippedAt;
4196
4185
  writeStatusPayload();
4197
- return omitUndefinedProperties({ agent: task.agent, context: task.context, output: "(skipped — fail-fast)", exitCode: -1 as number | null, skipped: true });
4186
+ return omitUndefinedProperties({ agent: task.agent, ...(task.sessionName ? { sessionName: task.sessionName } : {}), context: task.context, output: "(skipped — fail-fast)", exitCode: -1 as number | null, skipped: true });
4198
4187
  }
4199
4188
  const taskStartTime = Date.now();
4200
4189
  statusPayload.currentStep = fi;
@@ -4230,13 +4219,11 @@ async function runSubagent(
4230
4219
  registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
4231
4220
  registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
4232
4221
  registerStop: (stop) => registerStepStop(fi, stop),
4233
- registerTurnBudgetAbort: (abort) => registerStepTurnBudgetAbort(fi, abort),
4234
4222
  timeoutSignal: timeoutAbortController.signal,
4235
4223
  stopSignal: stopAbortController.signal,
4236
4224
  trackedMutationEvidenceForCompletionGuard: false,
4237
4225
  timeoutMessage,
4238
4226
  stopMessage,
4239
- turnBudget: config.turnBudget,
4240
4227
  toolTimeoutMs: task.toolTimeoutMs ?? config.toolTimeoutMs,
4241
4228
  onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking, attempt.contextLimit),
4242
4229
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
@@ -4244,6 +4231,7 @@ async function runSubagent(
4244
4231
  onExternalProcess: (process) => updateExternalProcess(fi, process),
4245
4232
  onExternalJob: (externalJob) => updateExternalJob(fi, externalJob),
4246
4233
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(fi),
4234
+ usageBudgetExhausted: () => refreshUsageBudget()?.exhausted === true,
4247
4235
  orcaProgressTab,
4248
4236
  }), config.deadlineAt);
4249
4237
  const taskEndTime = Date.now();
@@ -4255,16 +4243,11 @@ async function runSubagent(
4255
4243
  requiredStatusStep(statusPayload, fi).exitCode = stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
4256
4244
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "timedOut", timedOut || singleResult.timedOut ? true : undefined);
4257
4245
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "stopped", stopped || childStopped ? true : undefined);
4258
- setOptionalProperty(requiredStatusStep(statusPayload, fi), "turnBudget", singleResult.turnBudget);
4259
- setOptionalProperty(requiredStatusStep(statusPayload, fi), "turnBudgetExceeded", singleResult.turnBudgetExceeded);
4260
- setOptionalProperty(requiredStatusStep(statusPayload, fi), "wrapUpRequested", singleResult.wrapUpRequested);
4261
4246
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "toolBudget", singleResult.toolBudget);
4262
4247
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "toolBudgetBlocked", singleResult.toolBudgetBlocked);
4263
4248
  if (singleResult.toolBudget) statusPayload.toolBudget = singleResult.toolBudget;
4264
4249
  if (singleResult.toolBudgetBlocked) statusPayload.toolBudgetBlocked = true;
4265
- if (singleResult.turnBudget) statusPayload.turnBudget = singleResult.turnBudget;
4266
- if (singleResult.turnBudgetExceeded) statusPayload.turnBudgetExceeded = true;
4267
- if (singleResult.wrapUpRequested) statusPayload.wrapUpRequested = true;
4250
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "sessionName", singleResult.sessionName);
4268
4251
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "model", singleResult.model);
4269
4252
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "thinking", resolveEffectiveThinking(singleResult.model, requiredStatusStep(statusPayload, fi).thinking));
4270
4253
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "attemptedModels", singleResult.attemptedModels);
@@ -4316,6 +4299,7 @@ async function runSubagent(
4316
4299
  for (const pr of parallelResults) {
4317
4300
  results.push(omitUndefinedProperties({
4318
4301
  agent: pr.agent,
4302
+ ...(pr.sessionName ? { sessionName: pr.sessionName } : {}),
4319
4303
  context: pr.context,
4320
4304
  agentContract: pr.agentContract,
4321
4305
  launchContractDigest: pr.launchContractDigest,
@@ -4331,9 +4315,6 @@ async function runSubagent(
4331
4315
  interrupted: pr.interrupted,
4332
4316
  timedOut: pr.timedOut,
4333
4317
  stopped: pr.stopped,
4334
- turnBudget: pr.turnBudget,
4335
- turnBudgetExceeded: pr.turnBudgetExceeded,
4336
- wrapUpRequested: pr.wrapUpRequested,
4337
4318
  toolBudget: pr.toolBudget,
4338
4319
  toolBudgetBlocked: pr.toolBudgetBlocked,
4339
4320
  sessionFile: pr.sessionFile,
@@ -4343,6 +4324,7 @@ async function runSubagent(
4343
4324
  modelAttempts: pr.modelAttempts,
4344
4325
  contextOverflow: pr.contextOverflow,
4345
4326
  totalCost: pr.totalCost,
4327
+ usage: pr.usage,
4346
4328
  artifactPaths: pr.artifactPaths,
4347
4329
  transcriptPath: pr.transcriptPath,
4348
4330
  transcriptError: pr.transcriptError,
@@ -4486,6 +4468,23 @@ async function runSubagent(
4486
4468
  ? omitUndefinedProperties({ hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs })
4487
4469
  : undefined,
4488
4470
  baseDir: config.worktreeBaseDir,
4471
+ beforeCreate: (plannedSetup) => {
4472
+ for (const worktree of plannedSetup.worktrees) setStatusWorktreeReference(requiredStatusStep(statusPayload, groupStartFlatIndex + worktree.index), worktree);
4473
+ const pendingHandoff = writePendingParallelHandoff({
4474
+ manifestPath: parallelHandoffPath(asyncDir),
4475
+ runId: id,
4476
+ mode: (config.resultMode ?? statusPayload.mode) === "parallel" ? "parallel" : "chain",
4477
+ source: "async",
4478
+ cwd,
4479
+ stepIndex,
4480
+ flatStartIndex: groupStartFlatIndex,
4481
+ setup: plannedSetup,
4482
+ laneBindings: handoffWorkflowKey || config.lane ? [{ index: groupStartFlatIndex, taskIndex: 0, ...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}), ...(handoffChildRunId ? { runId: handoffChildRunId } : {}), ...(config.lane ? { lane: config.lane } : {}) }] : undefined,
4483
+ });
4484
+ statusPayload.parallelHandoff = pendingHandoff;
4485
+ statusPayload.lastUpdate = Date.now();
4486
+ writeStatusPayload();
4487
+ },
4489
4488
  }));
4490
4489
  } catch (error) {
4491
4490
  const setupError = error instanceof Error ? error.message : String(error);
@@ -4510,18 +4509,6 @@ async function runSubagent(
4510
4509
  }
4511
4510
 
4512
4511
  try {
4513
- if (worktreeSetup) {
4514
- writePendingParallelHandoff({
4515
- manifestPath: parallelHandoffPath(asyncDir),
4516
- runId: id,
4517
- mode: (config.resultMode ?? statusPayload.mode) === "parallel" ? "parallel" : "chain",
4518
- source: "async",
4519
- cwd,
4520
- stepIndex,
4521
- flatStartIndex: groupStartFlatIndex,
4522
- setup: worktreeSetup,
4523
- });
4524
- }
4525
4512
  if (group.worktree) ensureParallelProgressFile(cwd, group);
4526
4513
  const groupStartTime = Date.now();
4527
4514
  markParallelGroupRunning({
@@ -4558,12 +4545,12 @@ async function runSubagent(
4558
4545
  appendJsonl(eventsPath, JSON.stringify({
4559
4546
  type: "subagent.step.failed", ts: skippedAt, runId: id, stepIndex: fi, agent: task.agent, exitCode: 1, durationMs: 0,
4560
4547
  }));
4561
- return omitUndefinedProperties({ agent: task.agent, context: task.context, output: message, error: message, exitCode: 1 as number | null, skipped: true });
4548
+ return omitUndefinedProperties({ agent: task.agent, ...(task.sessionName ? { sessionName: task.sessionName } : {}), context: task.context, output: message, error: message, exitCode: 1 as number | null, skipped: true });
4562
4549
  }
4563
- if (timedOut) return timedOutStepResult(task.agent, task.context);
4564
- if (stopped) return stoppedStepResult(task.agent, task.context);
4550
+ if (timedOut) return timedOutStepResult(task.agent, task.context, task.sessionName);
4551
+ if (stopped) return stoppedStepResult(task.agent, task.context, task.sessionName);
4565
4552
  if (childStopRequests.has(fi)) return childStopResult(fi, task.agent, task.context);
4566
- if (interrupted) return pausedStepResult(task.agent, task.context);
4553
+ if (interrupted) return pausedStepResult(task.agent, task.context, task.sessionName);
4567
4554
  if (aborted && failFast) {
4568
4555
  const skippedAt = Date.now();
4569
4556
  requiredStatusStep(statusPayload, fi).status = "failed";
@@ -4578,7 +4565,7 @@ async function runSubagent(
4578
4565
  appendJsonl(eventsPath, JSON.stringify({
4579
4566
  type: "subagent.step.failed", ts: skippedAt, runId: id, stepIndex: fi, agent: task.agent, exitCode: -1, durationMs: 0,
4580
4567
  }));
4581
- return omitUndefinedProperties({ agent: task.agent, context: task.context, output: "(skipped — fail-fast)", exitCode: -1 as number | null, skipped: true });
4568
+ return omitUndefinedProperties({ agent: task.agent, ...(task.sessionName ? { sessionName: task.sessionName } : {}), context: task.context, output: "(skipped — fail-fast)", exitCode: -1 as number | null, skipped: true });
4582
4569
  }
4583
4570
 
4584
4571
  const taskStartTime = Date.now();
@@ -4626,13 +4613,11 @@ async function runSubagent(
4626
4613
  registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
4627
4614
  registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
4628
4615
  registerStop: (stop) => registerStepStop(fi, stop),
4629
- registerTurnBudgetAbort: (abort) => registerStepTurnBudgetAbort(fi, abort),
4630
4616
  timeoutSignal: timeoutAbortController.signal,
4631
4617
  stopSignal: stopAbortController.signal,
4632
4618
  trackedMutationEvidenceForCompletionGuard: Boolean(worktreeSetup),
4633
4619
  timeoutMessage,
4634
4620
  stopMessage,
4635
- turnBudget: config.turnBudget,
4636
4621
  toolTimeoutMs: taskForRun.toolTimeoutMs ?? config.toolTimeoutMs,
4637
4622
  onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking, attempt.contextLimit),
4638
4623
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
@@ -4640,6 +4625,7 @@ async function runSubagent(
4640
4625
  onExternalProcess: (process) => updateExternalProcess(fi, process),
4641
4626
  onExternalJob: (externalJob) => updateExternalJob(fi, externalJob),
4642
4627
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(fi),
4628
+ usageBudgetExhausted: () => refreshUsageBudget()?.exhausted === true,
4643
4629
  orcaProgressTab,
4644
4630
  }), config.deadlineAt);
4645
4631
  if (task.sessionFile) {
@@ -4657,16 +4643,11 @@ async function runSubagent(
4657
4643
  requiredStatusStep(statusPayload, fi).exitCode = stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
4658
4644
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "timedOut", timedOut || singleResult.timedOut ? true : undefined);
4659
4645
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "stopped", stopped || childStopped ? true : undefined);
4660
- setOptionalProperty(requiredStatusStep(statusPayload, fi), "turnBudget", singleResult.turnBudget);
4661
- setOptionalProperty(requiredStatusStep(statusPayload, fi), "turnBudgetExceeded", singleResult.turnBudgetExceeded);
4662
- setOptionalProperty(requiredStatusStep(statusPayload, fi), "wrapUpRequested", singleResult.wrapUpRequested);
4663
4646
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "toolBudget", singleResult.toolBudget);
4664
4647
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "toolBudgetBlocked", singleResult.toolBudgetBlocked);
4665
4648
  if (singleResult.toolBudget) statusPayload.toolBudget = singleResult.toolBudget;
4666
4649
  if (singleResult.toolBudgetBlocked) statusPayload.toolBudgetBlocked = true;
4667
- if (singleResult.turnBudget) statusPayload.turnBudget = singleResult.turnBudget;
4668
- if (singleResult.turnBudgetExceeded) statusPayload.turnBudgetExceeded = true;
4669
- if (singleResult.wrapUpRequested) statusPayload.wrapUpRequested = true;
4650
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "sessionName", singleResult.sessionName);
4670
4651
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "model", singleResult.model);
4671
4652
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "thinking", resolveEffectiveThinking(singleResult.model, requiredStatusStep(statusPayload, fi).thinking));
4672
4653
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "attemptedModels", singleResult.attemptedModels);
@@ -4775,9 +4756,6 @@ async function runSubagent(
4775
4756
  interrupted: pr.interrupted,
4776
4757
  timedOut: pr.timedOut,
4777
4758
  stopped: pr.stopped,
4778
- turnBudget: pr.turnBudget,
4779
- turnBudgetExceeded: pr.turnBudgetExceeded,
4780
- wrapUpRequested: pr.wrapUpRequested,
4781
4759
  toolBudget: pr.toolBudget,
4782
4760
  toolBudgetBlocked: pr.toolBudgetBlocked,
4783
4761
  sessionFile: pr.sessionFile,
@@ -4787,6 +4765,7 @@ async function runSubagent(
4787
4765
  modelAttempts: pr.modelAttempts,
4788
4766
  contextOverflow: pr.contextOverflow,
4789
4767
  totalCost: pr.totalCost,
4768
+ usage: pr.usage,
4790
4769
  artifactPaths: pr.artifactPaths,
4791
4770
  transcriptPath: pr.transcriptPath,
4792
4771
  transcriptError: pr.transcriptError,
@@ -4840,12 +4819,14 @@ async function runSubagent(
4840
4819
  diffs: captured.diffs,
4841
4820
  results: parallelResults.map((result) => ({
4842
4821
  agent: result.agent,
4822
+ ...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}),
4823
+ ...(handoffChildRunId ? { runId: handoffChildRunId } : {}),
4824
+ ...(config.lane ? { lane: config.lane } : {}),
4843
4825
  status: result.stopped || (result.exitCode !== 0 && isUnexplainedProcessSignal(omitUndefinedProperties({
4844
4826
  processSignal: result.processSignal,
4845
4827
  interrupted: result.interrupted,
4846
4828
  timedOut: result.timedOut,
4847
4829
  stopped: result.stopped,
4848
- turnBudgetExceeded: result.turnBudgetExceeded,
4849
4830
  }))) ? "stopped" as const : result.interrupted ? "paused" as const : result.exitCode === 0 ? "completed" as const : "failed" as const,
4850
4831
  summary: result.output || result.error || "(no output)",
4851
4832
  ...(result.artifactPaths?.outputPath ? { outputPath: result.artifactPaths.outputPath } : {}),
@@ -4891,12 +4872,12 @@ async function runSubagent(
4891
4872
  } else {
4892
4873
  const seqStep = step as SubagentStep;
4893
4874
  if (timedOut) {
4894
- results.push(timedOutStepResult(seqStep.agent, seqStep.context));
4875
+ results.push(timedOutStepResult(seqStep.agent, seqStep.context, seqStep.sessionName));
4895
4876
  flatIndex++;
4896
4877
  continue;
4897
4878
  }
4898
4879
  if (stopped) {
4899
- results.push(stoppedStepResult(seqStep.agent, seqStep.context));
4880
+ results.push(stoppedStepResult(seqStep.agent, seqStep.context, seqStep.sessionName));
4900
4881
  flatIndex++;
4901
4882
  continue;
4902
4883
  }
@@ -4906,7 +4887,7 @@ async function runSubagent(
4906
4887
  continue;
4907
4888
  }
4908
4889
  if (interrupted) {
4909
- results.push(pausedStepResult(seqStep.agent, seqStep.context));
4890
+ results.push(pausedStepResult(seqStep.agent, seqStep.context, seqStep.sessionName));
4910
4891
  flatIndex++;
4911
4892
  continue;
4912
4893
  }
@@ -4919,17 +4900,27 @@ async function runSubagent(
4919
4900
  ? omitUndefinedProperties({ hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs })
4920
4901
  : undefined,
4921
4902
  baseDir: config.worktreeBaseDir,
4903
+ beforeCreate: (plannedSetup) => {
4904
+ const worktree = plannedSetup.worktrees[0];
4905
+ if (worktree) {
4906
+ setStatusWorktreeReference(requiredStatusStep(statusPayload, flatIndex), worktree);
4907
+ }
4908
+ const pendingHandoff = writePendingParallelHandoff({
4909
+ manifestPath: parallelHandoffPath(asyncDir),
4910
+ runId: id,
4911
+ mode: "single",
4912
+ source: "async",
4913
+ cwd,
4914
+ stepIndex,
4915
+ flatStartIndex: flatIndex,
4916
+ setup: plannedSetup,
4917
+ laneBindings: handoffWorkflowKey || config.lane ? [{ index: flatIndex, taskIndex: 0, ...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}), ...(handoffChildRunId ? { runId: handoffChildRunId } : {}), ...(config.lane ? { lane: config.lane } : {}) }] : undefined,
4918
+ });
4919
+ statusPayload.parallelHandoff = pendingHandoff;
4920
+ statusPayload.lastUpdate = Date.now();
4921
+ writeStatusPayload();
4922
+ },
4922
4923
  }));
4923
- writePendingParallelHandoff({
4924
- manifestPath: parallelHandoffPath(asyncDir),
4925
- runId: id,
4926
- mode: "single",
4927
- source: "async",
4928
- cwd,
4929
- stepIndex,
4930
- flatStartIndex: flatIndex,
4931
- setup: singleWorktreeSetup,
4932
- });
4933
4924
  } catch (error) {
4934
4925
  if (singleWorktreeSetup) cleanupWorktrees(singleWorktreeSetup);
4935
4926
  throw error;
@@ -4982,12 +4973,10 @@ async function runSubagent(
4982
4973
  registerInterrupt: (interrupt) => registerStepInterrupt(flatIndex, interrupt),
4983
4974
  registerTimeout: (interrupt) => registerStepTimeout(flatIndex, interrupt),
4984
4975
  registerStop: (stop) => registerStepStop(flatIndex, stop),
4985
- registerTurnBudgetAbort: (abort) => registerStepTurnBudgetAbort(flatIndex, abort),
4986
4976
  timeoutSignal: timeoutAbortController.signal,
4987
4977
  stopSignal: stopAbortController.signal,
4988
4978
  timeoutMessage,
4989
4979
  stopMessage,
4990
- turnBudget: config.turnBudget,
4991
4980
  toolTimeoutMs: seqStep.toolTimeoutMs ?? config.toolTimeoutMs,
4992
4981
  onAttemptStart: (attempt) => updateStepModel(flatIndex, attempt.model, attempt.thinking, attempt.contextLimit),
4993
4982
  onChildEvent: (event) => updateStepFromChildEvent(flatIndex, event),
@@ -4995,6 +4984,7 @@ async function runSubagent(
4995
4984
  onExternalProcess: (process) => updateExternalProcess(flatIndex, process),
4996
4985
  onExternalJob: (externalJob) => updateExternalJob(flatIndex, externalJob),
4997
4986
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(flatIndex),
4987
+ usageBudgetExhausted: () => refreshUsageBudget()?.exhausted === true,
4998
4988
  orcaProgressTab,
4999
4989
  }), config.deadlineAt);
5000
4990
  } catch (error) {
@@ -5009,6 +4999,7 @@ async function runSubagent(
5009
4999
  const childStopped = singleResult.stopped === true;
5010
5000
  results.push(omitUndefinedProperties({
5011
5001
  agent: singleResult.agent,
5002
+ ...(singleResult.sessionName ? { sessionName: singleResult.sessionName } : {}),
5012
5003
  context: singleResult.context,
5013
5004
  agentContract: singleResult.agentContract,
5014
5005
  launchContractDigest: singleResult.launchContractDigest,
@@ -5027,6 +5018,7 @@ async function runSubagent(
5027
5018
  modelAttempts: singleResult.modelAttempts,
5028
5019
  contextOverflow: singleResult.contextOverflow,
5029
5020
  totalCost: singleResult.totalCost,
5021
+ usage: singleResult.usage,
5030
5022
  artifactPaths: singleResult.artifactPaths,
5031
5023
  transcriptPath: singleResult.transcriptPath,
5032
5024
  transcriptError: singleResult.transcriptError,
@@ -5044,9 +5036,6 @@ async function runSubagent(
5044
5036
  interrupted: singleResult.interrupted,
5045
5037
  timedOut: timedOut || singleResult.timedOut ? true : undefined,
5046
5038
  stopped: stopped || childStopped ? true : undefined,
5047
- turnBudget: singleResult.turnBudget,
5048
- turnBudgetExceeded: singleResult.turnBudgetExceeded,
5049
- wrapUpRequested: singleResult.wrapUpRequested,
5050
5039
  toolBudget: singleResult.toolBudget,
5051
5040
  toolBudgetBlocked: singleResult.toolBudgetBlocked,
5052
5041
  runner: singleResult.runner,
@@ -5101,16 +5090,11 @@ async function runSubagent(
5101
5090
  requiredStatusStep(statusPayload, flatIndex).exitCode = stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
5102
5091
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "timedOut", timedOut || singleResult.timedOut ? true : undefined);
5103
5092
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "stopped", stopped || childStopped ? true : undefined);
5104
- setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "turnBudget", singleResult.turnBudget);
5105
- setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "turnBudgetExceeded", singleResult.turnBudgetExceeded);
5106
- setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "wrapUpRequested", singleResult.wrapUpRequested);
5107
5093
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "toolBudget", singleResult.toolBudget);
5108
5094
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "toolBudgetBlocked", singleResult.toolBudgetBlocked);
5109
5095
  if (singleResult.toolBudget) statusPayload.toolBudget = singleResult.toolBudget;
5110
5096
  if (singleResult.toolBudgetBlocked) statusPayload.toolBudgetBlocked = true;
5111
- if (singleResult.turnBudget) statusPayload.turnBudget = singleResult.turnBudget;
5112
- if (singleResult.turnBudgetExceeded) statusPayload.turnBudgetExceeded = true;
5113
- if (singleResult.wrapUpRequested) statusPayload.wrapUpRequested = true;
5097
+ setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "sessionName", singleResult.sessionName);
5114
5098
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "model", singleResult.model);
5115
5099
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "thinking", resolveEffectiveThinking(singleResult.model, requiredStatusStep(statusPayload, flatIndex).thinking));
5116
5100
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "attemptedModels", singleResult.attemptedModels);
@@ -5171,6 +5155,9 @@ async function runSubagent(
5171
5155
  diffs,
5172
5156
  results: [{
5173
5157
  agent: singleResult.agent,
5158
+ ...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}),
5159
+ ...(handoffChildRunId ? { runId: handoffChildRunId } : {}),
5160
+ ...(config.lane ? { lane: config.lane } : {}),
5174
5161
  status: singleResult.stopped ? "stopped" as const : singleResult.interrupted ? "paused" as const : singleResult.exitCode === 0 ? "completed" as const : "failed" as const,
5175
5162
  summary: singleResult.output || singleResult.error || "(no output)",
5176
5163
  ...(singleResult.artifactPaths?.outputPath ? { outputPath: singleResult.artifactPaths.outputPath } : {}),
@@ -5291,14 +5278,14 @@ async function runSubagent(
5291
5278
  if (!timedOut && !stopped && !interrupted && config.timeoutMs !== undefined && results.some((result) => result.timedOut === true && result.error === timeoutMessage)) {
5292
5279
  timedOut = true;
5293
5280
  }
5294
- const signalTerminated = !stopped && !timedOut && !turnBudgetExceeded && !interrupted && results.some((result) => result.exitCode !== 0 && isUnexplainedProcessSignal(omitUndefinedProperties({
5281
+ const signalTerminated = !stopped && !timedOut && !interrupted && results.some((result) => result.exitCode !== 0 && isUnexplainedProcessSignal(omitUndefinedProperties({
5295
5282
  processSignal: result.processSignal,
5296
5283
  interrupted: result.interrupted,
5297
5284
  timedOut: result.timedOut,
5298
5285
  stopped: result.stopped,
5299
- turnBudgetExceeded: result.turnBudgetExceeded,
5300
5286
  })));
5301
- statusPayload.state = stopped || signalTerminated ? "stopped" : timedOut || turnBudgetExceeded || usageBudgetExceeded || statusPayload.error ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed";
5287
+ const partialWithEvidence = !stopped && !signalTerminated && !timedOut && !usageBudgetExceeded && !interrupted && results.some(partialEvidenceResult) && !results.some(concreteFailureResult);
5288
+ statusPayload.state = stopped || signalTerminated ? "stopped" : timedOut || usageBudgetExceeded ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : partialWithEvidence ? "partial" : "failed";
5302
5289
  closeSteerInbox(asyncDir, statusPayload.state, (filePath, payload) => runPersistence.write(filePath, payload));
5303
5290
  disposeControlInbox();
5304
5291
  for (const request of consumeSteerRequests(asyncDir)) deliverSteerRequest(request);
@@ -5328,13 +5315,17 @@ async function runSubagent(
5328
5315
  statusPayload.timedOut = true;
5329
5316
  statusPayload.error = timeoutMessage ?? "Subagent timed out.";
5330
5317
  }
5331
- if (turnBudgetExceeded && !statusPayload.error) {
5332
- const budget = statusPayload.turnBudget;
5333
- statusPayload.error = budget ? turnBudgetExceededMessage(budget, budget.turnCount) : "Subagent exceeded turn budget.";
5334
- }
5335
5318
  if (usageBudgetExceeded && statusPayload.usageBudget && !statusPayload.error) {
5336
5319
  statusPayload.error = usageBudgetExceededMessage(statusPayload.usageBudget);
5337
5320
  }
5321
+ if (partialWithEvidence) {
5322
+ const partialResult = results.find(partialEvidenceResult);
5323
+ statusPayload.activityState = "needs_attention";
5324
+ statusPayload.error = partialResult?.error ?? statusPayload.error;
5325
+ for (const step of statusPayload.steps) {
5326
+ if (step.status === "failed") step.activityState = "needs_attention";
5327
+ }
5328
+ }
5338
5329
  statusPayload.endedAt = runEndedAt;
5339
5330
  statusPayload.lastUpdate = runEndedAt;
5340
5331
  setOptionalProperty(statusPayload, "sessionFile", effectiveSessionFile);
@@ -5344,8 +5335,10 @@ async function runSubagent(
5344
5335
  setOptionalProperty(statusPayload, "shareUrl", shareUrl);
5345
5336
  setOptionalProperty(statusPayload, "gistUrl", gistUrl);
5346
5337
  setOptionalProperty(statusPayload, "shareError", shareError);
5347
- if (statusPayload.state === "failed" && !statusPayload.error) {
5348
- const failedStep = statusPayload.steps.find((s) => s.status === "failed");
5338
+ if ((statusPayload.state === "failed" || statusPayload.state === "partial") && !statusPayload.error) {
5339
+ const concreteFailure = results.find(concreteFailureResult);
5340
+ const failedStep = concreteFailure ? undefined : statusPayload.steps.find((s) => s.status === "failed");
5341
+ if (concreteFailure?.error) statusPayload.error = concreteFailure.error;
5349
5342
  if (failedStep?.agent) {
5350
5343
  statusPayload.error = `Step failed: ${failedStep.agent}`;
5351
5344
  }
@@ -5357,20 +5350,18 @@ async function runSubagent(
5357
5350
  id,
5358
5351
  agent: agentName,
5359
5352
  mode: resultMode,
5360
- success: !stopped && !timedOut && !turnBudgetExceeded && !usageBudgetExceeded && !interrupted && !signalTerminated && results.every((r) => r.success),
5361
- state: stopped || signalTerminated ? "stopped" : timedOut || turnBudgetExceeded || usageBudgetExceeded ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed",
5362
- summary: stopped ? stopMessage : signalTerminated ? (statusPayload.error ?? "Subagent process terminated by signal.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? (statusPayload.error ?? "Subagent exceeded turn budget.") : usageBudgetExceeded ? (statusPayload.error ?? "Usage budget exhausted.") : interrupted ? "Paused after interrupt. Waiting for explicit next action." : summary,
5353
+ success: statusPayload.state === "complete",
5354
+ state: statusPayload.state,
5355
+ summary: stopped ? stopMessage : signalTerminated ? (statusPayload.error ?? "Subagent process terminated by signal.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : usageBudgetExceeded ? (statusPayload.error ?? "Usage budget exhausted.") : interrupted ? "Paused after interrupt. Waiting for explicit next action." : statusPayload.state === "partial" ? (statusPayload.error ?? summary) : summary,
5363
5356
  ...(config.timeoutMs !== undefined ? { timeoutMs: config.timeoutMs } : {}),
5364
5357
  ...(config.deadlineAt !== undefined ? { deadlineAt: config.deadlineAt } : {}),
5365
- ...(statusPayload.turnBudget ? { turnBudget: statusPayload.turnBudget } : {}),
5366
- ...(statusPayload.turnBudgetExceeded ? { turnBudgetExceeded: true } : {}),
5367
- ...(statusPayload.wrapUpRequested ? { wrapUpRequested: true } : {}),
5368
5358
  ...(statusPayload.toolBudget ? { toolBudget: statusPayload.toolBudget } : {}),
5369
5359
  ...(statusPayload.toolBudgetBlocked ? { toolBudgetBlocked: true } : {}),
5370
5360
  ...(statusPayload.usageBudget ? { usageBudget: statusPayload.usageBudget } : {}),
5371
- ...(stopped ? { stopped: true, error: stopMessage } : timedOut ? { timedOut: true, error: timeoutMessage ?? "Subagent timed out." } : turnBudgetExceeded ? { error: statusPayload.error ?? "Subagent exceeded turn budget." } : usageBudgetExceeded ? { error: statusPayload.error ?? "Usage budget exhausted." } : {}),
5361
+ ...(stopped ? { stopped: true, error: stopMessage } : timedOut ? { timedOut: true, error: timeoutMessage ?? "Subagent timed out." } : usageBudgetExceeded ? { error: statusPayload.error ?? "Usage budget exhausted." } : {}),
5372
5362
  results: results.map((r) => omitUndefinedProperties({
5373
5363
  agent: r.agent,
5364
+ ...(r.sessionName ? { sessionName: r.sessionName } : {}),
5374
5365
  context: r.context,
5375
5366
  output: r.output,
5376
5367
  outputState: r.outputState,
@@ -5382,9 +5373,6 @@ async function runSubagent(
5382
5373
  timedOut: r.timedOut || undefined,
5383
5374
  stopped: r.stopped || undefined,
5384
5375
  processSignal: r.processSignal || undefined,
5385
- turnBudget: r.turnBudget,
5386
- turnBudgetExceeded: r.turnBudgetExceeded || undefined,
5387
- wrapUpRequested: r.wrapUpRequested || undefined,
5388
5376
  toolBudget: r.toolBudget,
5389
5377
  toolBudgetBlocked: r.toolBudgetBlocked || undefined,
5390
5378
  sessionFile: r.sessionFile,
@@ -5394,6 +5382,7 @@ async function runSubagent(
5394
5382
  modelAttempts: r.modelAttempts,
5395
5383
  contextOverflow: r.contextOverflow,
5396
5384
  totalCost: r.totalCost,
5385
+ usage: r.usage,
5397
5386
  artifactPaths: r.artifactPaths,
5398
5387
  outputSaveError: r.outputSaveError,
5399
5388
  metadataSaveError: r.metadataSaveError,
@@ -5426,7 +5415,7 @@ async function runSubagent(
5426
5415
  capabilityAudit: statusPayload.capabilityAudit,
5427
5416
  ...(config.parentWorkflowRunId ? { parentWorkflowRunId: config.parentWorkflowRunId } : {}),
5428
5417
  ...(config.workflowKey ? { workflowKey: config.workflowKey } : {}),
5429
- exitCode: stopped || timedOut || turnBudgetExceeded || usageBudgetExceeded ? 1 : interrupted || results.every((r) => r.success) ? 0 : 1,
5418
+ exitCode: statusPayload.state === "complete" || statusPayload.state === "paused" ? 0 : 1,
5430
5419
  timestamp: runEndedAt,
5431
5420
  durationMs: runEndedAt - overallStartTime,
5432
5421
  totalTokens: statusPayload.totalTokens,