pi-subagents 0.58.0 → 0.60.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 (116) hide show
  1. package/CHANGELOG.md +102 -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 +93 -5
  8. package/docs/workflows.md +94 -3
  9. package/package.json +3 -1
  10. package/prompts/review-loop.md +2 -2
  11. package/skills/council-mode/SKILL.md +48 -243
  12. package/skills/council-mode/references/pass-contracts.md +150 -0
  13. package/skills/pi-subagents/SKILL.md +87 -37
  14. package/skills/pi-subagents/references/constraints-and-recipes.md +29 -233
  15. package/skills/pi-subagents/references/execution-controls.md +52 -7
  16. package/skills/pi-subagents/references/management-authoring-rpc.md +3 -4
  17. package/skills/pi-subagents/references/multi-lane-orchestration.md +13 -1
  18. package/skills/pi-subagents/references/prompting-and-roles.md +54 -26
  19. package/skills/pi-subagents/references/review-and-validation.md +73 -0
  20. package/src/agents/agent-management.ts +159 -42
  21. package/src/agents/agent-serializer.ts +4 -2
  22. package/src/agents/agents.ts +67 -26
  23. package/src/agents/runtime-agent-registry.ts +9 -16
  24. package/src/api/background-work.ts +5 -1
  25. package/src/api/delegation.ts +0 -7
  26. package/src/api/preflight.ts +6 -9
  27. package/src/api/shared-types.ts +2 -0
  28. package/src/extension/fanout-child.ts +5 -3
  29. package/src/extension/index.ts +51 -23
  30. package/src/extension/public-execution.ts +16 -2
  31. package/src/extension/schemas.ts +36 -10
  32. package/src/extension/tool-description.ts +24 -5
  33. package/src/intercom/result-intercom.ts +2 -0
  34. package/src/profiles/profiles.ts +5 -6
  35. package/src/runs/background/active-async-capacity.ts +2 -2
  36. package/src/runs/background/async-execution.ts +52 -37
  37. package/src/runs/background/async-job-tracker.ts +17 -12
  38. package/src/runs/background/async-resume.ts +23 -25
  39. package/src/runs/background/async-status-snapshot.ts +23 -261
  40. package/src/runs/background/async-status.ts +110 -8
  41. package/src/runs/background/chain-append.ts +6 -3
  42. package/src/runs/background/chain-root-attachment.ts +60 -8
  43. package/src/runs/background/control-channel.ts +3 -2
  44. package/src/runs/background/fleet-view.ts +21 -11
  45. package/src/runs/background/notify.ts +158 -6
  46. package/src/runs/background/result-files.ts +2 -1
  47. package/src/runs/background/result-watcher.ts +2 -0
  48. package/src/runs/background/resume-guidance.ts +1 -1
  49. package/src/runs/background/retained-children.ts +1 -1
  50. package/src/runs/background/run-status.ts +30 -9
  51. package/src/runs/background/scheduled-runs.ts +86 -7
  52. package/src/runs/background/stale-run-reconciler.ts +10 -4
  53. package/src/runs/background/steering.ts +4 -14
  54. package/src/runs/background/subagent-runner.ts +351 -358
  55. package/src/runs/background/subagent-wait.ts +68 -12
  56. package/src/runs/background/terminal-run-index.ts +1 -1
  57. package/src/runs/background/wait-completions.ts +25 -1
  58. package/src/runs/background/wait-config.ts +23 -9
  59. package/src/runs/background/wait-tool.ts +9 -2
  60. package/src/runs/foreground/async-steering-action.ts +2 -2
  61. package/src/runs/foreground/execution.ts +125 -122
  62. package/src/runs/foreground/foreground-control.ts +3 -0
  63. package/src/runs/foreground/foreground-history.ts +1 -0
  64. package/src/runs/foreground/subagent-executor.ts +576 -240
  65. package/src/runs/foreground/workflow-detach-reconcile.ts +99 -200
  66. package/src/runs/shared/abort-recovery.ts +119 -0
  67. package/src/runs/shared/async-status-projection.ts +597 -0
  68. package/src/runs/shared/background-process-options.ts +9 -0
  69. package/src/runs/shared/child-identity.ts +19 -4
  70. package/src/runs/shared/child-launch-plan.ts +151 -0
  71. package/src/runs/shared/completion-evidence.ts +89 -0
  72. package/src/runs/shared/completion-guard.ts +1 -1
  73. package/src/runs/shared/dynamic-fanout.ts +2 -2
  74. package/src/runs/shared/host-step-status.ts +230 -0
  75. package/src/runs/shared/lane-metadata.ts +105 -0
  76. package/src/runs/shared/mcp-config-sources.ts +42 -6
  77. package/src/runs/shared/mcp-direct-tool-allowlist.ts +93 -143
  78. package/src/runs/shared/mcp-direct-tool-grant.ts +194 -0
  79. package/src/runs/shared/model-fallback.ts +9 -2
  80. package/src/runs/shared/mutation-evidence.ts +52 -3
  81. package/src/runs/shared/nested-events.ts +6 -2
  82. package/src/runs/shared/nested-render.ts +7 -3
  83. package/src/runs/shared/parallel-handoff.ts +419 -7
  84. package/src/runs/shared/parallel-utils.ts +7 -0
  85. package/src/runs/shared/pi-args.ts +67 -3
  86. package/src/runs/shared/single-output.ts +72 -22
  87. package/src/runs/shared/subagent-prompt-runtime.ts +33 -6
  88. package/src/runs/shared/workflow-graph.ts +15 -0
  89. package/src/runs/shared/worktree-cleanup-plan.ts +847 -0
  90. package/src/runs/shared/worktree.ts +18 -0
  91. package/src/shared/child-session-name.ts +46 -0
  92. package/src/shared/extension-context.ts +24 -0
  93. package/src/shared/formatters.ts +5 -2
  94. package/src/shared/launch-contract.ts +1 -1
  95. package/src/shared/settings.ts +9 -103
  96. package/src/shared/types.ts +232 -30
  97. package/src/shared/utils.ts +35 -55
  98. package/src/slash/delegation-adapters.ts +1 -8
  99. package/src/slash/delegation-request.ts +0 -4
  100. package/src/slash/slash-bridge.ts +1 -2
  101. package/src/slash/slash-commands.ts +369 -90
  102. package/src/slash/slash-live-state.ts +22 -11
  103. package/src/tui/fleet-status.ts +134 -75
  104. package/src/tui/fleet.ts +11 -5
  105. package/src/tui/render-helpers.ts +31 -0
  106. package/src/tui/render.ts +895 -121
  107. package/src/watchdog/change-signature.ts +40 -1
  108. package/src/watchdog/turn-delta.ts +1 -1
  109. package/src/workflows/chat-progress.ts +6 -3
  110. package/src/workflows/host-command.ts +235 -0
  111. package/src/workflows/scripted-workflow.ts +503 -38
  112. package/src/workflows/workflow-child-summary.ts +9 -5
  113. package/src/workflows/workflow-preflight.ts +270 -0
  114. package/src/workflows/workflow-receipt.ts +43 -4
  115. package/src/workflows/workflow-settlement.ts +246 -0
  116. 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";
@@ -79,7 +75,8 @@ import {
79
75
  DEFAULT_GLOBAL_CONCURRENCY_LIMIT,
80
76
  Semaphore,
81
77
  } from "../shared/parallel-utils.ts";
82
- import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan, type SubagentTaskDelivery } from "../shared/pi-args.ts";
78
+ import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, deriveForkPromptCacheKey, 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,
@@ -1619,13 +1640,13 @@ async function runSingleStepInner(
1619
1640
  const writerProcesses: PiWriterProcessInstanceExitV1[] = [];
1620
1641
  let writerAttemptCount = 0;
1621
1642
  const attemptNotes: string[] = [];
1643
+ let finalRequiredOutputMissing: boolean | undefined;
1622
1644
  const eventsPath = path.join(path.dirname(ctx.outputFile), "events.jsonl");
1623
1645
  let finalResult: RunPiStreamingResult | undefined;
1624
1646
  let finalOutputSnapshot: SingleOutputSnapshot | undefined;
1625
1647
  let structuredAcceptanceReport: unknown;
1626
1648
  let structuredAcceptanceReportError: string | undefined;
1627
1649
  let completionGuardTriggeredFinal = false;
1628
- let turnBudget = ctx.turnBudget ? initialTurnBudgetState(ctx.turnBudget) : undefined;
1629
1650
  let toolBudget = step.toolBudget ? initialToolBudgetState(step.toolBudget) : undefined;
1630
1651
  let toolBudgetBlocked = false;
1631
1652
  let actualLaunchContractDigest = step.launchContractDigest;
@@ -1639,8 +1660,12 @@ async function runSingleStepInner(
1639
1660
  let taskDeliveryOverride: SubagentTaskDelivery | undefined;
1640
1661
  let contextOverflow = false;
1641
1662
  let launchWarningsEmitted = false;
1663
+ let abortRecoveryAttempted = false;
1664
+ let nextAttemptTask = task;
1642
1665
  modelAttemptsLoop: while (modelIndex < candidates.length) {
1643
1666
  if (ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break;
1667
+ const recoveringAbort = abortRecoveryAttempted;
1668
+ const attemptTask = nextAttemptTask;
1644
1669
  const candidate = candidates[modelIndex];
1645
1670
  const expectedModelForVerification = candidate && !(step.skipPrimaryModelVerification && modelIndex === 0) ? candidate : undefined;
1646
1671
  try {
@@ -1674,8 +1699,9 @@ async function runSingleStepInner(
1674
1699
  : undefined;
1675
1700
  const { args, env, tempDir, toolDiagnosticPath, runtimeAcknowledgedExtensionsPath, capabilityAudit: attemptCapabilityAudit, warnings } = buildPiArgs(omitUndefinedProperties({
1676
1701
  parentSessionId: step.parentSessionId,
1702
+ forkCacheKey: step.context === "fork" ? deriveForkPromptCacheKey(step.parentSessionId) : undefined,
1677
1703
  baseArgs: ["--mode", "json", "-p"],
1678
- task,
1704
+ task: attemptTask,
1679
1705
  taskDelivery: taskDeliveryOverride,
1680
1706
  sessionEnabled,
1681
1707
  sessionDir,
@@ -1686,11 +1712,12 @@ async function runSingleStepInner(
1686
1712
  inheritSkills: step.inheritSkills,
1687
1713
  requireReadTool: Boolean(step.skills?.length),
1688
1714
  tools: step.tools,
1715
+ allowNestedSubagents: step.allowNestedSubagents,
1689
1716
  extensions: step.extensions,
1690
1717
  subagentOnlyExtensions: step.subagentOnlyExtensions,
1691
1718
  fast: step.fast,
1692
1719
  modelCandidates: step.modelCandidates,
1693
- systemPrompt: appendTurnBudgetSystemPrompt(step.systemPrompt ?? "", ctx.turnBudget),
1720
+ systemPrompt: step.systemPrompt ?? "",
1694
1721
  systemPromptMode: step.systemPromptMode,
1695
1722
  mcpDirectTools: step.mcpDirectTools,
1696
1723
  mcpConfig: step.mcpConfig,
@@ -1699,6 +1726,7 @@ async function runSingleStepInner(
1699
1726
  cwd: step.cwd ?? ctx.cwd,
1700
1727
  promptFileStem: step.agent,
1701
1728
  intercomSessionName: ctx.childIntercomTarget,
1729
+ sessionName: childSessionName,
1702
1730
  orchestratorIntercomTarget: ctx.orchestratorIntercomTarget,
1703
1731
  runId: ctx.id,
1704
1732
  childAgentName: step.agent,
@@ -1722,6 +1750,7 @@ async function runSingleStepInner(
1722
1750
  : undefined,
1723
1751
  childWatchdog,
1724
1752
  waitToolEnabled: step.waitToolEnabled,
1753
+ waitToolDefaultTimeoutMs: step.waitToolDefaultTimeoutMs,
1725
1754
  thinkingCeiling: step.thinkingCeiling,
1726
1755
  extensionBindings,
1727
1756
  }));
@@ -1732,6 +1761,7 @@ async function runSingleStepInner(
1732
1761
  if (step.definitionDigest) {
1733
1762
  const toolPlan = resolvedTaskToolPlan ?? resolvePiLaunchToolPlan(omitUndefinedProperties({
1734
1763
  tools: step.tools,
1764
+ allowNestedSubagents: step.allowNestedSubagents,
1735
1765
  extensions: step.extensions,
1736
1766
  subagentOnlyExtensions: step.subagentOnlyExtensions,
1737
1767
  fast: step.fast,
@@ -1756,7 +1786,7 @@ async function runSingleStepInner(
1756
1786
  ...(step.fast !== undefined ? { fast: step.fast } : {}),
1757
1787
  ...(resolveEffectiveThinking(candidate, step.thinking) ? { thinking: resolveEffectiveThinking(candidate, step.thinking) } : {}),
1758
1788
  ...(step.thinkingCeiling ? { thinkingCeiling: step.thinkingCeiling } : {}),
1759
- systemPrompt: appendTurnBudgetSystemPrompt(step.systemPrompt ?? "", ctx.turnBudget),
1789
+ systemPrompt: step.systemPrompt ?? "",
1760
1790
  systemPromptMode: step.systemPromptMode,
1761
1791
  inheritProjectContext: step.inheritProjectContext,
1762
1792
  inheritGlobalContext: step.inheritGlobalContext,
@@ -1789,7 +1819,6 @@ async function runSingleStepInner(
1789
1819
  ctx.timeoutMessage,
1790
1820
  ctx.registerStop,
1791
1821
  ctx.stopMessage,
1792
- ctx.registerTurnBudgetAbort,
1793
1822
  ctx.onWriterProcess,
1794
1823
  ctx.toolTimeoutMs,
1795
1824
  ctx.deadlineAt,
@@ -1809,25 +1838,6 @@ async function runSingleStepInner(
1809
1838
  processTree: run.processTree,
1810
1839
  });
1811
1840
  }
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
1841
  const toolAvailabilityError = run.exitCode === 0 && !run.error
1832
1842
  ? readChildToolDiagnosticError(toolDiagnosticPath)
1833
1843
  : undefined;
@@ -1838,7 +1848,6 @@ async function runSingleStepInner(
1838
1848
  && !run.interrupted
1839
1849
  && !run.timedOut
1840
1850
  && !run.stopped
1841
- && !run.turnBudgetExceeded
1842
1851
  && !run.protocolError
1843
1852
  && !toolAvailabilityError
1844
1853
  ? formatMidToolExitError({
@@ -1876,14 +1885,17 @@ async function runSingleStepInner(
1876
1885
  const hiddenError = run.exitCode === 0 && !run.error && !toolAvailabilityError && !structuredError && !midToolExitError
1877
1886
  ? detectSubagentError(errorMessages)
1878
1887
  : null;
1888
+ const terminalEmptyAfterUsefulWork = !validatedStructuredOutput
1889
+ && hasEmptyTerminalAssistantResponse(run.messages)
1890
+ && (run.toolCount > 0 || Boolean(run.finalOutput.trim()));
1879
1891
  const emptyOutputError = run.exitCode === 0
1880
1892
  && !run.error
1881
1893
  && !toolAvailabilityError
1882
1894
  && !structuredError
1883
- && !run.finalOutput.trim()
1884
1895
  && !validatedStructuredOutput
1896
+ && (!run.finalOutput.trim() || terminalEmptyAfterUsefulWork)
1885
1897
  && (!hiddenError?.hasError || hasEmptyTerminalAssistantResponse(run.messages))
1886
- ? "Subagent produced no output (possible model cold-start or empty response)."
1898
+ ? formatEmptyTerminalAssistantResponseError(run.messages)
1887
1899
  : undefined;
1888
1900
  const completionGuardEnabled = isAgentContractV1(step.agentContract) ? step.completionGuard === true : step.completionGuard !== false;
1889
1901
  const completionToolPlan = resolvedTaskToolPlan;
@@ -1904,25 +1916,25 @@ async function runSingleStepInner(
1904
1916
  }))
1905
1917
  : undefined;
1906
1918
  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."
1919
+ const completionEvidence = planCompletionEvidence({
1920
+ guard: completionGuard,
1921
+ completionGuardEnabled,
1922
+ mutationCapable: hasMutationToolCapability(completionTools, completionToolPlan?.effectiveMcpTools ?? step.mcpDirectTools),
1923
+ implementationMutationExpected: expectsImplementationMutation(step.agent, taskForCompletionGuard),
1924
+ mutationAttemptObserved,
1925
+ mutationEvidence: completionMutationEvidence,
1926
+ agentContractV1: isAgentContractV1(step.agentContract),
1927
+ });
1928
+ const finalOutputHasPersistableFileContent = run.exitCode === 0 && !run.error && !emptyOutputError && Boolean(stripAcceptanceReport(run.finalOutput).trim());
1929
+ const requiredOutput = step.outputMode === "file-only" && step.outputPath
1930
+ ? { kind: "file-only" as const, path: step.outputPath, missing: !fs.existsSync(step.outputPath) && !finalOutputHasPersistableFileContent }
1931
+ : effectiveStructuredOutput
1932
+ ? { kind: "structured" as const, path: effectiveStructuredOutput.outputPath, missing: !fs.existsSync(effectiveStructuredOutput.outputPath) }
1924
1933
  : undefined;
1925
- const effectiveExitCode = toolAvailabilityError || (completionGuardTriggered && !isAgentContractV1(step.agentContract)) || midToolExitError || structuredError || emptyOutputError
1934
+ finalRequiredOutputMissing = requiredOutput?.missing;
1935
+ const missingRequiredOutputError = formatRequiredOutputError(requiredOutput);
1936
+ const missingRequiredOutputAfterMutation = Boolean(missingRequiredOutputError) && (mutationAttemptObserved || Boolean(mutationEvidence.changedFiles.length));
1937
+ const effectiveExitCode = toolAvailabilityError || completionEvidence.legacyFailureError || midToolExitError || structuredError || emptyOutputError || missingRequiredOutputError
1926
1938
  ? 1
1927
1939
  : hiddenError?.hasError
1928
1940
  ? (hiddenError.exitCode ?? 1)
@@ -1934,19 +1946,23 @@ async function runSingleStepInner(
1934
1946
  interrupted: run.interrupted,
1935
1947
  timedOut: run.timedOut,
1936
1948
  stopped: run.stopped,
1937
- turnBudgetExceeded: run.turnBudgetExceeded,
1938
1949
  })) ? formatProcessSignalError(run.processSignal!) : undefined;
1950
+ const underlyingError = toolAvailabilityError
1951
+ ?? midToolExitError
1952
+ ?? structuredError
1953
+ ?? run.error
1954
+ ?? signalError
1955
+ ?? (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined)
1956
+ ?? ((emptyOutputError || missingRequiredOutputError) && run.stderr.trim() ? run.stderr.trim() : undefined)
1957
+ ?? emptyOutputError
1958
+ ?? (missingRequiredOutputAfterMutation ? missingRequiredOutputError : undefined)
1959
+ ?? (hiddenError?.hasError
1960
+ ? hiddenError.details
1961
+ ? `${hiddenError.errorType} failed (exit ${effectiveExitCode}): ${hiddenError.details}`
1962
+ : `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
1963
+ : undefined);
1939
1964
  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)),
1965
+ underlyingError ?? missingRequiredOutputError ?? completionEvidence.legacyFailureError,
1950
1966
  {
1951
1967
  agent: step.agent,
1952
1968
  ambientExtensionsEnabled: launchResolvedExtensions?.disableAmbientExtensions === false,
@@ -1960,8 +1976,8 @@ async function runSingleStepInner(
1960
1976
  usage: run.usage,
1961
1977
  });
1962
1978
  modelAttempts.push(attempt);
1963
- if (candidate && startupAttemptIndex === 0) attemptedModels.push(candidate);
1964
- completionGuardTriggeredFinal = completionGuardTriggered;
1979
+ if (!recoveringAbort && candidate && startupAttemptIndex === 0) attemptedModels.push(candidate);
1980
+ completionGuardTriggeredFinal = completionEvidence.guardTriggered && !underlyingError && !missingRequiredOutputError;
1965
1981
  finalOutputSnapshot = outputSnapshot;
1966
1982
  if (step.toolBudget) {
1967
1983
  const toolMessages = run.messages.filter((message) => message.role === "toolResult");
@@ -1969,27 +1985,48 @@ async function runSingleStepInner(
1969
1985
  toolBudgetBlocked = Boolean(blockedMessage);
1970
1986
  toolBudget = toolBudgetState(step.toolBudget, toolMessages.length, blockedMessage ? (blockedMessage as { toolName?: string }).toolName : undefined);
1971
1987
  }
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;
1988
+ const settlementDiagnostic = projectSettlementDiagnostic(completionEvidence, {
1989
+ terminalFailed: effectiveExitCode !== 0,
1990
+ finalTextPresent: Boolean(stripAcceptanceReport(run.finalOutput).trim()),
1991
+ mutationObserved: mutationEvidence.attemptedMutation,
1992
+ requiredOutput,
1993
+ afterCompactionSettlement: run.afterCompactionSettlement === true,
1994
+ });
1995
+ const fileMutationEffect = completionEvidence.fileMutation ?? (missingRequiredOutputAfterMutation ? { status: "observed" as const, expected: completionEvidence.mutationExpected, attempted: true, evidence: mutationEvidence } : undefined);
1989
1996
  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;
1997
+ const abortRecovery = !attempt.success ? planAbortRecovery({
1998
+ messages: run.messages,
1999
+ error,
2000
+ processSignal: run.processSignal,
2001
+ sessionAvailable: Boolean(step.sessionFile && fs.existsSync(step.sessionFile)),
2002
+ alreadyResumed: abortRecoveryAttempted,
2003
+ stopped: run.stopped || ctx.stopSignal?.aborted || ctx.skipAcceptance?.(),
2004
+ interrupted: run.interrupted,
2005
+ timedOut: run.timedOut || ctx.timeoutSignal?.aborted,
2006
+ toolBudgetExhausted: run.toolBudgetBlocked || toolBudgetBlocked,
2007
+ usageBudgetExhausted: ctx.usageBudgetExhausted?.(),
2008
+ structuredOutputFailed: Boolean(structuredError),
2009
+ acceptanceFailed: false,
2010
+ currentTool: run.currentTool,
2011
+ afterCompactionSettlement: run.afterCompactionSettlement,
2012
+ }) : undefined;
2013
+ if (abortRecovery?.action === "settle" && abortRecovery.diagnostic) {
2014
+ attempt.error = attempt.error
2015
+ ? `${abortRecovery.diagnostic}\n${attempt.error.slice(0, 8_000)}`
2016
+ : abortRecovery.diagnostic;
2017
+ if (finalResult) finalResult.abortRecoveryDiagnostic = abortRecovery.diagnostic;
2018
+ }
1991
2019
  if (run.stopped || run.timedOut || ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break modelAttemptsLoop;
1992
- if (attempt.success || completionGuardTriggered) break modelAttemptsLoop;
2020
+ if (abortRecovery?.action === "settle" && abortRecovery.diagnostic) break modelAttemptsLoop;
2021
+ if (attempt.success) break modelAttemptsLoop;
2022
+ if (recoveringAbort) break modelAttemptsLoop;
2023
+ if (abortRecovery?.action === "resume") {
2024
+ abortRecoveryAttempted = true;
2025
+ nextAttemptTask = abortRecovery.prompt;
2026
+ attemptNotes.push("[abort-recovery] provider/transport abort after useful progress; resuming the retained child session once.");
2027
+ continue;
2028
+ }
2029
+ if (completionEvidence.guardTriggered) break modelAttemptsLoop;
1993
2030
 
1994
2031
  const startupFailure = isRetryableSubagentStartupFailure(omitUndefinedProperties({
1995
2032
  exitCode: effectiveExitCode,
@@ -2005,7 +2042,6 @@ async function runSingleStepInner(
2005
2042
  interrupted: run.interrupted,
2006
2043
  timedOut: run.timedOut,
2007
2044
  stopped: run.stopped,
2008
- turnBudgetExceeded: run.turnBudgetExceeded,
2009
2045
  }));
2010
2046
  const retryDelayMs = SUBAGENT_STARTUP_RETRY_DELAYS_MS[startupAttemptIndex];
2011
2047
  if (startupFailure && retryDelayMs !== undefined) {
@@ -2052,8 +2088,14 @@ async function runSingleStepInner(
2052
2088
  const rawOutput = finalResult?.finalOutput ?? "";
2053
2089
  const outputForPersistence = stripAcceptanceReport(rawOutput);
2054
2090
  const resolvedOutput = step.outputPath && finalResult?.exitCode === 0
2055
- ? resolveSingleOutput(step.outputPath, outputForPersistence, finalOutputSnapshot)
2091
+ ? resolveSingleOutput(step.outputPath, outputForPersistence, finalOutputSnapshot, step.outputClaimPath)
2056
2092
  : { fullOutput: outputForPersistence };
2093
+ if (resolvedOutput.fatalError) {
2094
+ if (finalResult) {
2095
+ finalResult.exitCode = 1;
2096
+ finalResult.error = finalResult.error ? `${finalResult.error}\n${resolvedOutput.saveError}` : resolvedOutput.saveError;
2097
+ }
2098
+ }
2057
2099
  const output = stripAcceptanceReport(resolvedOutput.fullOutput);
2058
2100
  const outputReference = resolvedOutput.savedPath ? formatSavedOutputReference(resolvedOutput.savedPath, output) : undefined;
2059
2101
  let outputForSummary = output;
@@ -2062,14 +2104,6 @@ async function runSingleStepInner(
2062
2104
  }
2063
2105
  if (finalResult?.stopped && !outputForSummary.trim()) {
2064
2106
  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
2107
  }
2074
2108
  const outputForAcceptance = rawOutput;
2075
2109
  const childWrittenOutput = step.outputPath
@@ -2086,6 +2120,7 @@ async function runSingleStepInner(
2086
2120
  ? buildTimeoutRecoverySummary({
2087
2121
  termination: "timed-out",
2088
2122
  evidence: finalMutationEvidence,
2123
+ requiredOutputMissing: finalRequiredOutputMissing,
2089
2124
  currentTool: finalResult?.currentTool,
2090
2125
  currentToolArgs: finalResult?.currentToolArgs,
2091
2126
  currentPath: finalResult?.currentPath,
@@ -2107,7 +2142,7 @@ async function runSingleStepInner(
2107
2142
  saveError: resolvedOutput.saveError,
2108
2143
  }));
2109
2144
  outputForSummary = finalizedOutput.displayOutput;
2110
- const acceptance = step.effectiveAcceptance && !finalResult?.stopped && !finalResult?.turnBudgetExceeded && !ctx.timeoutSignal?.aborted && !ctx.stopSignal?.aborted && !ctx.skipAcceptance?.()
2145
+ const acceptance = step.effectiveAcceptance && !finalResult?.stopped && !ctx.timeoutSignal?.aborted && !ctx.stopSignal?.aborted && !ctx.skipAcceptance?.()
2111
2146
  ? await evaluateAcceptance(omitUndefinedProperties({
2112
2147
  acceptance: step.effectiveAcceptance,
2113
2148
  output: outputForAcceptance,
@@ -2126,39 +2161,31 @@ async function runSingleStepInner(
2126
2161
  : undefined;
2127
2162
  const stoppedAfterAcceptance = finalResult?.stopped === true || ctx.stopSignal?.aborted === true;
2128
2163
  const timedOutAfterAcceptance = !stoppedAfterAcceptance && (finalResult?.timedOut === true || ctx.timeoutSignal?.aborted === true);
2129
- const turnBudgetExceeded = finalResult?.turnBudgetExceeded === true;
2130
2164
  const effectiveAcceptance = step.effectiveAcceptance
2131
2165
  ? stoppedAfterAcceptance
2132
2166
  ? buildSkippedAcceptanceLedger(step.effectiveAcceptance, { id: "stopped", message: "Acceptance was not evaluated because the subagent was stopped." })
2133
2167
  : timedOutAfterAcceptance
2134
2168
  ? 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
2169
+ : acceptance
2138
2170
  : undefined;
2139
2171
  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;
2172
+ const acceptanceCanFailRun = acceptanceFailure && effectiveAcceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted && !timedOutAfterAcceptance && !stoppedAfterAcceptance && !isAgentContractV1(step.agentContract);
2173
+ const effectiveFinalExitCode = timedOutAfterAcceptance || stoppedAfterAcceptance ? 1 : acceptanceCanFailRun ? 1 : finalResult?.exitCode ?? 1;
2142
2174
  const intercomDetachReceipt = finalResult?.finalOutput === INTERCOM_DETACH_RECEIPT;
2143
2175
  const baseFinalError = stoppedAfterAcceptance
2144
2176
  ? ctx.stopMessage ?? "Subagent stopped by user."
2145
2177
  : timedOutAfterAcceptance
2146
2178
  ? finalResult?.error ?? ctx.timeoutMessage ?? "Subagent timed out."
2147
- : turnBudgetExceeded
2148
- ? finalResult?.error ?? (turnBudget ? turnBudgetExceededMessage(turnBudget, turnBudget.turnCount) : "Subagent exceeded turn budget.")
2149
- : acceptanceCanFailRun
2179
+ : acceptanceCanFailRun
2150
2180
  ? (finalResult?.error ? `${finalResult.error}\n${acceptanceFailure}` : acceptanceFailure)
2151
2181
  : finalResult?.error ?? (intercomDetachReceipt ? INTERCOM_DETACH_RECEIPT : undefined);
2152
2182
  const effectiveFinalError = formatChildFailureDiagnostic({
2153
2183
  error: baseFinalError,
2154
2184
  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,
2185
+ abortRecoveryDiagnostic: effectiveFinalExitCode !== 0 ? finalResult?.abortRecoveryDiagnostic : undefined,
2186
+ requiredOutput: effectiveFinalExitCode !== 0 ? finalResult?.effects?.settlementDiagnostic?.requiredOutput : undefined,
2161
2187
  });
2188
+ const usage = usageFromAttempts(modelAttempts);
2162
2189
 
2163
2190
  const artifactErrors = artifactPaths && ctx.artifactConfig?.enabled !== false
2164
2191
  ? persistStepArtifacts({
@@ -2178,6 +2205,7 @@ async function runSingleStepInner(
2178
2205
  model: finalResult?.model,
2179
2206
  attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
2180
2207
  modelAttempts,
2208
+ usage,
2181
2209
  error: effectiveFinalError,
2182
2210
  acceptance: effectiveAcceptance,
2183
2211
  ...(capabilityAudit ? { capabilityCeiling: capabilityAudit.ceiling, capabilityAudit } : {}),
@@ -2194,6 +2222,7 @@ async function runSingleStepInner(
2194
2222
 
2195
2223
  const result: StepResult & { completionGuardTriggered?: boolean } = omitUndefinedProperties({
2196
2224
  agent: step.agent,
2225
+ ...(childSessionName ? { sessionName: childSessionName } : {}),
2197
2226
  context: step.context,
2198
2227
  ...(step.agentContract ? { agentContract: step.agentContract } : {}),
2199
2228
  launchContractDigest: actualLaunchContractDigest,
@@ -2209,26 +2238,24 @@ async function runSingleStepInner(
2209
2238
  modelAttempts,
2210
2239
  contextOverflow: contextOverflow || undefined,
2211
2240
  totalCost: costSummaryFromAttempts(modelAttempts),
2241
+ usage,
2212
2242
  artifactPaths,
2213
- outputSaveError: artifactErrors.outputSaveError,
2243
+ outputSaveError: [resolvedOutput.saveError, artifactErrors.outputSaveError].filter(Boolean).join("\n") || undefined,
2214
2244
  metadataSaveError: artifactErrors.metadataSaveError,
2215
2245
  transcriptPath: transcriptWriter ? artifactPaths?.transcriptPath : undefined,
2216
2246
  transcriptError: transcriptWriter?.getError(),
2217
- interrupted: timedOutAfterAcceptance || stoppedAfterAcceptance || turnBudgetExceeded ? false : finalResult?.interrupted,
2247
+ interrupted: timedOutAfterAcceptance || stoppedAfterAcceptance ? false : finalResult?.interrupted,
2218
2248
  timedOut: timedOutAfterAcceptance ? true : finalResult?.timedOut,
2219
2249
  stopped: stoppedAfterAcceptance ? true : finalResult?.stopped,
2220
2250
  processSignal: finalResult?.processSignal,
2221
2251
  timeoutRecovery,
2222
- turnBudget,
2223
- turnBudgetExceeded: turnBudgetExceeded || undefined,
2224
- wrapUpRequested: finalResult?.wrapUpRequested || turnBudget?.outcome === "wrap-up-requested" || turnBudget?.outcome === "termination-deferred" || turnBudgetExceeded || undefined,
2225
2252
  toolBudget,
2226
2253
  toolBudgetBlocked: toolBudgetBlocked || undefined,
2227
2254
  completionGuardTriggered: completionGuardTriggeredFinal,
2228
2255
  ...((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,
2256
+ structuredOutput: timedOutAfterAcceptance || stoppedAfterAcceptance ? undefined : (finalResult as (RunPiStreamingResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput,
2257
+ structuredOutputPath: timedOutAfterAcceptance || stoppedAfterAcceptance ? undefined : effectiveStructuredOutput?.outputPath,
2258
+ structuredOutputSchemaPath: timedOutAfterAcceptance || stoppedAfterAcceptance ? undefined : effectiveStructuredOutput?.schemaPath,
2232
2259
  acceptance: effectiveAcceptance,
2233
2260
  watchdog: finalResult?.watchdog,
2234
2261
  ...(capabilityAudit ? { capabilityCeiling: capabilityAudit.ceiling, capabilityAudit } : {}),
@@ -2302,6 +2329,11 @@ function requiredStatusStep(statusPayload: RunnerStatusPayload, index: number):
2302
2329
  return step;
2303
2330
  }
2304
2331
 
2332
+ function setStatusWorktreeReference(statusStep: RunnerStatusStep, worktree: WorktreeSetup["worktrees"][number]): void {
2333
+ statusStep.worktreePath = worktree.path;
2334
+ statusStep.branch = worktree.branch;
2335
+ }
2336
+
2305
2337
  function markParallelGroupSetupFailure(input: {
2306
2338
  statusPayload: RunnerStatusPayload;
2307
2339
  results: StepResult[];
@@ -2430,6 +2462,25 @@ function resolveAsyncStepTranscriptPath(input: {
2430
2462
 
2431
2463
  type SingleStepResult = Awaited<ReturnType<typeof runSingleStep>>;
2432
2464
 
2465
+ function missingRequiredOutputAfterUsefulMutation(result: SingleStepResult): boolean {
2466
+ const effects = result.effects;
2467
+ return effects?.settlementDiagnostic?.requiredOutput?.missing === true
2468
+ && (effects.settlementDiagnostic.mutation.attempted || effects.fileMutation?.attempted === true || Boolean(effects.fileMutation?.evidence?.changedFiles.length));
2469
+ }
2470
+
2471
+ function partialExecutionWithUsefulMutation(result: SingleStepResult): boolean {
2472
+ const fileMutation = result.effects?.fileMutation;
2473
+ return result.execution?.status === "partial" && (fileMutation?.attempted === true || Boolean(fileMutation?.evidence?.changedFiles.length));
2474
+ }
2475
+
2476
+ function partialEvidenceResult(result: SingleStepResult): boolean {
2477
+ return missingRequiredOutputAfterUsefulMutation(result) || partialExecutionWithUsefulMutation(result);
2478
+ }
2479
+
2480
+ function concreteFailureResult(result: SingleStepResult): boolean {
2481
+ return result.success === false && !partialEvidenceResult(result);
2482
+ }
2483
+
2433
2484
  function combinedAbortSignal(signals: Array<AbortSignal | undefined>): AbortSignal | undefined {
2434
2485
  const activeSignals = signals.filter((signal): signal is AbortSignal => Boolean(signal));
2435
2486
  if (activeSignals.length === 0) return undefined;
@@ -2501,6 +2552,8 @@ async function runSubagent(
2501
2552
  const overallStartTime = Date.now();
2502
2553
  const shareEnabled = config.share === true;
2503
2554
  const asyncDir = config.asyncDir;
2555
+ const handoffWorkflowKey = config.workflowKey;
2556
+ const handoffChildRunId = handoffWorkflowKey ? id : undefined;
2504
2557
  const statusPath = path.join(asyncDir, "status.json");
2505
2558
  const eventsPath = path.join(asyncDir, "events.jsonl");
2506
2559
  const logPath = path.join(asyncDir, `subagent-log-${id}.md`);
@@ -2509,7 +2562,6 @@ async function runSubagent(
2509
2562
  const activeChildTimeouts = new Map<number, () => void>();
2510
2563
  const activeChildStops = new Map<number, () => void>();
2511
2564
  const childStopRequests = new Map<number, { childId: string; requestedAt: number }>();
2512
- const activeChildTurnBudgetAborts = new Map<number, (message: string, state?: TurnBudgetState) => void>();
2513
2565
  const pendingStepSteers: SteerRequest[] = [];
2514
2566
  const steeringCapabilities = new Map<number, SteerCapability>();
2515
2567
  let interrupted = false;
@@ -2518,7 +2570,6 @@ async function runSubagent(
2518
2570
  let timeoutTimer: NodeJS.Timeout | undefined;
2519
2571
  let timedOut = false;
2520
2572
  let stopped = false;
2521
- let turnBudgetExceeded = false;
2522
2573
  let usageBudgetExceeded = false;
2523
2574
  const timeoutMessage = config.timeoutMs !== undefined ? `Subagent timed out after ${config.timeoutMs}ms.` : undefined;
2524
2575
  const stopMessage = "Subagent stopped by user.";
@@ -2539,8 +2590,11 @@ async function runSubagent(
2539
2590
  for (const task of step.parallel) {
2540
2591
  const taskFlatIndex = flatStepCount;
2541
2592
  const transcriptPath = resolveAsyncStepTranscriptPath(omitUndefinedProperties({ artifactsDir, artifactConfig, runId: id, agent: task.agent, flatIndex: taskFlatIndex, flatStepCount: initialFlatStepCount }));
2593
+ const taskSessionName = task.sessionName ?? deriveChildSessionName({ agent: task.agent, task: task.task, label: task.label });
2542
2594
  initialStatusSteps.push(omitUndefinedProperties({
2543
2595
  agent: task.agent,
2596
+ ...(task.lane ? { lane: task.lane } : config.lane ? { lane: config.lane } : {}),
2597
+ ...(taskSessionName ? { sessionName: taskSessionName } : {}),
2544
2598
  ...(externalRunnerStatus(task.runner) ? { runner: externalRunnerStatus(task.runner) } : {}),
2545
2599
  ...(statusStepDescription(task.task) ? { description: statusStepDescription(task.task) } : {}),
2546
2600
  ...(task.context ? { context: task.context } : {}),
@@ -2590,8 +2644,11 @@ async function runSubagent(
2590
2644
  } else {
2591
2645
  const stepFlatIndex = flatStepCount;
2592
2646
  const transcriptPath = resolveAsyncStepTranscriptPath(omitUndefinedProperties({ artifactsDir, artifactConfig, runId: id, agent: step.agent, flatIndex: stepFlatIndex, flatStepCount: initialFlatStepCount }));
2647
+ const stepSessionName = step.sessionName ?? deriveChildSessionName({ agent: step.agent, task: step.task, label: step.label });
2593
2648
  initialStatusSteps.push(omitUndefinedProperties({
2594
2649
  agent: step.agent,
2650
+ ...(step.lane ? { lane: step.lane } : config.lane ? { lane: config.lane } : {}),
2651
+ ...(stepSessionName ? { sessionName: stepSessionName } : {}),
2595
2652
  ...(externalRunnerStatus(step.runner) ? { runner: externalRunnerStatus(step.runner) } : {}),
2596
2653
  ...(statusStepDescription(step.task) ? { description: statusStepDescription(step.task) } : {}),
2597
2654
  ...(step.context ? { context: step.context } : {}),
@@ -2654,7 +2711,6 @@ async function runSubagent(
2654
2711
  lastUpdate: overallStartTime,
2655
2712
  ...(config.timeoutMs !== undefined ? { timeoutMs: config.timeoutMs } : {}),
2656
2713
  ...(config.deadlineAt !== undefined ? { deadlineAt: config.deadlineAt } : {}),
2657
- ...(config.turnBudget ? { turnBudget: initialTurnBudgetState(config.turnBudget) } : {}),
2658
2714
  ...(config.toolBudget ? { toolBudget: initialToolBudgetState(config.toolBudget) } : {}),
2659
2715
  ...(config.usageBudget ? { usageBudget: usageBudgetState(config.usageBudget, undefined) } : {}),
2660
2716
  pid: process.pid,
@@ -2669,6 +2725,7 @@ async function runSubagent(
2669
2725
  ...(config.runFanoutBudget ? { runFanoutBudget: getRunFanoutBudgetSnapshot(config.runFanoutBudget) } : {}),
2670
2726
  ...(config.parentWorkflowRunId ? { parentWorkflowRunId: config.parentWorkflowRunId } : {}),
2671
2727
  ...(config.workflowKey ? { workflowKey: config.workflowKey } : {}),
2728
+ ...(config.lane ? { lane: config.lane } : {}),
2672
2729
  ...(config.runnerProcessInstanceId ? { processTerminal: { version: 1 as const, state: "pending" as const, runId: id, runnerProcessInstanceId: config.runnerProcessInstanceId } } : {}),
2673
2730
  steps: initialStatusSteps,
2674
2731
  artifactsDir,
@@ -2786,6 +2843,7 @@ async function runSubagent(
2786
2843
  const statusResultSummary = (state: AsyncStatus["state"]): string => {
2787
2844
  if (statusPayload.error) return statusPayload.error;
2788
2845
  if (state === "paused") return "Paused after interrupt. Waiting for explicit next action.";
2846
+ if (state === "partial") return "Subagent needs attention after partial work.";
2789
2847
  if (state === "stopped") return stopMessage;
2790
2848
  if (state === "rejected") return "Subagent rejected.";
2791
2849
  return state === "complete" ? "Subagent completed." : "Subagent failed.";
@@ -2794,12 +2852,13 @@ async function runSubagent(
2794
2852
  if (step.status === "complete" || step.status === "completed") return true;
2795
2853
  if (step.status === "failed" || step.status === "stopped" || step.status === "rejected") return false;
2796
2854
  if (state === "complete") return true;
2797
- if (state === "failed" || state === "stopped" || state === "rejected") return false;
2855
+ if (state === "failed" || state === "partial" || state === "stopped" || state === "rejected") return false;
2798
2856
  return undefined;
2799
2857
  };
2800
2858
  const writeRecoverableStatusResult = (): void => {
2801
2859
  const state = statusResultState();
2802
2860
  if (!state || finalResultCommitted || !config.sessionId) return;
2861
+ if ((state as string) === "complete") return;
2803
2862
  const now = statusPayload.endedAt ?? statusPayload.lastUpdate ?? Date.now();
2804
2863
  const summary = statusResultSummary(state);
2805
2864
  runPersistence.write(resultPath, omitUndefinedProperties({
@@ -2811,10 +2870,11 @@ async function runSubagent(
2811
2870
  success: state === "complete",
2812
2871
  state,
2813
2872
  summary,
2814
- error: state === "failed" || state === "stopped" || state === "rejected" ? summary : undefined,
2873
+ error: state === "failed" || state === "partial" || state === "stopped" || state === "rejected" ? summary : undefined,
2815
2874
  stopped: state === "stopped" ? true : undefined,
2816
2875
  results: statusPayload.steps.map((step) => omitUndefinedProperties({
2817
2876
  agent: step.agent,
2877
+ ...(step.sessionName ? { sessionName: step.sessionName } : {}),
2818
2878
  output: step.status === "complete" || step.status === "completed" ? "" : step.error ?? summary,
2819
2879
  error: step.error,
2820
2880
  success: statusResultSuccess(state, step),
@@ -2822,6 +2882,7 @@ async function runSubagent(
2822
2882
  model: step.model,
2823
2883
  attemptedModels: step.attemptedModels,
2824
2884
  modelAttempts: step.modelAttempts,
2885
+ usage: usageFromAttempts(step.modelAttempts),
2825
2886
  contextOverflow: step.contextOverflow,
2826
2887
  })),
2827
2888
  exitCode: state === "complete" || state === "paused" ? 0 : 1,
@@ -2916,7 +2977,7 @@ async function runSubagent(
2916
2977
  };
2917
2978
  const childStopResult = (index: number, agent: string, context?: "fresh" | "fork"): SingleStepResult => {
2918
2979
  markChildStopped(index);
2919
- return stoppedStepResult(agent, context);
2980
+ return stoppedStepResult(agent, context, requiredStatusStep(statusPayload, index).sessionName);
2920
2981
  };
2921
2982
  const stopChildStep = (request: StopRequest): void => {
2922
2983
  if (request.targetIndex === undefined) {
@@ -2959,13 +3020,6 @@ async function runSubagent(
2959
3020
  activeChildStops.set(flatIndex, stop);
2960
3021
  if (stopped || childStopRequests.has(flatIndex)) stop();
2961
3022
  };
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
3023
  const interruptActiveChildren = (): void => {
2970
3024
  for (const interrupt of [...activeChildInterrupts.values()]) interrupt();
2971
3025
  };
@@ -3075,23 +3129,26 @@ async function runSubagent(
3075
3129
  }
3076
3130
  }
3077
3131
  };
3078
- const pausedStepResult = (agent: string, context?: "fresh" | "fork"): SingleStepResult => omitUndefinedProperties({
3132
+ const pausedStepResult = (agent: string, context?: "fresh" | "fork", sessionName?: string): SingleStepResult => omitUndefinedProperties({
3079
3133
  agent,
3134
+ sessionName,
3080
3135
  context,
3081
3136
  output: "Paused after interrupt. Waiting for explicit next action.",
3082
3137
  exitCode: 0,
3083
3138
  interrupted: true,
3084
3139
  });
3085
- const timedOutStepResult = (agent: string, context?: "fresh" | "fork"): SingleStepResult => omitUndefinedProperties({
3140
+ const timedOutStepResult = (agent: string, context?: "fresh" | "fork", sessionName?: string): SingleStepResult => omitUndefinedProperties({
3086
3141
  agent,
3142
+ sessionName,
3087
3143
  context,
3088
3144
  output: timeoutMessage ?? "Subagent timed out.",
3089
3145
  error: timeoutMessage ?? "Subagent timed out.",
3090
3146
  exitCode: 1,
3091
3147
  timedOut: true,
3092
3148
  });
3093
- const stoppedStepResult = (agent: string, context?: "fresh" | "fork"): SingleStepResult => omitUndefinedProperties({
3149
+ const stoppedStepResult = (agent: string, context?: "fresh" | "fork", sessionName?: string): SingleStepResult => omitUndefinedProperties({
3094
3150
  agent,
3151
+ sessionName,
3095
3152
  context,
3096
3153
  output: stopMessage,
3097
3154
  error: stopMessage,
@@ -3481,66 +3538,6 @@ async function runSubagent(
3481
3538
  statusPayload.lastUpdate = now;
3482
3539
  writeStatusPayload();
3483
3540
  };
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
3541
  const updateStepFromChildEvent = (flatIndex: number, event: ChildEvent): void => {
3545
3542
  const step = statusPayload.steps[flatIndex];
3546
3543
  if (!step) return;
@@ -3681,13 +3678,6 @@ async function runSubagent(
3681
3678
  refreshUsageBudget();
3682
3679
  }
3683
3680
  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
3681
  }
3692
3682
  syncTopLevelCurrentTool();
3693
3683
  step.lastActivityAt = now;
@@ -3916,7 +3906,7 @@ async function runSubagent(
3916
3906
  let flatIndex = 0;
3917
3907
  let stepCursor = 0;
3918
3908
  while (true) {
3919
- if (interrupted || timedOut || stopped || turnBudgetExceeded) break;
3909
+ if (interrupted || timedOut || stopped) break;
3920
3910
  consumePendingAppendRequests();
3921
3911
  if (stepCursor >= steps.length) break;
3922
3912
  refreshUsageBudget();
@@ -4061,10 +4051,12 @@ async function runSubagent(
4061
4051
  : step.parallel.outputPath;
4062
4052
  const taskText = task.task ?? step.parallel.task;
4063
4053
  const materializedTask = step.parallel.namespaceOutputPath ? injectSingleOutputInstruction(taskText, outputPath, step.parallel) : taskText;
4054
+ const sessionName = deriveChildSessionName({ agent: step.parallel.agent, task: taskText, label: task.label ?? step.parallel.label });
4064
4055
  return omitUndefinedProperties({
4065
4056
  ...step.parallel,
4066
4057
  runFanoutPath: `chain[${stepIndex}].expand[${itemIndex}]`,
4067
4058
  task: materializedTask,
4059
+ ...(sessionName ? { sessionName } : {}),
4068
4060
  effectiveAcceptance: resolveEffectiveAcceptance(omitUndefinedProperties({
4069
4061
  explicit: step.parallel.acceptanceInput,
4070
4062
  agentName: step.parallel.agent,
@@ -4095,6 +4087,7 @@ async function runSubagent(
4095
4087
  const transcriptPath = resolveAsyncStepTranscriptPath(omitUndefinedProperties({ artifactsDir, artifactConfig, runId: id, agent: task.agent, flatIndex: groupStartFlatIndex + itemIndex, flatStepCount: dynamicFlatStepCount }));
4096
4088
  return omitUndefinedProperties({
4097
4089
  agent: task.agent,
4090
+ ...(task.sessionName ? { sessionName: task.sessionName } : {}),
4098
4091
  ...(statusStepDescription(task.task) ? { description: statusStepDescription(task.task) } : {}),
4099
4092
  ...(task.context ? { context: task.context } : {}),
4100
4093
  ...(task.phase ?? step.phase ? { phase: task.phase ?? step.phase } : {}),
@@ -4178,12 +4171,12 @@ async function runSubagent(
4178
4171
  usageBudgetExceeded = true;
4179
4172
  writeStatusPayload();
4180
4173
  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 });
4174
+ 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
4175
  }
4183
- if (timedOut) return timedOutStepResult(task.agent, task.context);
4184
- if (stopped) return stoppedStepResult(task.agent, task.context);
4176
+ if (timedOut) return timedOutStepResult(task.agent, task.context, task.sessionName);
4177
+ if (stopped) return stoppedStepResult(task.agent, task.context, task.sessionName);
4185
4178
  if (childStopRequests.has(fi)) return childStopResult(fi, task.agent, task.context);
4186
- if (interrupted) return pausedStepResult(task.agent, task.context);
4179
+ if (interrupted) return pausedStepResult(task.agent, task.context, task.sessionName);
4187
4180
  if (aborted && failFast) {
4188
4181
  const skippedAt = Date.now();
4189
4182
  requiredStatusStep(statusPayload, fi).status = "failed";
@@ -4194,7 +4187,7 @@ async function runSubagent(
4194
4187
  requiredStatusStep(statusPayload, fi).exitCode = -1;
4195
4188
  statusPayload.lastUpdate = skippedAt;
4196
4189
  writeStatusPayload();
4197
- return omitUndefinedProperties({ agent: task.agent, context: task.context, output: "(skipped — fail-fast)", exitCode: -1 as number | null, skipped: true });
4190
+ 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
4191
  }
4199
4192
  const taskStartTime = Date.now();
4200
4193
  statusPayload.currentStep = fi;
@@ -4230,13 +4223,11 @@ async function runSubagent(
4230
4223
  registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
4231
4224
  registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
4232
4225
  registerStop: (stop) => registerStepStop(fi, stop),
4233
- registerTurnBudgetAbort: (abort) => registerStepTurnBudgetAbort(fi, abort),
4234
4226
  timeoutSignal: timeoutAbortController.signal,
4235
4227
  stopSignal: stopAbortController.signal,
4236
4228
  trackedMutationEvidenceForCompletionGuard: false,
4237
4229
  timeoutMessage,
4238
4230
  stopMessage,
4239
- turnBudget: config.turnBudget,
4240
4231
  toolTimeoutMs: task.toolTimeoutMs ?? config.toolTimeoutMs,
4241
4232
  onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking, attempt.contextLimit),
4242
4233
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
@@ -4244,6 +4235,7 @@ async function runSubagent(
4244
4235
  onExternalProcess: (process) => updateExternalProcess(fi, process),
4245
4236
  onExternalJob: (externalJob) => updateExternalJob(fi, externalJob),
4246
4237
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(fi),
4238
+ usageBudgetExhausted: () => refreshUsageBudget()?.exhausted === true,
4247
4239
  orcaProgressTab,
4248
4240
  }), config.deadlineAt);
4249
4241
  const taskEndTime = Date.now();
@@ -4255,16 +4247,11 @@ async function runSubagent(
4255
4247
  requiredStatusStep(statusPayload, fi).exitCode = stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
4256
4248
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "timedOut", timedOut || singleResult.timedOut ? true : undefined);
4257
4249
  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
4250
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "toolBudget", singleResult.toolBudget);
4262
4251
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "toolBudgetBlocked", singleResult.toolBudgetBlocked);
4263
4252
  if (singleResult.toolBudget) statusPayload.toolBudget = singleResult.toolBudget;
4264
4253
  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;
4254
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "sessionName", singleResult.sessionName);
4268
4255
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "model", singleResult.model);
4269
4256
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "thinking", resolveEffectiveThinking(singleResult.model, requiredStatusStep(statusPayload, fi).thinking));
4270
4257
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "attemptedModels", singleResult.attemptedModels);
@@ -4316,6 +4303,7 @@ async function runSubagent(
4316
4303
  for (const pr of parallelResults) {
4317
4304
  results.push(omitUndefinedProperties({
4318
4305
  agent: pr.agent,
4306
+ ...(pr.sessionName ? { sessionName: pr.sessionName } : {}),
4319
4307
  context: pr.context,
4320
4308
  agentContract: pr.agentContract,
4321
4309
  launchContractDigest: pr.launchContractDigest,
@@ -4331,9 +4319,6 @@ async function runSubagent(
4331
4319
  interrupted: pr.interrupted,
4332
4320
  timedOut: pr.timedOut,
4333
4321
  stopped: pr.stopped,
4334
- turnBudget: pr.turnBudget,
4335
- turnBudgetExceeded: pr.turnBudgetExceeded,
4336
- wrapUpRequested: pr.wrapUpRequested,
4337
4322
  toolBudget: pr.toolBudget,
4338
4323
  toolBudgetBlocked: pr.toolBudgetBlocked,
4339
4324
  sessionFile: pr.sessionFile,
@@ -4343,6 +4328,7 @@ async function runSubagent(
4343
4328
  modelAttempts: pr.modelAttempts,
4344
4329
  contextOverflow: pr.contextOverflow,
4345
4330
  totalCost: pr.totalCost,
4331
+ usage: pr.usage,
4346
4332
  artifactPaths: pr.artifactPaths,
4347
4333
  transcriptPath: pr.transcriptPath,
4348
4334
  transcriptError: pr.transcriptError,
@@ -4486,6 +4472,23 @@ async function runSubagent(
4486
4472
  ? omitUndefinedProperties({ hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs })
4487
4473
  : undefined,
4488
4474
  baseDir: config.worktreeBaseDir,
4475
+ beforeCreate: (plannedSetup) => {
4476
+ for (const worktree of plannedSetup.worktrees) setStatusWorktreeReference(requiredStatusStep(statusPayload, groupStartFlatIndex + worktree.index), worktree);
4477
+ const pendingHandoff = writePendingParallelHandoff({
4478
+ manifestPath: parallelHandoffPath(asyncDir),
4479
+ runId: id,
4480
+ mode: (config.resultMode ?? statusPayload.mode) === "parallel" ? "parallel" : "chain",
4481
+ source: "async",
4482
+ cwd,
4483
+ stepIndex,
4484
+ flatStartIndex: groupStartFlatIndex,
4485
+ setup: plannedSetup,
4486
+ laneBindings: handoffWorkflowKey || config.lane ? [{ index: groupStartFlatIndex, taskIndex: 0, ...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}), ...(handoffChildRunId ? { runId: handoffChildRunId } : {}), ...(config.lane ? { lane: config.lane } : {}) }] : undefined,
4487
+ });
4488
+ statusPayload.parallelHandoff = pendingHandoff;
4489
+ statusPayload.lastUpdate = Date.now();
4490
+ writeStatusPayload();
4491
+ },
4489
4492
  }));
4490
4493
  } catch (error) {
4491
4494
  const setupError = error instanceof Error ? error.message : String(error);
@@ -4510,18 +4513,6 @@ async function runSubagent(
4510
4513
  }
4511
4514
 
4512
4515
  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
4516
  if (group.worktree) ensureParallelProgressFile(cwd, group);
4526
4517
  const groupStartTime = Date.now();
4527
4518
  markParallelGroupRunning({
@@ -4558,12 +4549,12 @@ async function runSubagent(
4558
4549
  appendJsonl(eventsPath, JSON.stringify({
4559
4550
  type: "subagent.step.failed", ts: skippedAt, runId: id, stepIndex: fi, agent: task.agent, exitCode: 1, durationMs: 0,
4560
4551
  }));
4561
- return omitUndefinedProperties({ agent: task.agent, context: task.context, output: message, error: message, exitCode: 1 as number | null, skipped: true });
4552
+ 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
4553
  }
4563
- if (timedOut) return timedOutStepResult(task.agent, task.context);
4564
- if (stopped) return stoppedStepResult(task.agent, task.context);
4554
+ if (timedOut) return timedOutStepResult(task.agent, task.context, task.sessionName);
4555
+ if (stopped) return stoppedStepResult(task.agent, task.context, task.sessionName);
4565
4556
  if (childStopRequests.has(fi)) return childStopResult(fi, task.agent, task.context);
4566
- if (interrupted) return pausedStepResult(task.agent, task.context);
4557
+ if (interrupted) return pausedStepResult(task.agent, task.context, task.sessionName);
4567
4558
  if (aborted && failFast) {
4568
4559
  const skippedAt = Date.now();
4569
4560
  requiredStatusStep(statusPayload, fi).status = "failed";
@@ -4578,7 +4569,7 @@ async function runSubagent(
4578
4569
  appendJsonl(eventsPath, JSON.stringify({
4579
4570
  type: "subagent.step.failed", ts: skippedAt, runId: id, stepIndex: fi, agent: task.agent, exitCode: -1, durationMs: 0,
4580
4571
  }));
4581
- return omitUndefinedProperties({ agent: task.agent, context: task.context, output: "(skipped — fail-fast)", exitCode: -1 as number | null, skipped: true });
4572
+ 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
4573
  }
4583
4574
 
4584
4575
  const taskStartTime = Date.now();
@@ -4626,13 +4617,11 @@ async function runSubagent(
4626
4617
  registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
4627
4618
  registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
4628
4619
  registerStop: (stop) => registerStepStop(fi, stop),
4629
- registerTurnBudgetAbort: (abort) => registerStepTurnBudgetAbort(fi, abort),
4630
4620
  timeoutSignal: timeoutAbortController.signal,
4631
4621
  stopSignal: stopAbortController.signal,
4632
4622
  trackedMutationEvidenceForCompletionGuard: Boolean(worktreeSetup),
4633
4623
  timeoutMessage,
4634
4624
  stopMessage,
4635
- turnBudget: config.turnBudget,
4636
4625
  toolTimeoutMs: taskForRun.toolTimeoutMs ?? config.toolTimeoutMs,
4637
4626
  onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking, attempt.contextLimit),
4638
4627
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
@@ -4640,6 +4629,7 @@ async function runSubagent(
4640
4629
  onExternalProcess: (process) => updateExternalProcess(fi, process),
4641
4630
  onExternalJob: (externalJob) => updateExternalJob(fi, externalJob),
4642
4631
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(fi),
4632
+ usageBudgetExhausted: () => refreshUsageBudget()?.exhausted === true,
4643
4633
  orcaProgressTab,
4644
4634
  }), config.deadlineAt);
4645
4635
  if (task.sessionFile) {
@@ -4657,16 +4647,11 @@ async function runSubagent(
4657
4647
  requiredStatusStep(statusPayload, fi).exitCode = stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
4658
4648
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "timedOut", timedOut || singleResult.timedOut ? true : undefined);
4659
4649
  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
4650
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "toolBudget", singleResult.toolBudget);
4664
4651
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "toolBudgetBlocked", singleResult.toolBudgetBlocked);
4665
4652
  if (singleResult.toolBudget) statusPayload.toolBudget = singleResult.toolBudget;
4666
4653
  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;
4654
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "sessionName", singleResult.sessionName);
4670
4655
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "model", singleResult.model);
4671
4656
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "thinking", resolveEffectiveThinking(singleResult.model, requiredStatusStep(statusPayload, fi).thinking));
4672
4657
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "attemptedModels", singleResult.attemptedModels);
@@ -4775,9 +4760,6 @@ async function runSubagent(
4775
4760
  interrupted: pr.interrupted,
4776
4761
  timedOut: pr.timedOut,
4777
4762
  stopped: pr.stopped,
4778
- turnBudget: pr.turnBudget,
4779
- turnBudgetExceeded: pr.turnBudgetExceeded,
4780
- wrapUpRequested: pr.wrapUpRequested,
4781
4763
  toolBudget: pr.toolBudget,
4782
4764
  toolBudgetBlocked: pr.toolBudgetBlocked,
4783
4765
  sessionFile: pr.sessionFile,
@@ -4787,6 +4769,7 @@ async function runSubagent(
4787
4769
  modelAttempts: pr.modelAttempts,
4788
4770
  contextOverflow: pr.contextOverflow,
4789
4771
  totalCost: pr.totalCost,
4772
+ usage: pr.usage,
4790
4773
  artifactPaths: pr.artifactPaths,
4791
4774
  transcriptPath: pr.transcriptPath,
4792
4775
  transcriptError: pr.transcriptError,
@@ -4840,12 +4823,14 @@ async function runSubagent(
4840
4823
  diffs: captured.diffs,
4841
4824
  results: parallelResults.map((result) => ({
4842
4825
  agent: result.agent,
4826
+ ...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}),
4827
+ ...(handoffChildRunId ? { runId: handoffChildRunId } : {}),
4828
+ ...(config.lane ? { lane: config.lane } : {}),
4843
4829
  status: result.stopped || (result.exitCode !== 0 && isUnexplainedProcessSignal(omitUndefinedProperties({
4844
4830
  processSignal: result.processSignal,
4845
4831
  interrupted: result.interrupted,
4846
4832
  timedOut: result.timedOut,
4847
4833
  stopped: result.stopped,
4848
- turnBudgetExceeded: result.turnBudgetExceeded,
4849
4834
  }))) ? "stopped" as const : result.interrupted ? "paused" as const : result.exitCode === 0 ? "completed" as const : "failed" as const,
4850
4835
  summary: result.output || result.error || "(no output)",
4851
4836
  ...(result.artifactPaths?.outputPath ? { outputPath: result.artifactPaths.outputPath } : {}),
@@ -4891,12 +4876,12 @@ async function runSubagent(
4891
4876
  } else {
4892
4877
  const seqStep = step as SubagentStep;
4893
4878
  if (timedOut) {
4894
- results.push(timedOutStepResult(seqStep.agent, seqStep.context));
4879
+ results.push(timedOutStepResult(seqStep.agent, seqStep.context, seqStep.sessionName));
4895
4880
  flatIndex++;
4896
4881
  continue;
4897
4882
  }
4898
4883
  if (stopped) {
4899
- results.push(stoppedStepResult(seqStep.agent, seqStep.context));
4884
+ results.push(stoppedStepResult(seqStep.agent, seqStep.context, seqStep.sessionName));
4900
4885
  flatIndex++;
4901
4886
  continue;
4902
4887
  }
@@ -4906,7 +4891,7 @@ async function runSubagent(
4906
4891
  continue;
4907
4892
  }
4908
4893
  if (interrupted) {
4909
- results.push(pausedStepResult(seqStep.agent, seqStep.context));
4894
+ results.push(pausedStepResult(seqStep.agent, seqStep.context, seqStep.sessionName));
4910
4895
  flatIndex++;
4911
4896
  continue;
4912
4897
  }
@@ -4919,17 +4904,27 @@ async function runSubagent(
4919
4904
  ? omitUndefinedProperties({ hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs })
4920
4905
  : undefined,
4921
4906
  baseDir: config.worktreeBaseDir,
4907
+ beforeCreate: (plannedSetup) => {
4908
+ const worktree = plannedSetup.worktrees[0];
4909
+ if (worktree) {
4910
+ setStatusWorktreeReference(requiredStatusStep(statusPayload, flatIndex), worktree);
4911
+ }
4912
+ const pendingHandoff = writePendingParallelHandoff({
4913
+ manifestPath: parallelHandoffPath(asyncDir),
4914
+ runId: id,
4915
+ mode: "single",
4916
+ source: "async",
4917
+ cwd,
4918
+ stepIndex,
4919
+ flatStartIndex: flatIndex,
4920
+ setup: plannedSetup,
4921
+ laneBindings: handoffWorkflowKey || config.lane ? [{ index: flatIndex, taskIndex: 0, ...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}), ...(handoffChildRunId ? { runId: handoffChildRunId } : {}), ...(config.lane ? { lane: config.lane } : {}) }] : undefined,
4922
+ });
4923
+ statusPayload.parallelHandoff = pendingHandoff;
4924
+ statusPayload.lastUpdate = Date.now();
4925
+ writeStatusPayload();
4926
+ },
4922
4927
  }));
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
4928
  } catch (error) {
4934
4929
  if (singleWorktreeSetup) cleanupWorktrees(singleWorktreeSetup);
4935
4930
  throw error;
@@ -4982,12 +4977,10 @@ async function runSubagent(
4982
4977
  registerInterrupt: (interrupt) => registerStepInterrupt(flatIndex, interrupt),
4983
4978
  registerTimeout: (interrupt) => registerStepTimeout(flatIndex, interrupt),
4984
4979
  registerStop: (stop) => registerStepStop(flatIndex, stop),
4985
- registerTurnBudgetAbort: (abort) => registerStepTurnBudgetAbort(flatIndex, abort),
4986
4980
  timeoutSignal: timeoutAbortController.signal,
4987
4981
  stopSignal: stopAbortController.signal,
4988
4982
  timeoutMessage,
4989
4983
  stopMessage,
4990
- turnBudget: config.turnBudget,
4991
4984
  toolTimeoutMs: seqStep.toolTimeoutMs ?? config.toolTimeoutMs,
4992
4985
  onAttemptStart: (attempt) => updateStepModel(flatIndex, attempt.model, attempt.thinking, attempt.contextLimit),
4993
4986
  onChildEvent: (event) => updateStepFromChildEvent(flatIndex, event),
@@ -4995,6 +4988,7 @@ async function runSubagent(
4995
4988
  onExternalProcess: (process) => updateExternalProcess(flatIndex, process),
4996
4989
  onExternalJob: (externalJob) => updateExternalJob(flatIndex, externalJob),
4997
4990
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(flatIndex),
4991
+ usageBudgetExhausted: () => refreshUsageBudget()?.exhausted === true,
4998
4992
  orcaProgressTab,
4999
4993
  }), config.deadlineAt);
5000
4994
  } catch (error) {
@@ -5009,6 +5003,7 @@ async function runSubagent(
5009
5003
  const childStopped = singleResult.stopped === true;
5010
5004
  results.push(omitUndefinedProperties({
5011
5005
  agent: singleResult.agent,
5006
+ ...(singleResult.sessionName ? { sessionName: singleResult.sessionName } : {}),
5012
5007
  context: singleResult.context,
5013
5008
  agentContract: singleResult.agentContract,
5014
5009
  launchContractDigest: singleResult.launchContractDigest,
@@ -5027,6 +5022,7 @@ async function runSubagent(
5027
5022
  modelAttempts: singleResult.modelAttempts,
5028
5023
  contextOverflow: singleResult.contextOverflow,
5029
5024
  totalCost: singleResult.totalCost,
5025
+ usage: singleResult.usage,
5030
5026
  artifactPaths: singleResult.artifactPaths,
5031
5027
  transcriptPath: singleResult.transcriptPath,
5032
5028
  transcriptError: singleResult.transcriptError,
@@ -5044,9 +5040,6 @@ async function runSubagent(
5044
5040
  interrupted: singleResult.interrupted,
5045
5041
  timedOut: timedOut || singleResult.timedOut ? true : undefined,
5046
5042
  stopped: stopped || childStopped ? true : undefined,
5047
- turnBudget: singleResult.turnBudget,
5048
- turnBudgetExceeded: singleResult.turnBudgetExceeded,
5049
- wrapUpRequested: singleResult.wrapUpRequested,
5050
5043
  toolBudget: singleResult.toolBudget,
5051
5044
  toolBudgetBlocked: singleResult.toolBudgetBlocked,
5052
5045
  runner: singleResult.runner,
@@ -5101,16 +5094,11 @@ async function runSubagent(
5101
5094
  requiredStatusStep(statusPayload, flatIndex).exitCode = stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
5102
5095
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "timedOut", timedOut || singleResult.timedOut ? true : undefined);
5103
5096
  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
5097
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "toolBudget", singleResult.toolBudget);
5108
5098
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "toolBudgetBlocked", singleResult.toolBudgetBlocked);
5109
5099
  if (singleResult.toolBudget) statusPayload.toolBudget = singleResult.toolBudget;
5110
5100
  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;
5101
+ setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "sessionName", singleResult.sessionName);
5114
5102
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "model", singleResult.model);
5115
5103
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "thinking", resolveEffectiveThinking(singleResult.model, requiredStatusStep(statusPayload, flatIndex).thinking));
5116
5104
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "attemptedModels", singleResult.attemptedModels);
@@ -5171,6 +5159,9 @@ async function runSubagent(
5171
5159
  diffs,
5172
5160
  results: [{
5173
5161
  agent: singleResult.agent,
5162
+ ...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}),
5163
+ ...(handoffChildRunId ? { runId: handoffChildRunId } : {}),
5164
+ ...(config.lane ? { lane: config.lane } : {}),
5174
5165
  status: singleResult.stopped ? "stopped" as const : singleResult.interrupted ? "paused" as const : singleResult.exitCode === 0 ? "completed" as const : "failed" as const,
5175
5166
  summary: singleResult.output || singleResult.error || "(no output)",
5176
5167
  ...(singleResult.artifactPaths?.outputPath ? { outputPath: singleResult.artifactPaths.outputPath } : {}),
@@ -5291,14 +5282,14 @@ async function runSubagent(
5291
5282
  if (!timedOut && !stopped && !interrupted && config.timeoutMs !== undefined && results.some((result) => result.timedOut === true && result.error === timeoutMessage)) {
5292
5283
  timedOut = true;
5293
5284
  }
5294
- const signalTerminated = !stopped && !timedOut && !turnBudgetExceeded && !interrupted && results.some((result) => result.exitCode !== 0 && isUnexplainedProcessSignal(omitUndefinedProperties({
5285
+ const signalTerminated = !stopped && !timedOut && !interrupted && results.some((result) => result.exitCode !== 0 && isUnexplainedProcessSignal(omitUndefinedProperties({
5295
5286
  processSignal: result.processSignal,
5296
5287
  interrupted: result.interrupted,
5297
5288
  timedOut: result.timedOut,
5298
5289
  stopped: result.stopped,
5299
- turnBudgetExceeded: result.turnBudgetExceeded,
5300
5290
  })));
5301
- statusPayload.state = stopped || signalTerminated ? "stopped" : timedOut || turnBudgetExceeded || usageBudgetExceeded || statusPayload.error ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed";
5291
+ const partialWithEvidence = !stopped && !signalTerminated && !timedOut && !usageBudgetExceeded && !interrupted && results.some(partialEvidenceResult) && !results.some(concreteFailureResult);
5292
+ statusPayload.state = stopped || signalTerminated ? "stopped" : timedOut || usageBudgetExceeded ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : partialWithEvidence ? "partial" : "failed";
5302
5293
  closeSteerInbox(asyncDir, statusPayload.state, (filePath, payload) => runPersistence.write(filePath, payload));
5303
5294
  disposeControlInbox();
5304
5295
  for (const request of consumeSteerRequests(asyncDir)) deliverSteerRequest(request);
@@ -5328,13 +5319,17 @@ async function runSubagent(
5328
5319
  statusPayload.timedOut = true;
5329
5320
  statusPayload.error = timeoutMessage ?? "Subagent timed out.";
5330
5321
  }
5331
- if (turnBudgetExceeded && !statusPayload.error) {
5332
- const budget = statusPayload.turnBudget;
5333
- statusPayload.error = budget ? turnBudgetExceededMessage(budget, budget.turnCount) : "Subagent exceeded turn budget.";
5334
- }
5335
5322
  if (usageBudgetExceeded && statusPayload.usageBudget && !statusPayload.error) {
5336
5323
  statusPayload.error = usageBudgetExceededMessage(statusPayload.usageBudget);
5337
5324
  }
5325
+ if (partialWithEvidence) {
5326
+ const partialResult = results.find(partialEvidenceResult);
5327
+ statusPayload.activityState = "needs_attention";
5328
+ statusPayload.error = partialResult?.error ?? statusPayload.error;
5329
+ for (const step of statusPayload.steps) {
5330
+ if (step.status === "failed") step.activityState = "needs_attention";
5331
+ }
5332
+ }
5338
5333
  statusPayload.endedAt = runEndedAt;
5339
5334
  statusPayload.lastUpdate = runEndedAt;
5340
5335
  setOptionalProperty(statusPayload, "sessionFile", effectiveSessionFile);
@@ -5344,8 +5339,10 @@ async function runSubagent(
5344
5339
  setOptionalProperty(statusPayload, "shareUrl", shareUrl);
5345
5340
  setOptionalProperty(statusPayload, "gistUrl", gistUrl);
5346
5341
  setOptionalProperty(statusPayload, "shareError", shareError);
5347
- if (statusPayload.state === "failed" && !statusPayload.error) {
5348
- const failedStep = statusPayload.steps.find((s) => s.status === "failed");
5342
+ if ((statusPayload.state === "failed" || statusPayload.state === "partial") && !statusPayload.error) {
5343
+ const concreteFailure = results.find(concreteFailureResult);
5344
+ const failedStep = concreteFailure ? undefined : statusPayload.steps.find((s) => s.status === "failed");
5345
+ if (concreteFailure?.error) statusPayload.error = concreteFailure.error;
5349
5346
  if (failedStep?.agent) {
5350
5347
  statusPayload.error = `Step failed: ${failedStep.agent}`;
5351
5348
  }
@@ -5357,20 +5354,18 @@ async function runSubagent(
5357
5354
  id,
5358
5355
  agent: agentName,
5359
5356
  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,
5357
+ success: statusPayload.state === "complete",
5358
+ state: statusPayload.state,
5359
+ 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
5360
  ...(config.timeoutMs !== undefined ? { timeoutMs: config.timeoutMs } : {}),
5364
5361
  ...(config.deadlineAt !== undefined ? { deadlineAt: config.deadlineAt } : {}),
5365
- ...(statusPayload.turnBudget ? { turnBudget: statusPayload.turnBudget } : {}),
5366
- ...(statusPayload.turnBudgetExceeded ? { turnBudgetExceeded: true } : {}),
5367
- ...(statusPayload.wrapUpRequested ? { wrapUpRequested: true } : {}),
5368
5362
  ...(statusPayload.toolBudget ? { toolBudget: statusPayload.toolBudget } : {}),
5369
5363
  ...(statusPayload.toolBudgetBlocked ? { toolBudgetBlocked: true } : {}),
5370
5364
  ...(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." } : {}),
5365
+ ...(stopped ? { stopped: true, error: stopMessage } : timedOut ? { timedOut: true, error: timeoutMessage ?? "Subagent timed out." } : usageBudgetExceeded ? { error: statusPayload.error ?? "Usage budget exhausted." } : {}),
5372
5366
  results: results.map((r) => omitUndefinedProperties({
5373
5367
  agent: r.agent,
5368
+ ...(r.sessionName ? { sessionName: r.sessionName } : {}),
5374
5369
  context: r.context,
5375
5370
  output: r.output,
5376
5371
  outputState: r.outputState,
@@ -5382,9 +5377,6 @@ async function runSubagent(
5382
5377
  timedOut: r.timedOut || undefined,
5383
5378
  stopped: r.stopped || undefined,
5384
5379
  processSignal: r.processSignal || undefined,
5385
- turnBudget: r.turnBudget,
5386
- turnBudgetExceeded: r.turnBudgetExceeded || undefined,
5387
- wrapUpRequested: r.wrapUpRequested || undefined,
5388
5380
  toolBudget: r.toolBudget,
5389
5381
  toolBudgetBlocked: r.toolBudgetBlocked || undefined,
5390
5382
  sessionFile: r.sessionFile,
@@ -5394,6 +5386,7 @@ async function runSubagent(
5394
5386
  modelAttempts: r.modelAttempts,
5395
5387
  contextOverflow: r.contextOverflow,
5396
5388
  totalCost: r.totalCost,
5389
+ usage: r.usage,
5397
5390
  artifactPaths: r.artifactPaths,
5398
5391
  outputSaveError: r.outputSaveError,
5399
5392
  metadataSaveError: r.metadataSaveError,
@@ -5426,7 +5419,7 @@ async function runSubagent(
5426
5419
  capabilityAudit: statusPayload.capabilityAudit,
5427
5420
  ...(config.parentWorkflowRunId ? { parentWorkflowRunId: config.parentWorkflowRunId } : {}),
5428
5421
  ...(config.workflowKey ? { workflowKey: config.workflowKey } : {}),
5429
- exitCode: stopped || timedOut || turnBudgetExceeded || usageBudgetExceeded ? 1 : interrupted || results.every((r) => r.success) ? 0 : 1,
5422
+ exitCode: statusPayload.state === "complete" || statusPayload.state === "paused" ? 0 : 1,
5430
5423
  timestamp: runEndedAt,
5431
5424
  durationMs: runEndedAt - overallStartTime,
5432
5425
  totalTokens: statusPayload.totalTokens,