pi-subagents 0.57.0 → 0.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/docs/agents.md +18 -9
  3. package/docs/configuration.md +4 -4
  4. package/docs/extension-api.md +40 -1
  5. package/docs/models.md +27 -4
  6. package/docs/observability.md +1 -1
  7. package/docs/tool-reference.md +92 -6
  8. package/docs/workflows.md +115 -0
  9. package/package.json +3 -1
  10. package/prompts/review-loop.md +2 -2
  11. package/skills/council-mode/SKILL.md +1 -1
  12. package/skills/pi-subagents/SKILL.md +3 -1
  13. package/skills/pi-subagents/references/execution-controls.md +6 -2
  14. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  15. package/skills/pi-subagents/references/prompting-and-roles.md +26 -5
  16. package/src/agents/agent-management.ts +28 -17
  17. package/src/agents/agent-serializer.ts +9 -2
  18. package/src/agents/agents.ts +103 -27
  19. package/src/agents/runtime-agent-events.ts +70 -0
  20. package/src/agents/runtime-agent-registry.ts +19 -18
  21. package/src/api/agents.ts +10 -5
  22. package/src/api/background-work.ts +5 -1
  23. package/src/api/delegation.ts +0 -7
  24. package/src/api/preflight.ts +32 -10
  25. package/src/extension/fanout-child.ts +5 -3
  26. package/src/extension/index.ts +66 -27
  27. package/src/extension/public-execution.ts +15 -2
  28. package/src/extension/rpc.ts +35 -14
  29. package/src/extension/schemas.ts +37 -12
  30. package/src/extension/tool-description.ts +25 -6
  31. package/src/integrations/herdr-status.ts +5 -0
  32. package/src/intercom/result-intercom.ts +2 -0
  33. package/src/profiles/profiles.ts +5 -6
  34. package/src/runs/background/active-async-capacity.ts +2 -2
  35. package/src/runs/background/async-execution.ts +78 -46
  36. package/src/runs/background/async-job-tracker.ts +14 -12
  37. package/src/runs/background/async-resume.ts +29 -27
  38. package/src/runs/background/async-status-snapshot.ts +23 -261
  39. package/src/runs/background/async-status.ts +68 -7
  40. package/src/runs/background/chain-append.ts +8 -3
  41. package/src/runs/background/chain-root-attachment.ts +60 -8
  42. package/src/runs/background/fleet-view.ts +21 -11
  43. package/src/runs/background/notify.ts +184 -9
  44. package/src/runs/background/result-delivery-ownership.ts +45 -0
  45. package/src/runs/background/result-files.ts +2 -1
  46. package/src/runs/background/result-watcher.ts +29 -10
  47. package/src/runs/background/resume-guidance.ts +1 -1
  48. package/src/runs/background/retained-children.ts +1 -1
  49. package/src/runs/background/run-status.ts +18 -8
  50. package/src/runs/background/scheduled-runs.ts +86 -7
  51. package/src/runs/background/stale-run-reconciler.ts +10 -4
  52. package/src/runs/background/steering.ts +4 -14
  53. package/src/runs/background/subagent-runner.ts +389 -350
  54. package/src/runs/background/subagent-wait.ts +58 -10
  55. package/src/runs/background/terminal-run-index.ts +1 -1
  56. package/src/runs/background/wait-completions.ts +22 -1
  57. package/src/runs/background/wait-config.ts +23 -9
  58. package/src/runs/background/wait-tool.ts +9 -2
  59. package/src/runs/foreground/async-steering-action.ts +2 -2
  60. package/src/runs/foreground/execution.ts +134 -121
  61. package/src/runs/foreground/foreground-control.ts +3 -0
  62. package/src/runs/foreground/foreground-history.ts +1 -0
  63. package/src/runs/foreground/subagent-executor.ts +527 -257
  64. package/src/runs/foreground/workflow-detach-reconcile.ts +130 -120
  65. package/src/runs/shared/abort-recovery.ts +119 -0
  66. package/src/runs/shared/async-status-projection.ts +463 -0
  67. package/src/runs/shared/child-identity.ts +19 -4
  68. package/src/runs/shared/child-launch-plan.ts +151 -0
  69. package/src/runs/shared/completion-evidence.ts +89 -0
  70. package/src/runs/shared/completion-guard.ts +5 -4
  71. package/src/runs/shared/dynamic-fanout.ts +3 -3
  72. package/src/runs/shared/fast-mode-extension.ts +5 -5
  73. package/src/runs/shared/host-step-status.ts +230 -0
  74. package/src/runs/shared/lane-metadata.ts +105 -0
  75. package/src/runs/shared/launch-cwd.ts +16 -0
  76. package/src/runs/shared/long-running-guard.ts +2 -1
  77. package/src/runs/shared/mcp-config-sources.ts +422 -0
  78. package/src/runs/shared/mcp-direct-tool-allowlist.ts +221 -158
  79. package/src/runs/shared/mcp-direct-tool-grant.ts +197 -0
  80. package/src/runs/shared/model-exclusions.ts +12 -0
  81. package/src/runs/shared/model-fallback.ts +25 -6
  82. package/src/runs/shared/nested-events.ts +6 -2
  83. package/src/runs/shared/nested-render.ts +7 -3
  84. package/src/runs/shared/parallel-handoff.ts +419 -7
  85. package/src/runs/shared/parallel-utils.ts +15 -1
  86. package/src/runs/shared/pi-args.ts +81 -8
  87. package/src/runs/shared/single-output.ts +44 -4
  88. package/src/runs/shared/subagent-prompt-runtime.ts +98 -13
  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 +11 -2
  94. package/src/shared/launch-contract.ts +5 -1
  95. package/src/shared/settings.ts +9 -103
  96. package/src/shared/types.ts +240 -30
  97. package/src/shared/utils.ts +41 -84
  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/slash/subagents-admin.ts +3 -0
  104. package/src/tui/fleet-status.ts +125 -74
  105. package/src/tui/fleet.ts +11 -5
  106. package/src/tui/render.ts +330 -33
  107. package/src/watchdog/turn-delta.ts +1 -1
  108. package/src/workflows/chat-progress.ts +6 -3
  109. package/src/workflows/host-command.ts +230 -0
  110. package/src/workflows/scripted-workflow.ts +496 -35
  111. package/src/workflows/workflow-child-summary.ts +9 -5
  112. package/src/workflows/workflow-preflight.ts +270 -0
  113. package/src/workflows/workflow-receipt.ts +79 -5
  114. package/src/workflows/workflow-settlement.ts +246 -0
  115. package/src/runs/shared/turn-budget.ts +0 -98
@@ -14,6 +14,7 @@ import { createChildTranscriptWriter, type ChildTranscriptWriter } from "../../s
14
14
  import { closeSteerInbox, consumeInterruptRequest, consumeSteerRequests, deliverInterruptRequest, deliverStopRequest, deliverTimeoutRequest, enqueueStepSteer, steerAcksDir, steerCapabilityPath, stepSteerInboxDir, watchAsyncControlInbox, type SteerAck, type SteerCapability, type SteerRequest, type StopRequest } from "./control-channel.ts";
15
15
  import { appendJsonl as appendRawJsonl, formatOutputArtifactContent, getArtifactPaths, writeArtifact, writeMetadata } from "../../shared/artifacts.ts";
16
16
  import { PI_CODING_AGENT_PACKAGE, getPiSpawnCommand, resolveInstalledPiPackageRoot } from "../shared/pi-spawn.ts";
17
+ import { preflightLaunchCwd } from "../shared/launch-cwd.ts";
17
18
  import { captureSingleOutputSnapshot, extractChildWrittenOutput, finalizeSingleOutput, formatSavedOutputReference, injectOutputPathSystemPrompt, injectSingleOutputInstruction, resolveSingleOutput, type SingleOutputSnapshot } from "../shared/single-output.ts";
18
19
  import {
19
20
  type ActivityState,
@@ -35,24 +36,21 @@ import {
35
36
  type NestedRouteInfo,
36
37
  type NestedRunSummary,
37
38
  type ResolvedControlConfig,
38
- type ResolvedTurnBudget,
39
39
  type ResolvedToolBudget,
40
40
  type RunFanoutBudgetDescriptor,
41
41
  type SubagentRunMode,
42
42
  type SubagentOutputState,
43
43
  type UsageBudgetConfig,
44
44
  type ToolBudgetState,
45
- type TurnBudgetState,
46
45
  type Usage,
47
46
  type WorkflowGraphSnapshot,
48
- type SteeringStatus,
49
47
  type SteeringTargetState,
50
48
  type SteeringTargetStatus,
51
49
  type SubagentChildStatusEvent,
50
+ type WorkflowLaneMetadata,
52
51
  DEFAULT_MAX_OUTPUT,
53
52
  type MaxOutputConfig,
54
53
  SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
55
- POLL_INTERVAL_MS,
56
54
  truncateOutput,
57
55
  getSubagentDepthEnv,
58
56
  } from "../../shared/types.ts";
@@ -78,6 +76,7 @@ import {
78
76
  Semaphore,
79
77
  } from "../shared/parallel-utils.ts";
80
78
  import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan, type SubagentTaskDelivery } from "../shared/pi-args.ts";
79
+ import { deriveChildSessionName } from "../../shared/child-session-name.ts";
81
80
  import { readRuntimeAcknowledgedExtensions } from "../shared/runtime-acknowledged-extensions.ts";
82
81
  import { outputEntryFromAsyncResult, resolveOutputReferences } from "../shared/chain-outputs.ts";
83
82
  import { createStructuredOutputRuntime, MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput, readStructuredOutputAcceptanceReport } from "../shared/structured-output.ts";
@@ -100,8 +99,10 @@ import { markProcessTerminalCandidateLeaseRelease, writeProcessTerminalCandidate
100
99
  import { createOwnedProcessTreeController, type OwnedProcessTreeController } from "./owned-process-tree.ts";
101
100
  import { createSteeringStatus, recordSteeringRequest, steeringStatus, terminalSteeringNoticeState, updateSteeringTarget } from "./steering.ts";
102
101
  import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
103
- import { PROMPT_REDACTED, detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput, hasEmptyTerminalAssistantResponse, readStatus } from "../../shared/utils.ts";
104
- import { evaluateCompletionMutationGuard, validateImplementationToolContract } from "../shared/completion-guard.ts";
102
+ import { PROMPT_REDACTED, detectSubagentError, extractTextFromContent, extractToolArgsPreview, formatEmptyTerminalAssistantResponseError, getFinalOutput, hasEmptyTerminalAssistantResponse, readStatus } from "../../shared/utils.ts";
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";
105
106
  import {
106
107
  createMutatingFailureState,
107
108
  didMutatingToolFail,
@@ -124,7 +125,7 @@ import {
124
125
  formatWorktreeTaskCwdConflict,
125
126
  type WorktreeSetup,
126
127
  } from "../shared/worktree.ts";
127
- import { resolveEffectiveThinking } from "../../shared/model-info.ts";
128
+ import { findModelInfo, resolveEffectiveThinking } from "../../shared/model-info.ts";
128
129
  import { assertThinkingWithinCeiling, decodeThinkingCeiling, SUBAGENT_THINKING_CEILING_ENV } from "../../shared/thinking-ceiling.ts";
129
130
  import { launchBindingDigest } from "../../shared/launch-contract.ts";
130
131
  import { writeInitialProgressFile } from "../../shared/settings.ts";
@@ -135,7 +136,6 @@ import { waitForImportedAsyncRoot } from "./chain-root-attachment.ts";
135
136
  import { normalizeExtensionBindings } from "../shared/extension-bindings.ts";
136
137
  import { appendRunnerStepsToStatus, consumeChainAppendRequests, countPendingChainAppendRequests, statusStepDescription } from "./chain-append.ts";
137
138
  import { asyncStatusChildIdentity } from "../shared/child-identity.ts";
138
- import { appendTurnBudgetSystemPrompt, formatTurnBudgetOutput, initialTurnBudgetState, turnBudgetDecision, turnBudgetDeferredNote, turnBudgetDeferredState, turnBudgetExceededMessage, turnBudgetSoftNote, turnBudgetState } from "../shared/turn-budget.ts";
139
139
  import { initialToolBudgetState, toolBudgetState } from "../shared/tool-budget.ts";
140
140
  import { effectiveToolTimeoutMs, formatToolTimeoutMessage, toolTimeoutCallKey } from "../shared/tool-timeout.ts";
141
141
  import { usageBudgetExceededMessage, usageBudgetState } from "../shared/usage-budget.ts";
@@ -197,7 +197,6 @@ interface SubagentRunConfig {
197
197
  deadlineAt?: number;
198
198
  /** Resolved configured hard per-tool-call timeout (ms); fast tools still have a default when undefined. */
199
199
  toolTimeoutMs?: number;
200
- turnBudget?: ResolvedTurnBudget;
201
200
  toolBudget?: ResolvedToolBudget;
202
201
  usageBudget?: UsageBudgetConfig;
203
202
  revivalLease?: SessionLeaseRequest;
@@ -213,10 +212,13 @@ interface SubagentRunConfig {
213
212
  launchBarrierToken?: string;
214
213
  parentWorkflowRunId?: string;
215
214
  workflowKey?: string;
215
+ lane?: WorkflowLaneMetadata;
216
216
  }
217
217
 
218
218
  interface StepResult {
219
219
  agent: string;
220
+ /** Human-readable display name for the child session, when derived at launch. */
221
+ sessionName?: string;
220
222
  context?: "fresh" | "fork";
221
223
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
222
224
  capabilityAudit?: import("../shared/capability-ceiling.ts").SubagentCapabilityAudit;
@@ -237,9 +239,6 @@ interface StepResult {
237
239
  stopped?: boolean;
238
240
  processSignal?: string | null;
239
241
  timeoutRecovery?: import("../../shared/types.ts").TimeoutRecoverySummary;
240
- turnBudget?: TurnBudgetState;
241
- turnBudgetExceeded?: boolean;
242
- wrapUpRequested?: boolean;
243
242
  toolBudget?: ToolBudgetState;
244
243
  toolBudgetBlocked?: boolean;
245
244
  sessionFile?: string;
@@ -427,6 +426,23 @@ function costSummaryFromAttempts(attempts: ModelAttempt[] | undefined): CostSumm
427
426
  : undefined;
428
427
  }
429
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
+
430
446
  function appendRecentStepOutput(step: RunnerStatusStep, lines: string[]): void {
431
447
  const nonEmpty = lines.filter((line) => line.trim());
432
448
  if (nonEmpty.length === 0) return;
@@ -535,14 +551,12 @@ interface RunPiStreamingResult {
535
551
  interrupted?: boolean;
536
552
  timedOut?: boolean;
537
553
  stopped?: boolean;
538
- turnBudget?: TurnBudgetState;
539
- turnBudgetExceeded?: boolean;
540
- wrapUpRequested?: boolean;
541
554
  toolBudget?: ToolBudgetState;
542
555
  toolBudgetBlocked?: boolean;
543
556
  observedMutationAttempt?: boolean;
544
557
  structuredOutputToolInvoked?: boolean;
545
558
  structuredOutputMessageStartIndex?: number;
559
+ structuredOutput?: unknown;
546
560
  watchdog?: ChildWatchdogStateSnapshot;
547
561
  runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
548
562
  processInstanceId: string;
@@ -553,24 +567,42 @@ interface RunPiStreamingResult {
553
567
  currentToolArgs?: string;
554
568
  currentPath?: string;
555
569
  afterCompactionSettlement?: boolean;
570
+ abortRecoveryDiagnostic?: string;
571
+ effects?: import("../../shared/types.ts").EffectsProjection;
556
572
  }
557
573
 
558
574
  const MAX_CHILD_FAILURE_DIAGNOSTIC_CHARS = 8_192;
559
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
+
560
585
  function formatChildFailureDiagnostic(input: {
561
586
  error: string | undefined;
562
587
  afterCompactionSettlement?: boolean;
563
- missingFileOnlyOutput?: string;
588
+ abortRecoveryDiagnostic?: string;
589
+ requiredOutput?: {
590
+ kind: "file-only" | "structured";
591
+ path: string;
592
+ missing: boolean;
593
+ };
564
594
  }): string | undefined {
565
- if (!input.error) return undefined;
595
+ const missingOutput = formatRequiredOutputError(input.requiredOutput);
566
596
  const notes = [
597
+ input.abortRecoveryDiagnostic,
567
598
  input.afterCompactionSettlement ? "Child failure followed session compaction and agent settlement." : undefined,
568
- input.missingFileOnlyOutput ? `Required file-only output was not produced: ${input.missingFileOnlyOutput.slice(0, 2_048)}` : undefined,
599
+ missingOutput && input.error !== missingOutput ? missingOutput : undefined,
569
600
  ].filter((note): note is string => Boolean(note));
570
601
  if (notes.length === 0) return input.error;
571
602
  const context = notes.join("\n");
572
603
  const errorLimit = MAX_CHILD_FAILURE_DIAGNOSTIC_CHARS - (context ? context.length + 1 : 0);
573
- return `${input.error.slice(0, Math.max(0, errorLimit))}${context ? `\n${context}` : ""}`;
604
+ const baseError = input.error || "Subagent failed.";
605
+ return `${baseError.slice(0, Math.max(0, errorLimit))}${context ? `\n${context}` : ""}`;
574
606
  }
575
607
 
576
608
  function runPiStreaming(
@@ -589,13 +621,13 @@ function runPiStreaming(
589
621
  timeoutMessage?: string,
590
622
  registerStop?: (stop: (() => void) | undefined) => void,
591
623
  stopMessage?: string,
592
- registerTurnBudgetAbort?: (abort: ((message: string, state?: TurnBudgetState) => void) | undefined) => void,
593
624
  onWriterProcess?: (writer: { state: "none" | "spawning" } | { state: "running"; pid: number }) => void,
594
625
  toolTimeoutMs?: number,
595
626
  runDeadlineAt?: number,
596
627
  orcaProgressTab?: OrcaProgressTab,
597
628
  expectedModelForVerification?: string,
598
629
  modelVerificationRegistry?: Array<{ provider: string; id: string; fullId: string }>,
630
+ mutationTools?: readonly string[],
599
631
  ): Promise<RunPiStreamingResult> {
600
632
  return new Promise((resolve) => {
601
633
  const startedAt = Date.now();
@@ -635,9 +667,6 @@ function runPiStreaming(
635
667
  let interrupted = false;
636
668
  let timedOut = false;
637
669
  let stopped = false;
638
- let turnBudgetExceeded = false;
639
- let turnBudgetMessage: string | undefined;
640
- let turnBudget: TurnBudgetState | undefined;
641
670
  let observedMutationAttempt = false;
642
671
  let structuredOutputToolInvoked = false;
643
672
  let structuredOutputMessageStartIndex: number | undefined;
@@ -740,8 +769,19 @@ function runPiStreaming(
740
769
  appendChildEvent(event as unknown as Record<string, unknown>);
741
770
  transcriptWriter?.writeChildEvent(event);
742
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
+ }
743
780
  const lifecycleAction = projectChildLifecycle(event, false, childLifecycleState);
744
- 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
+ }
745
785
  applyChildLifecycle(lifecycleAction);
746
786
 
747
787
  if (isChildWatchdogStatusEvent(event)) {
@@ -791,7 +831,7 @@ function runPiStreaming(
791
831
  structuredOutputToolInvoked = true;
792
832
  structuredOutputMessageStartIndex = messages.length;
793
833
  }
794
- observedMutationAttempt = observedMutationAttempt || isMutatingTool(event.toolName, event.args);
834
+ observedMutationAttempt = observedMutationAttempt || isMutatingTool(event.toolName, event.args, mutationTools);
795
835
  const toolArgs = extractToolArgsPreview(event.args ?? {});
796
836
  writeOutputLine(toolArgs ? `${event.toolName}: ${toolArgs}` : event.toolName);
797
837
  return;
@@ -849,11 +889,10 @@ function runPiStreaming(
849
889
  let cleanTerminalAssistantStopReceived = false;
850
890
  let agentSettledReceived = false;
851
891
  let compactionStartedReceived = false;
892
+ let afterCompactionSettlement = false;
852
893
  let finalDrainTimer: NodeJS.Timeout | undefined;
853
894
  let finalHardKillTimer: NodeJS.Timeout | undefined;
854
895
  let watchdogTailTimer: NodeJS.Timeout | undefined;
855
- let turnBudgetTerminationTimer: NodeJS.Timeout | undefined;
856
- let turnBudgetHardKillTimer: NodeJS.Timeout | undefined;
857
896
  let protocolHardKillTimer: NodeJS.Timeout | undefined;
858
897
  let protocolError: ProtocolOutputLimit | undefined;
859
898
  let settled = false;
@@ -974,23 +1013,6 @@ function runPiStreaming(
974
1013
  if (processTreeController) void processTreeController.terminate();
975
1014
  else trySignalChild(child, "SIGTERM");
976
1015
  });
977
- registerTurnBudgetAbort?.((message, state) => {
978
- if (settled || timedOut || stopped || turnBudgetExceeded) return;
979
- turnBudgetExceeded = true;
980
- turnBudgetMessage = message;
981
- turnBudget = state;
982
- interrupted = false;
983
- error = message;
984
- trySignalChild(child, "SIGINT");
985
- turnBudgetTerminationTimer = setTimeout(() => {
986
- if (!settled && !timedOut && !stopped) trySignalChild(child, "SIGTERM");
987
- }, 1000);
988
- turnBudgetTerminationTimer.unref?.();
989
- turnBudgetHardKillTimer = setTimeout(() => {
990
- if (!settled && !timedOut && !stopped) trySignalChild(child, "SIGKILL");
991
- }, 4000);
992
- turnBudgetHardKillTimer.unref?.();
993
- });
994
1016
  const clearDrainTimers = () => {
995
1017
  clearAllToolTimeouts();
996
1018
  if (finalDrainTimer) {
@@ -1002,14 +1024,6 @@ function runPiStreaming(
1002
1024
  finalHardKillTimer = undefined;
1003
1025
  }
1004
1026
  clearWatchdogTailTimer();
1005
- if (turnBudgetTerminationTimer) {
1006
- clearTimeout(turnBudgetTerminationTimer);
1007
- turnBudgetTerminationTimer = undefined;
1008
- }
1009
- if (turnBudgetHardKillTimer) {
1010
- clearTimeout(turnBudgetHardKillTimer);
1011
- turnBudgetHardKillTimer = undefined;
1012
- }
1013
1027
  if (protocolHardKillTimer) {
1014
1028
  clearTimeout(protocolHardKillTimer);
1015
1029
  protocolHardKillTimer = undefined;
@@ -1077,7 +1091,6 @@ function runPiStreaming(
1077
1091
  registerInterrupt?.(undefined);
1078
1092
  registerTimeout?.(undefined);
1079
1093
  registerStop?.(undefined);
1080
- registerTurnBudgetAbort?.(undefined);
1081
1094
  clearDrainTimers();
1082
1095
  clearStdioGuard();
1083
1096
  stdoutReader.end();
@@ -1087,32 +1100,32 @@ function runPiStreaming(
1087
1100
  const finalOutput = getFinalOutput(messages) || rawStdoutTail.text().trim();
1088
1101
  const finalError = error ?? assistantError;
1089
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;
1090
1107
  const signalError = isUnexplainedProcessSignal({
1091
1108
  processSignal: signal,
1092
1109
  interrupted,
1093
1110
  timedOut,
1094
1111
  stopped,
1095
- turnBudgetExceeded,
1096
- forcedDrainAfterFinalSuccess,
1112
+ forcedDrainAfterFinalSuccess: forcedDrainAfterFinalSuccess && !forcedDrainAfterEmptyTerminal,
1097
1113
  }) ? formatProcessSignalError(signal!) : undefined;
1098
1114
  resolve(omitUndefinedProperties({
1099
1115
  stderr,
1100
- 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,
1101
1117
  messages,
1102
1118
  usage,
1103
1119
  toolCount,
1104
1120
  durationMs: Date.now() - startedAt,
1105
1121
  model,
1106
- 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,
1107
1123
  protocolError,
1108
1124
  finalOutput: (timedOut || stopped) && !finalOutput.trim() ? (stopped ? stopMessage ?? "Subagent stopped by user." : timeoutMessage ?? "Subagent timed out.") : finalOutput,
1109
1125
  outputState: finalOutput.trim() ? "present" : "absent",
1110
1126
  interrupted,
1111
1127
  timedOut,
1112
1128
  stopped,
1113
- turnBudget,
1114
- turnBudgetExceeded,
1115
- wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudget?.outcome === "termination-deferred" || turnBudgetExceeded || undefined,
1116
1129
  observedMutationAttempt,
1117
1130
  structuredOutputToolInvoked,
1118
1131
  structuredOutputMessageStartIndex,
@@ -1124,7 +1137,7 @@ function runPiStreaming(
1124
1137
  currentTool,
1125
1138
  currentToolArgs,
1126
1139
  currentPath,
1127
- afterCompactionSettlement: (compactionStartedReceived && agentSettledReceived) || undefined,
1140
+ afterCompactionSettlement: afterCompactionSettlement || undefined,
1128
1141
  }));
1129
1142
  });
1130
1143
 
@@ -1138,7 +1151,6 @@ function runPiStreaming(
1138
1151
  registerInterrupt?.(undefined);
1139
1152
  registerTimeout?.(undefined);
1140
1153
  registerStop?.(undefined);
1141
- registerTurnBudgetAbort?.(undefined);
1142
1154
  clearDrainTimers();
1143
1155
  clearStdioGuard();
1144
1156
  stdoutReader.end();
@@ -1147,7 +1159,7 @@ function runPiStreaming(
1147
1159
  const stderr = stderrTail.text();
1148
1160
  const finalOutput = getFinalOutput(messages) || rawStdoutTail.text().trim();
1149
1161
  const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
1150
- 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 } }));
1151
1163
  });
1152
1164
  });
1153
1165
  }
@@ -1282,7 +1294,6 @@ interface SingleStepContext {
1282
1294
  registerInterrupt?: (interrupt: (() => void) | undefined) => void;
1283
1295
  registerTimeout?: (interrupt: (() => void) | undefined) => void;
1284
1296
  registerStop?: (stop: (() => void) | undefined) => void;
1285
- registerTurnBudgetAbort?: (abort: ((message: string, state?: TurnBudgetState) => void) | undefined) => void;
1286
1297
  timeoutSignal?: AbortSignal;
1287
1298
  stopSignal?: AbortSignal;
1288
1299
  timeoutMessage?: string;
@@ -1291,18 +1302,18 @@ interface SingleStepContext {
1291
1302
  toolTimeoutMs?: number;
1292
1303
  /** Effective step deadline (Date.now() + effective timeout) when a run budget exists. */
1293
1304
  deadlineAt?: number;
1294
- turnBudget?: ResolvedTurnBudget;
1295
1305
  childIntercomTarget?: string;
1296
1306
  orchestratorIntercomTarget?: string;
1297
1307
  nestedRoute?: NestedRouteInfo;
1298
1308
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
1299
1309
  runFanoutBudget?: RunFanoutBudgetDescriptor;
1300
- onAttemptStart?: (attempt: { model?: string; thinking?: string }) => void;
1310
+ onAttemptStart?: (attempt: { model?: string; thinking?: string; contextLimit?: number }) => void;
1301
1311
  onChildEvent?: (event: ChildEvent) => void;
1302
1312
  onWriterProcess?: (writer: { state: "none" | "spawning" } | { state: "running"; pid: number }) => void;
1303
1313
  onExternalProcess?: (process: ExternalProcessStatus) => void;
1304
1314
  onExternalJob?: (status: ExternalJobStatus) => void;
1305
1315
  skipAcceptance?: () => boolean;
1316
+ usageBudgetExhausted?: () => boolean;
1306
1317
  /** False when sibling work in the same Git worktree could have caused the tracked diff. */
1307
1318
  trackedMutationEvidenceForCompletionGuard?: boolean;
1308
1319
  orcaProgressTab?: OrcaProgressTab;
@@ -1371,10 +1382,13 @@ async function runSingleStepInner(
1371
1382
  modelAttempts: imported.modelAttempts,
1372
1383
  contextOverflow: imported.contextOverflow,
1373
1384
  totalCost: imported.totalCost,
1385
+ usage: imported.usage,
1374
1386
  structuredOutput: timedOut || stopped ? undefined : imported.structuredOutput,
1375
1387
  structuredOutputPath: timedOut || stopped ? undefined : imported.structuredOutputPath,
1376
1388
  structuredOutputSchemaPath: timedOut || stopped ? undefined : imported.structuredOutputSchemaPath,
1377
1389
  acceptance: timedOut || stopped ? undefined : imported.acceptance,
1390
+ execution: timedOut || stopped ? undefined : imported.execution,
1391
+ effects: timedOut || stopped ? undefined : imported.effects,
1378
1392
  });
1379
1393
  } finally {
1380
1394
  ctx.registerTimeout?.(undefined);
@@ -1393,12 +1407,15 @@ async function runSingleStepInner(
1393
1407
  if (!step.runner) {
1394
1408
  resolvedTaskToolPlan = resolvePiLaunchToolPlan(omitUndefinedProperties({
1395
1409
  tools: step.tools,
1410
+ allowNestedSubagents: step.allowNestedSubagents,
1396
1411
  extensions: step.extensions,
1397
1412
  subagentOnlyExtensions: step.subagentOnlyExtensions,
1398
1413
  fast: step.fast,
1399
1414
  model: step.model,
1400
1415
  modelCandidates: step.modelCandidates,
1401
1416
  mcpDirectTools: step.mcpDirectTools,
1417
+ mcpConfig: step.mcpConfig,
1418
+ runtimeServerNames: step.runtimeServerNames,
1402
1419
  cwd: step.cwd ?? ctx.cwd,
1403
1420
  requireReadTool: Boolean(step.skills?.length),
1404
1421
  structuredOutput: Boolean(effectiveStructuredOutput),
@@ -1429,6 +1446,9 @@ async function runSingleStepInner(
1429
1446
  });
1430
1447
  }
1431
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 });
1432
1452
  if (step.effectiveAcceptance) {
1433
1453
  const acceptancePrompt = formatAcceptancePrompt(step.effectiveAcceptance, { reportOptional: isAgentContractV1(step.agentContract), structuredOutput: Boolean(step.structuredOutput?.acceptanceReportPath) });
1434
1454
  if (acceptancePrompt) task = `${task}\n${acceptancePrompt}`;
@@ -1492,14 +1512,18 @@ async function runSingleStepInner(
1492
1512
  }));
1493
1513
  try { fs.writeFileSync(ctx.outputFile, external.output, "utf-8"); } catch { /* Observability output is best-effort. */ }
1494
1514
  const resolvedOutput = step.outputPath && external.exitCode === 0
1495
- ? resolveSingleOutput(step.outputPath, external.output, outputSnapshot)
1515
+ ? resolveSingleOutput(step.outputPath, external.output, outputSnapshot, step.outputClaimPath)
1496
1516
  : { fullOutput: external.output };
1497
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;
1498
1522
  const finalizedOutput = finalizeSingleOutput(omitUndefinedProperties({
1499
1523
  fullOutput: resolvedOutput.fullOutput,
1500
1524
  outputPath: step.outputPath,
1501
1525
  outputMode: step.outputMode,
1502
- exitCode: external.exitCode ?? 1,
1526
+ exitCode: exitCode ?? 1,
1503
1527
  savedPath: resolvedOutput.savedPath,
1504
1528
  outputReference,
1505
1529
  saveError: resolvedOutput.saveError,
@@ -1514,16 +1538,17 @@ async function runSingleStepInner(
1514
1538
  : {};
1515
1539
  return omitUndefinedProperties({
1516
1540
  agent: step.agent,
1541
+ ...(childSessionName ? { sessionName: childSessionName } : {}),
1517
1542
  context: step.context,
1518
1543
  output: finalizedOutput.displayOutput,
1519
1544
  outputState: external.output.trim() ? "present" : "absent",
1520
- exitCode: external.exitCode,
1521
- error: external.error,
1545
+ exitCode,
1546
+ error,
1522
1547
  timedOut: external.timedOut,
1523
1548
  stopped: external.stopped,
1524
1549
  processSignal: external.processSignal,
1525
1550
  artifactPaths,
1526
- outputSaveError: artifactErrors.outputSaveError,
1551
+ outputSaveError: [resolvedOutput.saveError, artifactErrors.outputSaveError].filter(Boolean).join("\n") || undefined,
1527
1552
  metadataSaveError: artifactErrors.metadataSaveError,
1528
1553
  runner,
1529
1554
  externalProcess: external.externalProcess,
@@ -1557,14 +1582,18 @@ async function runSingleStepInner(
1557
1582
  }));
1558
1583
  try { fs.writeFileSync(ctx.outputFile, external.output, "utf-8"); } catch { /* Observability output is best-effort. */ }
1559
1584
  const resolvedOutput = step.outputPath && external.exitCode === 0
1560
- ? resolveSingleOutput(step.outputPath, external.output, outputSnapshot)
1585
+ ? resolveSingleOutput(step.outputPath, external.output, outputSnapshot, step.outputClaimPath)
1561
1586
  : { fullOutput: external.output };
1562
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;
1563
1592
  const finalizedOutput = finalizeSingleOutput(omitUndefinedProperties({
1564
1593
  fullOutput: resolvedOutput.fullOutput,
1565
1594
  outputPath: step.outputPath,
1566
1595
  outputMode: step.outputMode,
1567
- exitCode: external.exitCode,
1596
+ exitCode: exitCode ?? 1,
1568
1597
  savedPath: resolvedOutput.savedPath,
1569
1598
  outputReference,
1570
1599
  saveError: resolvedOutput.saveError,
@@ -1579,21 +1608,26 @@ async function runSingleStepInner(
1579
1608
  : {};
1580
1609
  return omitUndefinedProperties({
1581
1610
  agent: step.agent,
1611
+ ...(childSessionName ? { sessionName: childSessionName } : {}),
1582
1612
  context: step.context,
1583
1613
  output: finalizedOutput.displayOutput,
1584
1614
  outputState: external.output.trim() ? "present" : "absent",
1585
- exitCode: external.exitCode,
1586
- error: external.error,
1615
+ exitCode,
1616
+ error,
1587
1617
  timedOut: external.timedOut,
1588
1618
  stopped: external.stopped,
1589
1619
  artifactPaths,
1590
- outputSaveError: artifactErrors.outputSaveError,
1620
+ outputSaveError: [resolvedOutput.saveError, artifactErrors.outputSaveError].filter(Boolean).join("\n") || undefined,
1591
1621
  metadataSaveError: artifactErrors.metadataSaveError,
1592
1622
  runner,
1593
1623
  externalJob: external.externalJob,
1594
1624
  });
1595
1625
  }
1596
1626
 
1627
+ const effectiveCwd = step.cwd ?? ctx.cwd;
1628
+ const cwdError = preflightLaunchCwd(step.requestedCwd ?? effectiveCwd, effectiveCwd);
1629
+ if (cwdError) return { agent: step.agent, output: cwdError, error: cwdError, exitCode: 1, context: step.context };
1630
+
1597
1631
  const candidates = step.modelCandidates !== undefined
1598
1632
  ? step.modelCandidates.length > 0 ? step.modelCandidates : [undefined]
1599
1633
  : step.model
@@ -1612,7 +1646,6 @@ async function runSingleStepInner(
1612
1646
  let structuredAcceptanceReport: unknown;
1613
1647
  let structuredAcceptanceReportError: string | undefined;
1614
1648
  let completionGuardTriggeredFinal = false;
1615
- let turnBudget = ctx.turnBudget ? initialTurnBudgetState(ctx.turnBudget) : undefined;
1616
1649
  let toolBudget = step.toolBudget ? initialToolBudgetState(step.toolBudget) : undefined;
1617
1650
  let toolBudgetBlocked = false;
1618
1651
  let actualLaunchContractDigest = step.launchContractDigest;
@@ -1626,8 +1659,12 @@ async function runSingleStepInner(
1626
1659
  let taskDeliveryOverride: SubagentTaskDelivery | undefined;
1627
1660
  let contextOverflow = false;
1628
1661
  let launchWarningsEmitted = false;
1662
+ let abortRecoveryAttempted = false;
1663
+ let nextAttemptTask = task;
1629
1664
  modelAttemptsLoop: while (modelIndex < candidates.length) {
1630
1665
  if (ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break;
1666
+ const recoveringAbort = abortRecoveryAttempted;
1667
+ const attemptTask = nextAttemptTask;
1631
1668
  const candidate = candidates[modelIndex];
1632
1669
  const expectedModelForVerification = candidate && !(step.skipPrimaryModelVerification && modelIndex === 0) ? candidate : undefined;
1633
1670
  try {
@@ -1636,7 +1673,11 @@ async function runSingleStepInner(
1636
1673
  const message = error instanceof Error ? error.message : String(error);
1637
1674
  return omitUndefinedProperties({ agent: step.agent, output: message, error: message, exitCode: 1, context: step.context, thinkingCeiling: step.thinkingCeiling });
1638
1675
  }
1639
- ctx.onAttemptStart?.(omitUndefinedProperties({ model: candidate, thinking: resolveEffectiveThinking(candidate, step.thinking) }));
1676
+ ctx.onAttemptStart?.(omitUndefinedProperties({
1677
+ model: candidate,
1678
+ thinking: resolveEffectiveThinking(candidate, step.thinking),
1679
+ contextLimit: findModelInfo(candidate, step.modelVerificationRegistry)?.contextWindow,
1680
+ }));
1640
1681
  const outputSnapshot = captureSingleOutputSnapshot(step.outputPath);
1641
1682
  if (effectiveStructuredOutput) {
1642
1683
  try {
@@ -1658,27 +1699,32 @@ async function runSingleStepInner(
1658
1699
  const { args, env, tempDir, toolDiagnosticPath, runtimeAcknowledgedExtensionsPath, capabilityAudit: attemptCapabilityAudit, warnings } = buildPiArgs(omitUndefinedProperties({
1659
1700
  parentSessionId: step.parentSessionId,
1660
1701
  baseArgs: ["--mode", "json", "-p"],
1661
- task,
1702
+ task: attemptTask,
1662
1703
  taskDelivery: taskDeliveryOverride,
1663
1704
  sessionEnabled,
1664
1705
  sessionDir,
1665
1706
  sessionFile: step.sessionFile,
1666
1707
  model: candidate,
1667
1708
  inheritProjectContext: step.inheritProjectContext,
1709
+ inheritGlobalContext: step.inheritGlobalContext,
1668
1710
  inheritSkills: step.inheritSkills,
1669
1711
  requireReadTool: Boolean(step.skills?.length),
1670
1712
  tools: step.tools,
1713
+ allowNestedSubagents: step.allowNestedSubagents,
1671
1714
  extensions: step.extensions,
1672
1715
  subagentOnlyExtensions: step.subagentOnlyExtensions,
1673
1716
  fast: step.fast,
1674
1717
  modelCandidates: step.modelCandidates,
1675
- systemPrompt: appendTurnBudgetSystemPrompt(step.systemPrompt ?? "", ctx.turnBudget),
1718
+ systemPrompt: step.systemPrompt ?? "",
1676
1719
  systemPromptMode: step.systemPromptMode,
1677
1720
  mcpDirectTools: step.mcpDirectTools,
1721
+ mcpConfig: step.mcpConfig,
1722
+ runtimeServerNames: step.runtimeServerNames,
1678
1723
  capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
1679
1724
  cwd: step.cwd ?? ctx.cwd,
1680
1725
  promptFileStem: step.agent,
1681
1726
  intercomSessionName: ctx.childIntercomTarget,
1727
+ sessionName: childSessionName,
1682
1728
  orchestratorIntercomTarget: ctx.orchestratorIntercomTarget,
1683
1729
  runId: ctx.id,
1684
1730
  childAgentName: step.agent,
@@ -1702,6 +1748,7 @@ async function runSingleStepInner(
1702
1748
  : undefined,
1703
1749
  childWatchdog,
1704
1750
  waitToolEnabled: step.waitToolEnabled,
1751
+ waitToolDefaultTimeoutMs: step.waitToolDefaultTimeoutMs,
1705
1752
  thinkingCeiling: step.thinkingCeiling,
1706
1753
  extensionBindings,
1707
1754
  }));
@@ -1712,12 +1759,15 @@ async function runSingleStepInner(
1712
1759
  if (step.definitionDigest) {
1713
1760
  const toolPlan = resolvedTaskToolPlan ?? resolvePiLaunchToolPlan(omitUndefinedProperties({
1714
1761
  tools: step.tools,
1762
+ allowNestedSubagents: step.allowNestedSubagents,
1715
1763
  extensions: step.extensions,
1716
1764
  subagentOnlyExtensions: step.subagentOnlyExtensions,
1717
1765
  fast: step.fast,
1718
1766
  model: step.model,
1719
1767
  modelCandidates: step.modelCandidates,
1720
1768
  mcpDirectTools: step.mcpDirectTools,
1769
+ mcpConfig: step.mcpConfig,
1770
+ runtimeServerNames: step.runtimeServerNames,
1721
1771
  cwd: step.cwd ?? ctx.cwd,
1722
1772
  requireReadTool: Boolean(step.skills?.length),
1723
1773
  structuredOutput: Boolean(effectiveStructuredOutput),
@@ -1734,9 +1784,10 @@ async function runSingleStepInner(
1734
1784
  ...(step.fast !== undefined ? { fast: step.fast } : {}),
1735
1785
  ...(resolveEffectiveThinking(candidate, step.thinking) ? { thinking: resolveEffectiveThinking(candidate, step.thinking) } : {}),
1736
1786
  ...(step.thinkingCeiling ? { thinkingCeiling: step.thinkingCeiling } : {}),
1737
- systemPrompt: appendTurnBudgetSystemPrompt(step.systemPrompt ?? "", ctx.turnBudget),
1787
+ systemPrompt: step.systemPrompt ?? "",
1738
1788
  systemPromptMode: step.systemPromptMode,
1739
1789
  inheritProjectContext: step.inheritProjectContext,
1790
+ inheritGlobalContext: step.inheritGlobalContext,
1740
1791
  inheritSkills: step.inheritSkills,
1741
1792
  skills: step.skills,
1742
1793
  tools: toolPlan.effectiveToolAllowlist,
@@ -1766,13 +1817,13 @@ async function runSingleStepInner(
1766
1817
  ctx.timeoutMessage,
1767
1818
  ctx.registerStop,
1768
1819
  ctx.stopMessage,
1769
- ctx.registerTurnBudgetAbort,
1770
1820
  ctx.onWriterProcess,
1771
1821
  ctx.toolTimeoutMs,
1772
1822
  ctx.deadlineAt,
1773
1823
  ctx.orcaProgressTab,
1774
1824
  expectedModelForVerification,
1775
1825
  step.modelVerificationRegistry,
1826
+ step.mutationTools,
1776
1827
  );
1777
1828
  if (run.processCloseObservedAt !== undefined) {
1778
1829
  writerProcesses.push({
@@ -1785,25 +1836,6 @@ async function runSingleStepInner(
1785
1836
  processTree: run.processTree,
1786
1837
  });
1787
1838
  }
1788
- if (run.turnBudget) turnBudget = run.turnBudget;
1789
- else if (ctx.turnBudget) {
1790
- const assistantMessages = run.messages.filter((message) => message.role === "assistant");
1791
- const turnCount = assistantMessages.length;
1792
- const lastAssistantMessage = assistantMessages.at(-1);
1793
- if (turnCount > 0 && turnCount < ctx.turnBudget.maxTurns) {
1794
- turnBudget = { ...ctx.turnBudget, outcome: "within-budget", turnCount };
1795
- } else if (turnCount >= ctx.turnBudget.maxTurns) {
1796
- const decision = turnBudgetDecision(
1797
- ctx.turnBudget,
1798
- turnCount,
1799
- lastAssistantMessage ? isTerminalAssistantStop(lastAssistantMessage) : false,
1800
- lastAssistantMessage ? assistantStartsToolCall(lastAssistantMessage) : false,
1801
- );
1802
- turnBudget = decision === "defer"
1803
- ? turnBudgetDeferredState(ctx.turnBudget, turnCount)
1804
- : turnBudgetState(ctx.turnBudget, turnCount, decision === "abort");
1805
- }
1806
- }
1807
1839
  const toolAvailabilityError = run.exitCode === 0 && !run.error
1808
1840
  ? readChildToolDiagnosticError(toolDiagnosticPath)
1809
1841
  : undefined;
@@ -1814,7 +1846,6 @@ async function runSingleStepInner(
1814
1846
  && !run.interrupted
1815
1847
  && !run.timedOut
1816
1848
  && !run.stopped
1817
- && !run.turnBudgetExceeded
1818
1849
  && !run.protocolError
1819
1850
  && !toolAvailabilityError
1820
1851
  ? formatMidToolExitError({
@@ -1852,17 +1883,21 @@ async function runSingleStepInner(
1852
1883
  const hiddenError = run.exitCode === 0 && !run.error && !toolAvailabilityError && !structuredError && !midToolExitError
1853
1884
  ? detectSubagentError(errorMessages)
1854
1885
  : null;
1886
+ const terminalEmptyAfterUsefulWork = !validatedStructuredOutput
1887
+ && hasEmptyTerminalAssistantResponse(run.messages)
1888
+ && (run.toolCount > 0 || Boolean(run.finalOutput.trim()));
1855
1889
  const emptyOutputError = run.exitCode === 0
1856
1890
  && !run.error
1857
1891
  && !toolAvailabilityError
1858
1892
  && !structuredError
1859
- && !run.finalOutput.trim()
1860
1893
  && !validatedStructuredOutput
1894
+ && (!run.finalOutput.trim() || terminalEmptyAfterUsefulWork)
1861
1895
  && (!hiddenError?.hasError || hasEmptyTerminalAssistantResponse(run.messages))
1862
- ? "Subagent produced no output (possible model cold-start or empty response)."
1896
+ ? formatEmptyTerminalAssistantResponseError(run.messages)
1863
1897
  : undefined;
1864
1898
  const completionGuardEnabled = isAgentContractV1(step.agentContract) ? step.completionGuard === true : step.completionGuard !== false;
1865
1899
  const completionToolPlan = resolvedTaskToolPlan;
1900
+ const completionTools = completionToolPlan ? (completionToolPlan.explicitToolAllowlist ? completionToolPlan.effectiveToolAllowlist : undefined) : step.tools;
1866
1901
  const mutationEvidence = collectTrackedMutationEvidence(mutationSnapshot, step.cwd ?? ctx.cwd);
1867
1902
  finalMutationEvidence = mutationEvidence;
1868
1903
  const completionMutationEvidence = ctx.trackedMutationEvidenceForCompletionGuard === false ? undefined : mutationEvidence;
@@ -1871,29 +1906,32 @@ async function runSingleStepInner(
1871
1906
  agent: step.agent,
1872
1907
  task: taskForCompletionGuard,
1873
1908
  messages: run.messages,
1874
- tools: completionToolPlan ? (completionToolPlan.explicitToolAllowlist ? completionToolPlan.effectiveToolAllowlist : undefined) : step.tools,
1909
+ tools: completionTools,
1875
1910
  mcpDirectTools: completionToolPlan?.effectiveMcpTools ?? step.mcpDirectTools,
1911
+ mutationTools: step.mutationTools,
1876
1912
  toolAvailabilityError,
1877
1913
  mutationEvidence: completionMutationEvidence,
1878
1914
  }))
1879
1915
  : undefined;
1880
1916
  const mutationAttemptObserved = run.observedMutationAttempt === true || completionMutationEvidence?.attemptedMutation === true;
1881
- const completionGuardTriggered = completionGuard?.triggered === true && !mutationAttemptObserved;
1882
- const completionGuardBlocked = completionGuard?.blocked === true;
1883
- const fileMutationEffect = completionGuard
1884
- ? {
1885
- status: completionGuardBlocked ? "blocked" as const : completionGuard.expectedMutation ? completionGuardTriggered ? "missing" as const : "observed" as const : "not-applicable" as const,
1886
- expected: completionGuard.expectedMutation,
1887
- attempted: completionGuardBlocked ? false : completionGuard.attemptedMutation || mutationAttemptObserved,
1888
- ...(completionMutationEvidence ? { evidence: completionMutationEvidence } : {}),
1889
- ...(completionGuardBlocked && completionGuard.message ? { message: completionGuard.message } : {}),
1890
- ...(completionGuardTriggered ? { message: "Subagent completed without making edits for an implementation task." } : {}),
1891
- }
1892
- : undefined;
1893
- const completionGuardError = completionGuardTriggered && !isAgentContractV1(step.agentContract)
1894
- ? "Subagent completed without making edits for an implementation task.\nIt appears to have returned planning or scratchpad output instead of applying changes."
1917
+ const completionEvidence = planCompletionEvidence({
1918
+ guard: completionGuard,
1919
+ completionGuardEnabled,
1920
+ mutationCapable: hasMutationToolCapability(completionTools, completionToolPlan?.effectiveMcpTools ?? step.mcpDirectTools),
1921
+ implementationMutationExpected: expectsImplementationMutation(step.agent, taskForCompletionGuard),
1922
+ mutationAttemptObserved,
1923
+ mutationEvidence: completionMutationEvidence,
1924
+ agentContractV1: isAgentContractV1(step.agentContract),
1925
+ });
1926
+ const finalOutputHasPersistableFileContent = run.exitCode === 0 && !run.error && !emptyOutputError && Boolean(stripAcceptanceReport(run.finalOutput).trim());
1927
+ const requiredOutput = step.outputMode === "file-only" && step.outputPath
1928
+ ? { kind: "file-only" as const, path: step.outputPath, missing: !fs.existsSync(step.outputPath) && !finalOutputHasPersistableFileContent }
1929
+ : effectiveStructuredOutput
1930
+ ? { kind: "structured" as const, path: effectiveStructuredOutput.outputPath, missing: !fs.existsSync(effectiveStructuredOutput.outputPath) }
1895
1931
  : undefined;
1896
- const effectiveExitCode = toolAvailabilityError || (completionGuardTriggered && !isAgentContractV1(step.agentContract)) || midToolExitError || structuredError || emptyOutputError
1932
+ const missingRequiredOutputError = formatRequiredOutputError(requiredOutput);
1933
+ const missingRequiredOutputAfterMutation = Boolean(missingRequiredOutputError) && (mutationAttemptObserved || Boolean(mutationEvidence.changedFiles.length));
1934
+ const effectiveExitCode = toolAvailabilityError || completionEvidence.legacyFailureError || midToolExitError || structuredError || emptyOutputError || missingRequiredOutputError
1897
1935
  ? 1
1898
1936
  : hiddenError?.hasError
1899
1937
  ? (hiddenError.exitCode ?? 1)
@@ -1905,19 +1943,23 @@ async function runSingleStepInner(
1905
1943
  interrupted: run.interrupted,
1906
1944
  timedOut: run.timedOut,
1907
1945
  stopped: run.stopped,
1908
- turnBudgetExceeded: run.turnBudgetExceeded,
1909
1946
  })) ? formatProcessSignalError(run.processSignal!) : undefined;
1947
+ const underlyingError = toolAvailabilityError
1948
+ ?? midToolExitError
1949
+ ?? structuredError
1950
+ ?? run.error
1951
+ ?? signalError
1952
+ ?? (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined)
1953
+ ?? ((emptyOutputError || missingRequiredOutputError) && run.stderr.trim() ? run.stderr.trim() : undefined)
1954
+ ?? emptyOutputError
1955
+ ?? (missingRequiredOutputAfterMutation ? missingRequiredOutputError : undefined)
1956
+ ?? (hiddenError?.hasError
1957
+ ? hiddenError.details
1958
+ ? `${hiddenError.errorType} failed (exit ${effectiveExitCode}): ${hiddenError.details}`
1959
+ : `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
1960
+ : undefined);
1910
1961
  const error = formatSubagentExtensionConflictError(
1911
- toolAvailabilityError
1912
- ?? completionGuardError
1913
- ?? midToolExitError
1914
- ?? structuredError
1915
- ?? emptyOutputError
1916
- ?? (hiddenError?.hasError
1917
- ? hiddenError.details
1918
- ? `${hiddenError.errorType} failed (exit ${effectiveExitCode}): ${hiddenError.details}`
1919
- : `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
1920
- : run.error || signalError || (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined)),
1962
+ underlyingError ?? missingRequiredOutputError ?? completionEvidence.legacyFailureError,
1921
1963
  {
1922
1964
  agent: step.agent,
1923
1965
  ambientExtensionsEnabled: launchResolvedExtensions?.disableAmbientExtensions === false,
@@ -1931,8 +1973,8 @@ async function runSingleStepInner(
1931
1973
  usage: run.usage,
1932
1974
  });
1933
1975
  modelAttempts.push(attempt);
1934
- if (candidate && startupAttemptIndex === 0) attemptedModels.push(candidate);
1935
- completionGuardTriggeredFinal = completionGuardTriggered;
1976
+ if (!recoveringAbort && candidate && startupAttemptIndex === 0) attemptedModels.push(candidate);
1977
+ completionGuardTriggeredFinal = completionEvidence.guardTriggered && !underlyingError && !missingRequiredOutputError;
1936
1978
  finalOutputSnapshot = outputSnapshot;
1937
1979
  if (step.toolBudget) {
1938
1980
  const toolMessages = run.messages.filter((message) => message.role === "toolResult");
@@ -1940,10 +1982,48 @@ async function runSingleStepInner(
1940
1982
  toolBudgetBlocked = Boolean(blockedMessage);
1941
1983
  toolBudget = toolBudgetState(step.toolBudget, toolMessages.length, blockedMessage ? (blockedMessage as { toolName?: string }).toolName : undefined);
1942
1984
  }
1943
- finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error, structuredOutput, runtimeAcknowledgedExtensions, ...(step.agentContract ? { agentContract: step.agentContract } : {}), ...(fileMutationEffect ? { effects: { fileMutation: fileMutationEffect } } : {}) } as RunPiStreamingResult & { structuredOutput?: unknown; agentContract?: import("../../shared/types.ts").AgentContract; effects?: import("../../shared/types.ts").EffectsProjection };
1944
- if (run.turnBudgetExceeded) break modelAttemptsLoop;
1985
+ const settlementDiagnostic = projectSettlementDiagnostic(completionEvidence, {
1986
+ terminalFailed: effectiveExitCode !== 0,
1987
+ finalTextPresent: Boolean(stripAcceptanceReport(run.finalOutput).trim()),
1988
+ mutationObserved: mutationEvidence.attemptedMutation,
1989
+ requiredOutput,
1990
+ afterCompactionSettlement: run.afterCompactionSettlement === true,
1991
+ });
1992
+ const fileMutationEffect = completionEvidence.fileMutation ?? (missingRequiredOutputAfterMutation ? { status: "observed" as const, expected: completionEvidence.mutationExpected, attempted: true, evidence: mutationEvidence } : undefined);
1993
+ finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error, structuredOutput, runtimeAcknowledgedExtensions, ...(step.agentContract ? { agentContract: step.agentContract } : {}), ...(fileMutationEffect || settlementDiagnostic ? { effects: { ...(fileMutationEffect ? { fileMutation: fileMutationEffect } : {}), ...(settlementDiagnostic ? { settlementDiagnostic } : {}) } } : {}) } as RunPiStreamingResult;
1994
+ const abortRecovery = !attempt.success ? planAbortRecovery({
1995
+ messages: run.messages,
1996
+ error,
1997
+ processSignal: run.processSignal,
1998
+ sessionAvailable: Boolean(step.sessionFile && fs.existsSync(step.sessionFile)),
1999
+ alreadyResumed: abortRecoveryAttempted,
2000
+ stopped: run.stopped || ctx.stopSignal?.aborted || ctx.skipAcceptance?.(),
2001
+ interrupted: run.interrupted,
2002
+ timedOut: run.timedOut || ctx.timeoutSignal?.aborted,
2003
+ toolBudgetExhausted: run.toolBudgetBlocked || toolBudgetBlocked,
2004
+ usageBudgetExhausted: ctx.usageBudgetExhausted?.(),
2005
+ structuredOutputFailed: Boolean(structuredError),
2006
+ acceptanceFailed: false,
2007
+ currentTool: run.currentTool,
2008
+ afterCompactionSettlement: run.afterCompactionSettlement,
2009
+ }) : undefined;
2010
+ if (abortRecovery?.action === "settle" && abortRecovery.diagnostic) {
2011
+ attempt.error = attempt.error
2012
+ ? `${abortRecovery.diagnostic}\n${attempt.error.slice(0, 8_000)}`
2013
+ : abortRecovery.diagnostic;
2014
+ if (finalResult) finalResult.abortRecoveryDiagnostic = abortRecovery.diagnostic;
2015
+ }
1945
2016
  if (run.stopped || run.timedOut || ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break modelAttemptsLoop;
1946
- if (attempt.success || completionGuardTriggered) break modelAttemptsLoop;
2017
+ if (abortRecovery?.action === "settle" && abortRecovery.diagnostic) break modelAttemptsLoop;
2018
+ if (attempt.success) break modelAttemptsLoop;
2019
+ if (recoveringAbort) break modelAttemptsLoop;
2020
+ if (abortRecovery?.action === "resume") {
2021
+ abortRecoveryAttempted = true;
2022
+ nextAttemptTask = abortRecovery.prompt;
2023
+ attemptNotes.push("[abort-recovery] provider/transport abort after useful progress; resuming the retained child session once.");
2024
+ continue;
2025
+ }
2026
+ if (completionEvidence.guardTriggered) break modelAttemptsLoop;
1947
2027
 
1948
2028
  const startupFailure = isRetryableSubagentStartupFailure(omitUndefinedProperties({
1949
2029
  exitCode: effectiveExitCode,
@@ -1959,7 +2039,6 @@ async function runSingleStepInner(
1959
2039
  interrupted: run.interrupted,
1960
2040
  timedOut: run.timedOut,
1961
2041
  stopped: run.stopped,
1962
- turnBudgetExceeded: run.turnBudgetExceeded,
1963
2042
  }));
1964
2043
  const retryDelayMs = SUBAGENT_STARTUP_RETRY_DELAYS_MS[startupAttemptIndex];
1965
2044
  if (startupFailure && retryDelayMs !== undefined) {
@@ -2006,8 +2085,14 @@ async function runSingleStepInner(
2006
2085
  const rawOutput = finalResult?.finalOutput ?? "";
2007
2086
  const outputForPersistence = stripAcceptanceReport(rawOutput);
2008
2087
  const resolvedOutput = step.outputPath && finalResult?.exitCode === 0
2009
- ? resolveSingleOutput(step.outputPath, outputForPersistence, finalOutputSnapshot)
2088
+ ? resolveSingleOutput(step.outputPath, outputForPersistence, finalOutputSnapshot, step.outputClaimPath)
2010
2089
  : { fullOutput: outputForPersistence };
2090
+ if (resolvedOutput.fatalError) {
2091
+ if (finalResult) {
2092
+ finalResult.exitCode = 1;
2093
+ finalResult.error = finalResult.error ? `${finalResult.error}\n${resolvedOutput.saveError}` : resolvedOutput.saveError;
2094
+ }
2095
+ }
2011
2096
  const output = stripAcceptanceReport(resolvedOutput.fullOutput);
2012
2097
  const outputReference = resolvedOutput.savedPath ? formatSavedOutputReference(resolvedOutput.savedPath, output) : undefined;
2013
2098
  let outputForSummary = output;
@@ -2016,14 +2101,6 @@ async function runSingleStepInner(
2016
2101
  }
2017
2102
  if (finalResult?.stopped && !outputForSummary.trim()) {
2018
2103
  outputForSummary = ctx.stopMessage ?? "Subagent stopped by user.";
2019
- } else if (!finalResult?.timedOut && !finalResult?.stopped && finalResult?.turnBudgetExceeded && turnBudget) {
2020
- outputForSummary = formatTurnBudgetOutput(turnBudgetExceededMessage(turnBudget, turnBudget.turnCount), outputForSummary);
2021
- } else if (!finalResult?.timedOut && !finalResult?.stopped && turnBudget?.outcome === "termination-deferred") {
2022
- const note = turnBudgetDeferredNote(turnBudget, turnBudget.terminationDeferredAtTurn ?? turnBudget.turnCount);
2023
- outputForSummary = outputForSummary.trim() ? `${note}\n\n${outputForSummary}` : note;
2024
- } else if (!finalResult?.timedOut && !finalResult?.stopped && turnBudget?.outcome === "wrap-up-requested") {
2025
- const note = turnBudgetSoftNote(turnBudget, turnBudget.wrapUpRequestedAtTurn ?? turnBudget.turnCount);
2026
- outputForSummary = outputForSummary.trim() ? `${note}\n\n${outputForSummary}` : note;
2027
2104
  }
2028
2105
  const outputForAcceptance = rawOutput;
2029
2106
  const childWrittenOutput = step.outputPath
@@ -2061,7 +2138,7 @@ async function runSingleStepInner(
2061
2138
  saveError: resolvedOutput.saveError,
2062
2139
  }));
2063
2140
  outputForSummary = finalizedOutput.displayOutput;
2064
- const acceptance = step.effectiveAcceptance && !finalResult?.stopped && !finalResult?.turnBudgetExceeded && !ctx.timeoutSignal?.aborted && !ctx.stopSignal?.aborted && !ctx.skipAcceptance?.()
2141
+ const acceptance = step.effectiveAcceptance && !finalResult?.stopped && !ctx.timeoutSignal?.aborted && !ctx.stopSignal?.aborted && !ctx.skipAcceptance?.()
2065
2142
  ? await evaluateAcceptance(omitUndefinedProperties({
2066
2143
  acceptance: step.effectiveAcceptance,
2067
2144
  output: outputForAcceptance,
@@ -2080,40 +2157,31 @@ async function runSingleStepInner(
2080
2157
  : undefined;
2081
2158
  const stoppedAfterAcceptance = finalResult?.stopped === true || ctx.stopSignal?.aborted === true;
2082
2159
  const timedOutAfterAcceptance = !stoppedAfterAcceptance && (finalResult?.timedOut === true || ctx.timeoutSignal?.aborted === true);
2083
- const turnBudgetExceeded = finalResult?.turnBudgetExceeded === true;
2084
2160
  const effectiveAcceptance = step.effectiveAcceptance
2085
2161
  ? stoppedAfterAcceptance
2086
2162
  ? buildSkippedAcceptanceLedger(step.effectiveAcceptance, { id: "stopped", message: "Acceptance was not evaluated because the subagent was stopped." })
2087
2163
  : timedOutAfterAcceptance
2088
2164
  ? buildSkippedAcceptanceLedger(step.effectiveAcceptance, { id: "timeout", message: "Acceptance was not evaluated because the subagent timed out." })
2089
- : turnBudgetExceeded
2090
- ? buildSkippedAcceptanceLedger(step.effectiveAcceptance, { id: "turn-budget", message: "Acceptance was not evaluated because the subagent exceeded its turn budget." })
2091
- : acceptance
2165
+ : acceptance
2092
2166
  : undefined;
2093
2167
  const acceptanceFailure = effectiveAcceptance ? acceptanceFailureMessage(effectiveAcceptance) : undefined;
2094
- const acceptanceCanFailRun = acceptanceFailure && effectiveAcceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted && !timedOutAfterAcceptance && !stoppedAfterAcceptance && !turnBudgetExceeded && !isAgentContractV1(step.agentContract);
2095
- const effectiveFinalExitCode = timedOutAfterAcceptance || stoppedAfterAcceptance || turnBudgetExceeded ? 1 : acceptanceCanFailRun ? 1 : finalResult?.exitCode ?? 1;
2168
+ const acceptanceCanFailRun = acceptanceFailure && effectiveAcceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted && !timedOutAfterAcceptance && !stoppedAfterAcceptance && !isAgentContractV1(step.agentContract);
2169
+ const effectiveFinalExitCode = timedOutAfterAcceptance || stoppedAfterAcceptance ? 1 : acceptanceCanFailRun ? 1 : finalResult?.exitCode ?? 1;
2096
2170
  const intercomDetachReceipt = finalResult?.finalOutput === INTERCOM_DETACH_RECEIPT;
2097
2171
  const baseFinalError = stoppedAfterAcceptance
2098
2172
  ? ctx.stopMessage ?? "Subagent stopped by user."
2099
2173
  : timedOutAfterAcceptance
2100
2174
  ? finalResult?.error ?? ctx.timeoutMessage ?? "Subagent timed out."
2101
- : turnBudgetExceeded
2102
- ? finalResult?.error ?? (turnBudget ? turnBudgetExceededMessage(turnBudget, turnBudget.turnCount) : "Subagent exceeded turn budget.")
2103
- : acceptanceCanFailRun
2175
+ : acceptanceCanFailRun
2104
2176
  ? (finalResult?.error ? `${finalResult.error}\n${acceptanceFailure}` : acceptanceFailure)
2105
2177
  : finalResult?.error ?? (intercomDetachReceipt ? INTERCOM_DETACH_RECEIPT : undefined);
2106
2178
  const effectiveFinalError = formatChildFailureDiagnostic({
2107
2179
  error: baseFinalError,
2108
- afterCompactionSettlement: finalResult?.afterCompactionSettlement,
2109
- missingFileOnlyOutput: effectiveFinalExitCode !== 0
2110
- && finalResult?.afterCompactionSettlement
2111
- && step.outputMode === "file-only"
2112
- && step.outputPath
2113
- && !fs.existsSync(step.outputPath)
2114
- ? step.outputPath
2115
- : undefined,
2180
+ afterCompactionSettlement: effectiveFinalExitCode !== 0 ? finalResult?.afterCompactionSettlement : undefined,
2181
+ abortRecoveryDiagnostic: effectiveFinalExitCode !== 0 ? finalResult?.abortRecoveryDiagnostic : undefined,
2182
+ requiredOutput: effectiveFinalExitCode !== 0 ? finalResult?.effects?.settlementDiagnostic?.requiredOutput : undefined,
2116
2183
  });
2184
+ const usage = usageFromAttempts(modelAttempts);
2117
2185
 
2118
2186
  const artifactErrors = artifactPaths && ctx.artifactConfig?.enabled !== false
2119
2187
  ? persistStepArtifacts({
@@ -2133,6 +2201,7 @@ async function runSingleStepInner(
2133
2201
  model: finalResult?.model,
2134
2202
  attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
2135
2203
  modelAttempts,
2204
+ usage,
2136
2205
  error: effectiveFinalError,
2137
2206
  acceptance: effectiveAcceptance,
2138
2207
  ...(capabilityAudit ? { capabilityCeiling: capabilityAudit.ceiling, capabilityAudit } : {}),
@@ -2149,6 +2218,7 @@ async function runSingleStepInner(
2149
2218
 
2150
2219
  const result: StepResult & { completionGuardTriggered?: boolean } = omitUndefinedProperties({
2151
2220
  agent: step.agent,
2221
+ ...(childSessionName ? { sessionName: childSessionName } : {}),
2152
2222
  context: step.context,
2153
2223
  ...(step.agentContract ? { agentContract: step.agentContract } : {}),
2154
2224
  launchContractDigest: actualLaunchContractDigest,
@@ -2164,26 +2234,24 @@ async function runSingleStepInner(
2164
2234
  modelAttempts,
2165
2235
  contextOverflow: contextOverflow || undefined,
2166
2236
  totalCost: costSummaryFromAttempts(modelAttempts),
2237
+ usage,
2167
2238
  artifactPaths,
2168
- outputSaveError: artifactErrors.outputSaveError,
2239
+ outputSaveError: [resolvedOutput.saveError, artifactErrors.outputSaveError].filter(Boolean).join("\n") || undefined,
2169
2240
  metadataSaveError: artifactErrors.metadataSaveError,
2170
2241
  transcriptPath: transcriptWriter ? artifactPaths?.transcriptPath : undefined,
2171
2242
  transcriptError: transcriptWriter?.getError(),
2172
- interrupted: timedOutAfterAcceptance || stoppedAfterAcceptance || turnBudgetExceeded ? false : finalResult?.interrupted,
2243
+ interrupted: timedOutAfterAcceptance || stoppedAfterAcceptance ? false : finalResult?.interrupted,
2173
2244
  timedOut: timedOutAfterAcceptance ? true : finalResult?.timedOut,
2174
2245
  stopped: stoppedAfterAcceptance ? true : finalResult?.stopped,
2175
2246
  processSignal: finalResult?.processSignal,
2176
2247
  timeoutRecovery,
2177
- turnBudget,
2178
- turnBudgetExceeded: turnBudgetExceeded || undefined,
2179
- wrapUpRequested: finalResult?.wrapUpRequested || turnBudget?.outcome === "wrap-up-requested" || turnBudget?.outcome === "termination-deferred" || turnBudgetExceeded || undefined,
2180
2248
  toolBudget,
2181
2249
  toolBudgetBlocked: toolBudgetBlocked || undefined,
2182
2250
  completionGuardTriggered: completionGuardTriggeredFinal,
2183
2251
  ...((finalResult as (RunPiStreamingResult & { effects?: import("../../shared/types.ts").EffectsProjection }) | undefined)?.effects ? { effects: (finalResult as RunPiStreamingResult & { effects?: import("../../shared/types.ts").EffectsProjection }).effects } : {}),
2184
- structuredOutput: timedOutAfterAcceptance || stoppedAfterAcceptance || turnBudgetExceeded ? undefined : (finalResult as (RunPiStreamingResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput,
2185
- structuredOutputPath: timedOutAfterAcceptance || stoppedAfterAcceptance || turnBudgetExceeded ? undefined : effectiveStructuredOutput?.outputPath,
2186
- structuredOutputSchemaPath: timedOutAfterAcceptance || stoppedAfterAcceptance || turnBudgetExceeded ? undefined : effectiveStructuredOutput?.schemaPath,
2252
+ structuredOutput: timedOutAfterAcceptance || stoppedAfterAcceptance ? undefined : (finalResult as (RunPiStreamingResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput,
2253
+ structuredOutputPath: timedOutAfterAcceptance || stoppedAfterAcceptance ? undefined : effectiveStructuredOutput?.outputPath,
2254
+ structuredOutputSchemaPath: timedOutAfterAcceptance || stoppedAfterAcceptance ? undefined : effectiveStructuredOutput?.schemaPath,
2187
2255
  acceptance: effectiveAcceptance,
2188
2256
  watchdog: finalResult?.watchdog,
2189
2257
  ...(capabilityAudit ? { capabilityCeiling: capabilityAudit.ceiling, capabilityAudit } : {}),
@@ -2257,6 +2325,11 @@ function requiredStatusStep(statusPayload: RunnerStatusPayload, index: number):
2257
2325
  return step;
2258
2326
  }
2259
2327
 
2328
+ function setStatusWorktreeReference(statusStep: RunnerStatusStep, worktree: WorktreeSetup["worktrees"][number]): void {
2329
+ statusStep.worktreePath = worktree.path;
2330
+ statusStep.branch = worktree.branch;
2331
+ }
2332
+
2260
2333
  function markParallelGroupSetupFailure(input: {
2261
2334
  statusPayload: RunnerStatusPayload;
2262
2335
  results: StepResult[];
@@ -2385,6 +2458,25 @@ function resolveAsyncStepTranscriptPath(input: {
2385
2458
 
2386
2459
  type SingleStepResult = Awaited<ReturnType<typeof runSingleStep>>;
2387
2460
 
2461
+ function missingRequiredOutputAfterUsefulMutation(result: SingleStepResult): boolean {
2462
+ const effects = result.effects;
2463
+ return effects?.settlementDiagnostic?.requiredOutput?.missing === true
2464
+ && (effects.settlementDiagnostic.mutation.attempted || effects.fileMutation?.attempted === true || Boolean(effects.fileMutation?.evidence?.changedFiles.length));
2465
+ }
2466
+
2467
+ function partialExecutionWithUsefulMutation(result: SingleStepResult): boolean {
2468
+ const fileMutation = result.effects?.fileMutation;
2469
+ return result.execution?.status === "partial" && (fileMutation?.attempted === true || Boolean(fileMutation?.evidence?.changedFiles.length));
2470
+ }
2471
+
2472
+ function partialEvidenceResult(result: SingleStepResult): boolean {
2473
+ return missingRequiredOutputAfterUsefulMutation(result) || partialExecutionWithUsefulMutation(result);
2474
+ }
2475
+
2476
+ function concreteFailureResult(result: SingleStepResult): boolean {
2477
+ return result.success === false && !partialEvidenceResult(result);
2478
+ }
2479
+
2388
2480
  function combinedAbortSignal(signals: Array<AbortSignal | undefined>): AbortSignal | undefined {
2389
2481
  const activeSignals = signals.filter((signal): signal is AbortSignal => Boolean(signal));
2390
2482
  if (activeSignals.length === 0) return undefined;
@@ -2456,6 +2548,8 @@ async function runSubagent(
2456
2548
  const overallStartTime = Date.now();
2457
2549
  const shareEnabled = config.share === true;
2458
2550
  const asyncDir = config.asyncDir;
2551
+ const handoffWorkflowKey = config.workflowKey;
2552
+ const handoffChildRunId = handoffWorkflowKey ? id : undefined;
2459
2553
  const statusPath = path.join(asyncDir, "status.json");
2460
2554
  const eventsPath = path.join(asyncDir, "events.jsonl");
2461
2555
  const logPath = path.join(asyncDir, `subagent-log-${id}.md`);
@@ -2464,7 +2558,6 @@ async function runSubagent(
2464
2558
  const activeChildTimeouts = new Map<number, () => void>();
2465
2559
  const activeChildStops = new Map<number, () => void>();
2466
2560
  const childStopRequests = new Map<number, { childId: string; requestedAt: number }>();
2467
- const activeChildTurnBudgetAborts = new Map<number, (message: string, state?: TurnBudgetState) => void>();
2468
2561
  const pendingStepSteers: SteerRequest[] = [];
2469
2562
  const steeringCapabilities = new Map<number, SteerCapability>();
2470
2563
  let interrupted = false;
@@ -2473,7 +2566,6 @@ async function runSubagent(
2473
2566
  let timeoutTimer: NodeJS.Timeout | undefined;
2474
2567
  let timedOut = false;
2475
2568
  let stopped = false;
2476
- let turnBudgetExceeded = false;
2477
2569
  let usageBudgetExceeded = false;
2478
2570
  const timeoutMessage = config.timeoutMs !== undefined ? `Subagent timed out after ${config.timeoutMs}ms.` : undefined;
2479
2571
  const stopMessage = "Subagent stopped by user.";
@@ -2494,8 +2586,11 @@ async function runSubagent(
2494
2586
  for (const task of step.parallel) {
2495
2587
  const taskFlatIndex = flatStepCount;
2496
2588
  const transcriptPath = resolveAsyncStepTranscriptPath(omitUndefinedProperties({ artifactsDir, artifactConfig, runId: id, agent: task.agent, flatIndex: taskFlatIndex, flatStepCount: initialFlatStepCount }));
2589
+ const taskSessionName = task.sessionName ?? deriveChildSessionName({ agent: task.agent, task: task.task, label: task.label });
2497
2590
  initialStatusSteps.push(omitUndefinedProperties({
2498
2591
  agent: task.agent,
2592
+ ...(task.lane ? { lane: task.lane } : config.lane ? { lane: config.lane } : {}),
2593
+ ...(taskSessionName ? { sessionName: taskSessionName } : {}),
2499
2594
  ...(externalRunnerStatus(task.runner) ? { runner: externalRunnerStatus(task.runner) } : {}),
2500
2595
  ...(statusStepDescription(task.task) ? { description: statusStepDescription(task.task) } : {}),
2501
2596
  ...(task.context ? { context: task.context } : {}),
@@ -2514,6 +2609,7 @@ async function runSubagent(
2514
2609
  ...(transcriptPath ? { transcriptPath } : {}),
2515
2610
  skills: task.skills,
2516
2611
  model: task.model,
2612
+ ...(task.contextLimit !== undefined ? { contextLimit: task.contextLimit } : {}),
2517
2613
  thinking: task.thinking,
2518
2614
  attemptedModels: task.modelCandidates && task.modelCandidates.length > 0 ? task.modelCandidates : task.model ? [task.model] : undefined,
2519
2615
  recentTools: [],
@@ -2531,6 +2627,7 @@ async function runSubagent(
2531
2627
  label: step.label ?? step.parallel.label ?? `Dynamic fanout (${step.collect.as})`,
2532
2628
  outputName: step.collect.as,
2533
2629
  structured: Boolean(step.collect.outputSchema),
2630
+ ...(step.parallel.contextLimit !== undefined ? { contextLimit: step.parallel.contextLimit } : {}),
2534
2631
  ...(step.agentContract ? { agentContract: step.agentContract } : {}),
2535
2632
  ...(step.capabilityCeiling ? { capabilityCeiling: step.capabilityCeiling } : {}),
2536
2633
  ...(step.thinkingCeiling ? { thinkingCeiling: step.thinkingCeiling } : {}),
@@ -2543,8 +2640,11 @@ async function runSubagent(
2543
2640
  } else {
2544
2641
  const stepFlatIndex = flatStepCount;
2545
2642
  const transcriptPath = resolveAsyncStepTranscriptPath(omitUndefinedProperties({ artifactsDir, artifactConfig, runId: id, agent: step.agent, flatIndex: stepFlatIndex, flatStepCount: initialFlatStepCount }));
2643
+ const stepSessionName = step.sessionName ?? deriveChildSessionName({ agent: step.agent, task: step.task, label: step.label });
2546
2644
  initialStatusSteps.push(omitUndefinedProperties({
2547
2645
  agent: step.agent,
2646
+ ...(step.lane ? { lane: step.lane } : config.lane ? { lane: config.lane } : {}),
2647
+ ...(stepSessionName ? { sessionName: stepSessionName } : {}),
2548
2648
  ...(externalRunnerStatus(step.runner) ? { runner: externalRunnerStatus(step.runner) } : {}),
2549
2649
  ...(statusStepDescription(step.task) ? { description: statusStepDescription(step.task) } : {}),
2550
2650
  ...(step.context ? { context: step.context } : {}),
@@ -2563,6 +2663,7 @@ async function runSubagent(
2563
2663
  ...(transcriptPath ? { transcriptPath } : {}),
2564
2664
  skills: step.skills,
2565
2665
  model: step.model,
2666
+ ...(step.contextLimit !== undefined ? { contextLimit: step.contextLimit } : {}),
2566
2667
  thinking: step.thinking,
2567
2668
  attemptedModels: step.modelCandidates && step.modelCandidates.length > 0 ? step.modelCandidates : step.model ? [step.model] : undefined,
2568
2669
  recentTools: [],
@@ -2606,7 +2707,6 @@ async function runSubagent(
2606
2707
  lastUpdate: overallStartTime,
2607
2708
  ...(config.timeoutMs !== undefined ? { timeoutMs: config.timeoutMs } : {}),
2608
2709
  ...(config.deadlineAt !== undefined ? { deadlineAt: config.deadlineAt } : {}),
2609
- ...(config.turnBudget ? { turnBudget: initialTurnBudgetState(config.turnBudget) } : {}),
2610
2710
  ...(config.toolBudget ? { toolBudget: initialToolBudgetState(config.toolBudget) } : {}),
2611
2711
  ...(config.usageBudget ? { usageBudget: usageBudgetState(config.usageBudget, undefined) } : {}),
2612
2712
  pid: process.pid,
@@ -2621,6 +2721,7 @@ async function runSubagent(
2621
2721
  ...(config.runFanoutBudget ? { runFanoutBudget: getRunFanoutBudgetSnapshot(config.runFanoutBudget) } : {}),
2622
2722
  ...(config.parentWorkflowRunId ? { parentWorkflowRunId: config.parentWorkflowRunId } : {}),
2623
2723
  ...(config.workflowKey ? { workflowKey: config.workflowKey } : {}),
2724
+ ...(config.lane ? { lane: config.lane } : {}),
2624
2725
  ...(config.runnerProcessInstanceId ? { processTerminal: { version: 1 as const, state: "pending" as const, runId: id, runnerProcessInstanceId: config.runnerProcessInstanceId } } : {}),
2625
2726
  steps: initialStatusSteps,
2626
2727
  artifactsDir,
@@ -2738,6 +2839,7 @@ async function runSubagent(
2738
2839
  const statusResultSummary = (state: AsyncStatus["state"]): string => {
2739
2840
  if (statusPayload.error) return statusPayload.error;
2740
2841
  if (state === "paused") return "Paused after interrupt. Waiting for explicit next action.";
2842
+ if (state === "partial") return "Subagent needs attention after partial work.";
2741
2843
  if (state === "stopped") return stopMessage;
2742
2844
  if (state === "rejected") return "Subagent rejected.";
2743
2845
  return state === "complete" ? "Subagent completed." : "Subagent failed.";
@@ -2746,12 +2848,13 @@ async function runSubagent(
2746
2848
  if (step.status === "complete" || step.status === "completed") return true;
2747
2849
  if (step.status === "failed" || step.status === "stopped" || step.status === "rejected") return false;
2748
2850
  if (state === "complete") return true;
2749
- if (state === "failed" || state === "stopped" || state === "rejected") return false;
2851
+ if (state === "failed" || state === "partial" || state === "stopped" || state === "rejected") return false;
2750
2852
  return undefined;
2751
2853
  };
2752
2854
  const writeRecoverableStatusResult = (): void => {
2753
2855
  const state = statusResultState();
2754
2856
  if (!state || finalResultCommitted || !config.sessionId) return;
2857
+ if ((state as string) === "complete") return;
2755
2858
  const now = statusPayload.endedAt ?? statusPayload.lastUpdate ?? Date.now();
2756
2859
  const summary = statusResultSummary(state);
2757
2860
  runPersistence.write(resultPath, omitUndefinedProperties({
@@ -2763,10 +2866,11 @@ async function runSubagent(
2763
2866
  success: state === "complete",
2764
2867
  state,
2765
2868
  summary,
2766
- error: state === "failed" || state === "stopped" || state === "rejected" ? summary : undefined,
2869
+ error: state === "failed" || state === "partial" || state === "stopped" || state === "rejected" ? summary : undefined,
2767
2870
  stopped: state === "stopped" ? true : undefined,
2768
2871
  results: statusPayload.steps.map((step) => omitUndefinedProperties({
2769
2872
  agent: step.agent,
2873
+ ...(step.sessionName ? { sessionName: step.sessionName } : {}),
2770
2874
  output: step.status === "complete" || step.status === "completed" ? "" : step.error ?? summary,
2771
2875
  error: step.error,
2772
2876
  success: statusResultSuccess(state, step),
@@ -2774,6 +2878,7 @@ async function runSubagent(
2774
2878
  model: step.model,
2775
2879
  attemptedModels: step.attemptedModels,
2776
2880
  modelAttempts: step.modelAttempts,
2881
+ usage: usageFromAttempts(step.modelAttempts),
2777
2882
  contextOverflow: step.contextOverflow,
2778
2883
  })),
2779
2884
  exitCode: state === "complete" || state === "paused" ? 0 : 1,
@@ -2868,7 +2973,7 @@ async function runSubagent(
2868
2973
  };
2869
2974
  const childStopResult = (index: number, agent: string, context?: "fresh" | "fork"): SingleStepResult => {
2870
2975
  markChildStopped(index);
2871
- return stoppedStepResult(agent, context);
2976
+ return stoppedStepResult(agent, context, requiredStatusStep(statusPayload, index).sessionName);
2872
2977
  };
2873
2978
  const stopChildStep = (request: StopRequest): void => {
2874
2979
  if (request.targetIndex === undefined) {
@@ -2911,13 +3016,6 @@ async function runSubagent(
2911
3016
  activeChildStops.set(flatIndex, stop);
2912
3017
  if (stopped || childStopRequests.has(flatIndex)) stop();
2913
3018
  };
2914
- const registerStepTurnBudgetAbort = (flatIndex: number, abort: ((message: string, state?: TurnBudgetState) => void) | undefined): void => {
2915
- if (!abort) {
2916
- activeChildTurnBudgetAborts.delete(flatIndex);
2917
- return;
2918
- }
2919
- activeChildTurnBudgetAborts.set(flatIndex, abort);
2920
- };
2921
3019
  const interruptActiveChildren = (): void => {
2922
3020
  for (const interrupt of [...activeChildInterrupts.values()]) interrupt();
2923
3021
  };
@@ -3027,23 +3125,26 @@ async function runSubagent(
3027
3125
  }
3028
3126
  }
3029
3127
  };
3030
- const pausedStepResult = (agent: string, context?: "fresh" | "fork"): SingleStepResult => omitUndefinedProperties({
3128
+ const pausedStepResult = (agent: string, context?: "fresh" | "fork", sessionName?: string): SingleStepResult => omitUndefinedProperties({
3031
3129
  agent,
3130
+ sessionName,
3032
3131
  context,
3033
3132
  output: "Paused after interrupt. Waiting for explicit next action.",
3034
3133
  exitCode: 0,
3035
3134
  interrupted: true,
3036
3135
  });
3037
- const timedOutStepResult = (agent: string, context?: "fresh" | "fork"): SingleStepResult => omitUndefinedProperties({
3136
+ const timedOutStepResult = (agent: string, context?: "fresh" | "fork", sessionName?: string): SingleStepResult => omitUndefinedProperties({
3038
3137
  agent,
3138
+ sessionName,
3039
3139
  context,
3040
3140
  output: timeoutMessage ?? "Subagent timed out.",
3041
3141
  error: timeoutMessage ?? "Subagent timed out.",
3042
3142
  exitCode: 1,
3043
3143
  timedOut: true,
3044
3144
  });
3045
- const stoppedStepResult = (agent: string, context?: "fresh" | "fork"): SingleStepResult => omitUndefinedProperties({
3145
+ const stoppedStepResult = (agent: string, context?: "fresh" | "fork", sessionName?: string): SingleStepResult => omitUndefinedProperties({
3046
3146
  agent,
3147
+ sessionName,
3047
3148
  context,
3048
3149
  output: stopMessage,
3049
3150
  error: stopMessage,
@@ -3424,74 +3525,15 @@ async function runSubagent(
3424
3525
  }
3425
3526
  pendingStepSteers.push(...remaining);
3426
3527
  };
3427
- const updateStepModel = (flatIndex: number, model: string | undefined, thinking: string | undefined, now = Date.now()): void => {
3528
+ const updateStepModel = (flatIndex: number, model: string | undefined, thinking: string | undefined, contextLimit?: number, now = Date.now()): void => {
3428
3529
  const step = statusPayload.steps[flatIndex];
3429
3530
  if (!step) return;
3430
3531
  setOptionalProperty(step, "model", model);
3431
3532
  setOptionalProperty(step, "thinking", thinking);
3533
+ setOptionalProperty(step, "contextLimit", contextLimit);
3432
3534
  statusPayload.lastUpdate = now;
3433
3535
  writeStatusPayload();
3434
3536
  };
3435
- const updateStepTurnBudget = (
3436
- flatIndex: number,
3437
- turnCount: number,
3438
- now: number,
3439
- terminalAssistantStop: boolean,
3440
- toolWorkActiveOrStarting: boolean,
3441
- ): void => {
3442
- const budget = config.turnBudget;
3443
- const step = statusPayload.steps[flatIndex];
3444
- if (!budget || !step || timedOut || stopped || turnBudgetExceeded || step.turnBudgetExceeded) return;
3445
- if (turnCount < budget.maxTurns) {
3446
- const state: TurnBudgetState = { ...budget, outcome: "within-budget", turnCount };
3447
- step.turnBudget = state;
3448
- statusPayload.turnBudget = state;
3449
- return;
3450
- }
3451
- if (!step.wrapUpRequested) {
3452
- step.wrapUpRequested = true;
3453
- statusPayload.wrapUpRequested = true;
3454
- appendRecentStepOutput(step, [turnBudgetSoftNote(budget, turnCount)]);
3455
- }
3456
- const decision = turnBudgetDecision(budget, turnCount, terminalAssistantStop, toolWorkActiveOrStarting);
3457
- if (decision === "defer") {
3458
- const firstDeferredTurn = step.turnBudget?.terminationDeferredAtTurn;
3459
- const deferredState = turnBudgetDeferredState(budget, turnCount, firstDeferredTurn);
3460
- step.turnBudget = deferredState;
3461
- statusPayload.turnBudget = deferredState;
3462
- if (firstDeferredTurn === undefined) {
3463
- appendJsonl(eventsPath, JSON.stringify({
3464
- type: "subagent.step.turn_budget_deferred",
3465
- ts: now,
3466
- runId: id,
3467
- stepIndex: flatIndex,
3468
- agent: step.agent,
3469
- turnCount,
3470
- maxTurns: budget.maxTurns,
3471
- graceTurns: budget.graceTurns,
3472
- }));
3473
- }
3474
- return;
3475
- }
3476
- const state = turnBudgetState(budget, turnCount, false);
3477
- step.turnBudget = state;
3478
- statusPayload.turnBudget = state;
3479
- if (decision !== "abort") return;
3480
- const exceededState = turnBudgetState(budget, turnCount, true);
3481
- const message = turnBudgetExceededMessage(budget, turnCount);
3482
- step.turnBudget = exceededState;
3483
- step.turnBudgetExceeded = true;
3484
- step.wrapUpRequested = true;
3485
- step.error = message;
3486
- turnBudgetExceeded = true;
3487
- statusPayload.turnBudget = exceededState;
3488
- statusPayload.turnBudgetExceeded = true;
3489
- statusPayload.wrapUpRequested = true;
3490
- statusPayload.error = message;
3491
- statusPayload.lastUpdate = now;
3492
- 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 }));
3493
- activeChildTurnBudgetAborts.get(flatIndex)?.(message, exceededState);
3494
- };
3495
3537
  const updateStepFromChildEvent = (flatIndex: number, event: ChildEvent): void => {
3496
3538
  const step = statusPayload.steps[flatIndex];
3497
3539
  if (!step) return;
@@ -3514,7 +3556,7 @@ async function runSubagent(
3514
3556
  return;
3515
3557
  }
3516
3558
  if (event.type === "tool_execution_start" && event.toolName) {
3517
- const mutates = isMutatingTool(event.toolName, event.args);
3559
+ const mutates = isMutatingTool(event.toolName, event.args, flatSteps[flatIndex]?.mutationTools);
3518
3560
  const currentPath = resolveCurrentPath(event.toolName, event.args);
3519
3561
  const argsPreview = extractToolArgsPreview(event.args ?? {});
3520
3562
  const blocksSupervisor = isBlockingSupervisorTool(event.toolName, event.args);
@@ -3632,13 +3674,6 @@ async function runSubagent(
3632
3674
  refreshUsageBudget();
3633
3675
  }
3634
3676
  statusPayload.turnCount = Math.max(statusPayload.turnCount ?? 0, step.turnCount);
3635
- updateStepTurnBudget(
3636
- flatIndex,
3637
- step.turnCount,
3638
- now,
3639
- isTerminalAssistantStop(event.message),
3640
- assistantStartsToolCall(event.message) || Boolean(step.currentTool),
3641
- );
3642
3677
  }
3643
3678
  syncTopLevelCurrentTool();
3644
3679
  step.lastActivityAt = now;
@@ -3867,7 +3902,7 @@ async function runSubagent(
3867
3902
  let flatIndex = 0;
3868
3903
  let stepCursor = 0;
3869
3904
  while (true) {
3870
- if (interrupted || timedOut || stopped || turnBudgetExceeded) break;
3905
+ if (interrupted || timedOut || stopped) break;
3871
3906
  consumePendingAppendRequests();
3872
3907
  if (stepCursor >= steps.length) break;
3873
3908
  refreshUsageBudget();
@@ -4012,10 +4047,12 @@ async function runSubagent(
4012
4047
  : step.parallel.outputPath;
4013
4048
  const taskText = task.task ?? step.parallel.task;
4014
4049
  const materializedTask = step.parallel.namespaceOutputPath ? injectSingleOutputInstruction(taskText, outputPath, step.parallel) : taskText;
4050
+ const sessionName = deriveChildSessionName({ agent: step.parallel.agent, task: taskText, label: task.label ?? step.parallel.label });
4015
4051
  return omitUndefinedProperties({
4016
4052
  ...step.parallel,
4017
4053
  runFanoutPath: `chain[${stepIndex}].expand[${itemIndex}]`,
4018
4054
  task: materializedTask,
4055
+ ...(sessionName ? { sessionName } : {}),
4019
4056
  effectiveAcceptance: resolveEffectiveAcceptance(omitUndefinedProperties({
4020
4057
  explicit: step.parallel.acceptanceInput,
4021
4058
  agentName: step.parallel.agent,
@@ -4046,6 +4083,7 @@ async function runSubagent(
4046
4083
  const transcriptPath = resolveAsyncStepTranscriptPath(omitUndefinedProperties({ artifactsDir, artifactConfig, runId: id, agent: task.agent, flatIndex: groupStartFlatIndex + itemIndex, flatStepCount: dynamicFlatStepCount }));
4047
4084
  return omitUndefinedProperties({
4048
4085
  agent: task.agent,
4086
+ ...(task.sessionName ? { sessionName: task.sessionName } : {}),
4049
4087
  ...(statusStepDescription(task.task) ? { description: statusStepDescription(task.task) } : {}),
4050
4088
  ...(task.context ? { context: task.context } : {}),
4051
4089
  ...(task.phase ?? step.phase ? { phase: task.phase ?? step.phase } : {}),
@@ -4059,6 +4097,7 @@ async function runSubagent(
4059
4097
  ...(transcriptPath ? { transcriptPath } : {}),
4060
4098
  ...(task.skills ? { skills: task.skills } : {}),
4061
4099
  ...(task.model ? { model: task.model } : {}),
4100
+ ...(task.contextLimit !== undefined ? { contextLimit: task.contextLimit } : {}),
4062
4101
  ...(task.thinking ? { thinking: task.thinking } : {}),
4063
4102
  ...(task.thinkingCeiling ? { thinkingCeiling: task.thinkingCeiling } : {}),
4064
4103
  ...(task.modelCandidates && task.modelCandidates.length > 0 ? { attemptedModels: task.modelCandidates } : task.model ? { attemptedModels: [task.model] } : {}),
@@ -4128,12 +4167,12 @@ async function runSubagent(
4128
4167
  usageBudgetExceeded = true;
4129
4168
  writeStatusPayload();
4130
4169
  appendJsonl(eventsPath, JSON.stringify({ type: "subagent.step.failed", ts: skippedAt, runId: id, stepIndex: fi, agent: task.agent, exitCode: 1, durationMs: 0 }));
4131
- return omitUndefinedProperties({ agent: task.agent, context: task.context, output: message, error: message, exitCode: 1 as number | null, skipped: true });
4170
+ return omitUndefinedProperties({ agent: task.agent, ...(task.sessionName ? { sessionName: task.sessionName } : {}), context: task.context, output: message, error: message, exitCode: 1 as number | null, skipped: true });
4132
4171
  }
4133
- if (timedOut) return timedOutStepResult(task.agent, task.context);
4134
- if (stopped) return stoppedStepResult(task.agent, task.context);
4172
+ if (timedOut) return timedOutStepResult(task.agent, task.context, task.sessionName);
4173
+ if (stopped) return stoppedStepResult(task.agent, task.context, task.sessionName);
4135
4174
  if (childStopRequests.has(fi)) return childStopResult(fi, task.agent, task.context);
4136
- if (interrupted) return pausedStepResult(task.agent, task.context);
4175
+ if (interrupted) return pausedStepResult(task.agent, task.context, task.sessionName);
4137
4176
  if (aborted && failFast) {
4138
4177
  const skippedAt = Date.now();
4139
4178
  requiredStatusStep(statusPayload, fi).status = "failed";
@@ -4144,7 +4183,7 @@ async function runSubagent(
4144
4183
  requiredStatusStep(statusPayload, fi).exitCode = -1;
4145
4184
  statusPayload.lastUpdate = skippedAt;
4146
4185
  writeStatusPayload();
4147
- return omitUndefinedProperties({ agent: task.agent, context: task.context, output: "(skipped — fail-fast)", exitCode: -1 as number | null, skipped: true });
4186
+ return omitUndefinedProperties({ agent: task.agent, ...(task.sessionName ? { sessionName: task.sessionName } : {}), context: task.context, output: "(skipped — fail-fast)", exitCode: -1 as number | null, skipped: true });
4148
4187
  }
4149
4188
  const taskStartTime = Date.now();
4150
4189
  statusPayload.currentStep = fi;
@@ -4180,20 +4219,19 @@ async function runSubagent(
4180
4219
  registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
4181
4220
  registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
4182
4221
  registerStop: (stop) => registerStepStop(fi, stop),
4183
- registerTurnBudgetAbort: (abort) => registerStepTurnBudgetAbort(fi, abort),
4184
4222
  timeoutSignal: timeoutAbortController.signal,
4185
4223
  stopSignal: stopAbortController.signal,
4186
4224
  trackedMutationEvidenceForCompletionGuard: false,
4187
4225
  timeoutMessage,
4188
4226
  stopMessage,
4189
- turnBudget: config.turnBudget,
4190
4227
  toolTimeoutMs: task.toolTimeoutMs ?? config.toolTimeoutMs,
4191
- onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking),
4228
+ onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking, attempt.contextLimit),
4192
4229
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
4193
4230
  onWriterProcess,
4194
4231
  onExternalProcess: (process) => updateExternalProcess(fi, process),
4195
4232
  onExternalJob: (externalJob) => updateExternalJob(fi, externalJob),
4196
4233
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(fi),
4234
+ usageBudgetExhausted: () => refreshUsageBudget()?.exhausted === true,
4197
4235
  orcaProgressTab,
4198
4236
  }), config.deadlineAt);
4199
4237
  const taskEndTime = Date.now();
@@ -4205,16 +4243,11 @@ async function runSubagent(
4205
4243
  requiredStatusStep(statusPayload, fi).exitCode = stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
4206
4244
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "timedOut", timedOut || singleResult.timedOut ? true : undefined);
4207
4245
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "stopped", stopped || childStopped ? true : undefined);
4208
- setOptionalProperty(requiredStatusStep(statusPayload, fi), "turnBudget", singleResult.turnBudget);
4209
- setOptionalProperty(requiredStatusStep(statusPayload, fi), "turnBudgetExceeded", singleResult.turnBudgetExceeded);
4210
- setOptionalProperty(requiredStatusStep(statusPayload, fi), "wrapUpRequested", singleResult.wrapUpRequested);
4211
4246
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "toolBudget", singleResult.toolBudget);
4212
4247
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "toolBudgetBlocked", singleResult.toolBudgetBlocked);
4213
4248
  if (singleResult.toolBudget) statusPayload.toolBudget = singleResult.toolBudget;
4214
4249
  if (singleResult.toolBudgetBlocked) statusPayload.toolBudgetBlocked = true;
4215
- if (singleResult.turnBudget) statusPayload.turnBudget = singleResult.turnBudget;
4216
- if (singleResult.turnBudgetExceeded) statusPayload.turnBudgetExceeded = true;
4217
- if (singleResult.wrapUpRequested) statusPayload.wrapUpRequested = true;
4250
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "sessionName", singleResult.sessionName);
4218
4251
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "model", singleResult.model);
4219
4252
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "thinking", resolveEffectiveThinking(singleResult.model, requiredStatusStep(statusPayload, fi).thinking));
4220
4253
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "attemptedModels", singleResult.attemptedModels);
@@ -4266,6 +4299,7 @@ async function runSubagent(
4266
4299
  for (const pr of parallelResults) {
4267
4300
  results.push(omitUndefinedProperties({
4268
4301
  agent: pr.agent,
4302
+ ...(pr.sessionName ? { sessionName: pr.sessionName } : {}),
4269
4303
  context: pr.context,
4270
4304
  agentContract: pr.agentContract,
4271
4305
  launchContractDigest: pr.launchContractDigest,
@@ -4281,9 +4315,6 @@ async function runSubagent(
4281
4315
  interrupted: pr.interrupted,
4282
4316
  timedOut: pr.timedOut,
4283
4317
  stopped: pr.stopped,
4284
- turnBudget: pr.turnBudget,
4285
- turnBudgetExceeded: pr.turnBudgetExceeded,
4286
- wrapUpRequested: pr.wrapUpRequested,
4287
4318
  toolBudget: pr.toolBudget,
4288
4319
  toolBudgetBlocked: pr.toolBudgetBlocked,
4289
4320
  sessionFile: pr.sessionFile,
@@ -4293,6 +4324,7 @@ async function runSubagent(
4293
4324
  modelAttempts: pr.modelAttempts,
4294
4325
  contextOverflow: pr.contextOverflow,
4295
4326
  totalCost: pr.totalCost,
4327
+ usage: pr.usage,
4296
4328
  artifactPaths: pr.artifactPaths,
4297
4329
  transcriptPath: pr.transcriptPath,
4298
4330
  transcriptError: pr.transcriptError,
@@ -4436,6 +4468,23 @@ async function runSubagent(
4436
4468
  ? omitUndefinedProperties({ hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs })
4437
4469
  : undefined,
4438
4470
  baseDir: config.worktreeBaseDir,
4471
+ beforeCreate: (plannedSetup) => {
4472
+ for (const worktree of plannedSetup.worktrees) setStatusWorktreeReference(requiredStatusStep(statusPayload, groupStartFlatIndex + worktree.index), worktree);
4473
+ const pendingHandoff = writePendingParallelHandoff({
4474
+ manifestPath: parallelHandoffPath(asyncDir),
4475
+ runId: id,
4476
+ mode: (config.resultMode ?? statusPayload.mode) === "parallel" ? "parallel" : "chain",
4477
+ source: "async",
4478
+ cwd,
4479
+ stepIndex,
4480
+ flatStartIndex: groupStartFlatIndex,
4481
+ setup: plannedSetup,
4482
+ laneBindings: handoffWorkflowKey || config.lane ? [{ index: groupStartFlatIndex, taskIndex: 0, ...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}), ...(handoffChildRunId ? { runId: handoffChildRunId } : {}), ...(config.lane ? { lane: config.lane } : {}) }] : undefined,
4483
+ });
4484
+ statusPayload.parallelHandoff = pendingHandoff;
4485
+ statusPayload.lastUpdate = Date.now();
4486
+ writeStatusPayload();
4487
+ },
4439
4488
  }));
4440
4489
  } catch (error) {
4441
4490
  const setupError = error instanceof Error ? error.message : String(error);
@@ -4460,18 +4509,6 @@ async function runSubagent(
4460
4509
  }
4461
4510
 
4462
4511
  try {
4463
- if (worktreeSetup) {
4464
- writePendingParallelHandoff({
4465
- manifestPath: parallelHandoffPath(asyncDir),
4466
- runId: id,
4467
- mode: (config.resultMode ?? statusPayload.mode) === "parallel" ? "parallel" : "chain",
4468
- source: "async",
4469
- cwd,
4470
- stepIndex,
4471
- flatStartIndex: groupStartFlatIndex,
4472
- setup: worktreeSetup,
4473
- });
4474
- }
4475
4512
  if (group.worktree) ensureParallelProgressFile(cwd, group);
4476
4513
  const groupStartTime = Date.now();
4477
4514
  markParallelGroupRunning({
@@ -4508,12 +4545,12 @@ async function runSubagent(
4508
4545
  appendJsonl(eventsPath, JSON.stringify({
4509
4546
  type: "subagent.step.failed", ts: skippedAt, runId: id, stepIndex: fi, agent: task.agent, exitCode: 1, durationMs: 0,
4510
4547
  }));
4511
- return omitUndefinedProperties({ agent: task.agent, context: task.context, output: message, error: message, exitCode: 1 as number | null, skipped: true });
4548
+ return omitUndefinedProperties({ agent: task.agent, ...(task.sessionName ? { sessionName: task.sessionName } : {}), context: task.context, output: message, error: message, exitCode: 1 as number | null, skipped: true });
4512
4549
  }
4513
- if (timedOut) return timedOutStepResult(task.agent, task.context);
4514
- if (stopped) return stoppedStepResult(task.agent, task.context);
4550
+ if (timedOut) return timedOutStepResult(task.agent, task.context, task.sessionName);
4551
+ if (stopped) return stoppedStepResult(task.agent, task.context, task.sessionName);
4515
4552
  if (childStopRequests.has(fi)) return childStopResult(fi, task.agent, task.context);
4516
- if (interrupted) return pausedStepResult(task.agent, task.context);
4553
+ if (interrupted) return pausedStepResult(task.agent, task.context, task.sessionName);
4517
4554
  if (aborted && failFast) {
4518
4555
  const skippedAt = Date.now();
4519
4556
  requiredStatusStep(statusPayload, fi).status = "failed";
@@ -4528,7 +4565,7 @@ async function runSubagent(
4528
4565
  appendJsonl(eventsPath, JSON.stringify({
4529
4566
  type: "subagent.step.failed", ts: skippedAt, runId: id, stepIndex: fi, agent: task.agent, exitCode: -1, durationMs: 0,
4530
4567
  }));
4531
- return omitUndefinedProperties({ agent: task.agent, context: task.context, output: "(skipped — fail-fast)", exitCode: -1 as number | null, skipped: true });
4568
+ return omitUndefinedProperties({ agent: task.agent, ...(task.sessionName ? { sessionName: task.sessionName } : {}), context: task.context, output: "(skipped — fail-fast)", exitCode: -1 as number | null, skipped: true });
4532
4569
  }
4533
4570
 
4534
4571
  const taskStartTime = Date.now();
@@ -4576,20 +4613,19 @@ async function runSubagent(
4576
4613
  registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
4577
4614
  registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
4578
4615
  registerStop: (stop) => registerStepStop(fi, stop),
4579
- registerTurnBudgetAbort: (abort) => registerStepTurnBudgetAbort(fi, abort),
4580
4616
  timeoutSignal: timeoutAbortController.signal,
4581
4617
  stopSignal: stopAbortController.signal,
4582
4618
  trackedMutationEvidenceForCompletionGuard: Boolean(worktreeSetup),
4583
4619
  timeoutMessage,
4584
4620
  stopMessage,
4585
- turnBudget: config.turnBudget,
4586
4621
  toolTimeoutMs: taskForRun.toolTimeoutMs ?? config.toolTimeoutMs,
4587
- onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking),
4622
+ onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking, attempt.contextLimit),
4588
4623
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
4589
4624
  onWriterProcess,
4590
4625
  onExternalProcess: (process) => updateExternalProcess(fi, process),
4591
4626
  onExternalJob: (externalJob) => updateExternalJob(fi, externalJob),
4592
4627
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(fi),
4628
+ usageBudgetExhausted: () => refreshUsageBudget()?.exhausted === true,
4593
4629
  orcaProgressTab,
4594
4630
  }), config.deadlineAt);
4595
4631
  if (task.sessionFile) {
@@ -4607,16 +4643,11 @@ async function runSubagent(
4607
4643
  requiredStatusStep(statusPayload, fi).exitCode = stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
4608
4644
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "timedOut", timedOut || singleResult.timedOut ? true : undefined);
4609
4645
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "stopped", stopped || childStopped ? true : undefined);
4610
- setOptionalProperty(requiredStatusStep(statusPayload, fi), "turnBudget", singleResult.turnBudget);
4611
- setOptionalProperty(requiredStatusStep(statusPayload, fi), "turnBudgetExceeded", singleResult.turnBudgetExceeded);
4612
- setOptionalProperty(requiredStatusStep(statusPayload, fi), "wrapUpRequested", singleResult.wrapUpRequested);
4613
4646
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "toolBudget", singleResult.toolBudget);
4614
4647
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "toolBudgetBlocked", singleResult.toolBudgetBlocked);
4615
4648
  if (singleResult.toolBudget) statusPayload.toolBudget = singleResult.toolBudget;
4616
4649
  if (singleResult.toolBudgetBlocked) statusPayload.toolBudgetBlocked = true;
4617
- if (singleResult.turnBudget) statusPayload.turnBudget = singleResult.turnBudget;
4618
- if (singleResult.turnBudgetExceeded) statusPayload.turnBudgetExceeded = true;
4619
- if (singleResult.wrapUpRequested) statusPayload.wrapUpRequested = true;
4650
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "sessionName", singleResult.sessionName);
4620
4651
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "model", singleResult.model);
4621
4652
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "thinking", resolveEffectiveThinking(singleResult.model, requiredStatusStep(statusPayload, fi).thinking));
4622
4653
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "attemptedModels", singleResult.attemptedModels);
@@ -4725,9 +4756,6 @@ async function runSubagent(
4725
4756
  interrupted: pr.interrupted,
4726
4757
  timedOut: pr.timedOut,
4727
4758
  stopped: pr.stopped,
4728
- turnBudget: pr.turnBudget,
4729
- turnBudgetExceeded: pr.turnBudgetExceeded,
4730
- wrapUpRequested: pr.wrapUpRequested,
4731
4759
  toolBudget: pr.toolBudget,
4732
4760
  toolBudgetBlocked: pr.toolBudgetBlocked,
4733
4761
  sessionFile: pr.sessionFile,
@@ -4737,6 +4765,7 @@ async function runSubagent(
4737
4765
  modelAttempts: pr.modelAttempts,
4738
4766
  contextOverflow: pr.contextOverflow,
4739
4767
  totalCost: pr.totalCost,
4768
+ usage: pr.usage,
4740
4769
  artifactPaths: pr.artifactPaths,
4741
4770
  transcriptPath: pr.transcriptPath,
4742
4771
  transcriptError: pr.transcriptError,
@@ -4790,12 +4819,14 @@ async function runSubagent(
4790
4819
  diffs: captured.diffs,
4791
4820
  results: parallelResults.map((result) => ({
4792
4821
  agent: result.agent,
4822
+ ...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}),
4823
+ ...(handoffChildRunId ? { runId: handoffChildRunId } : {}),
4824
+ ...(config.lane ? { lane: config.lane } : {}),
4793
4825
  status: result.stopped || (result.exitCode !== 0 && isUnexplainedProcessSignal(omitUndefinedProperties({
4794
4826
  processSignal: result.processSignal,
4795
4827
  interrupted: result.interrupted,
4796
4828
  timedOut: result.timedOut,
4797
4829
  stopped: result.stopped,
4798
- turnBudgetExceeded: result.turnBudgetExceeded,
4799
4830
  }))) ? "stopped" as const : result.interrupted ? "paused" as const : result.exitCode === 0 ? "completed" as const : "failed" as const,
4800
4831
  summary: result.output || result.error || "(no output)",
4801
4832
  ...(result.artifactPaths?.outputPath ? { outputPath: result.artifactPaths.outputPath } : {}),
@@ -4841,12 +4872,12 @@ async function runSubagent(
4841
4872
  } else {
4842
4873
  const seqStep = step as SubagentStep;
4843
4874
  if (timedOut) {
4844
- results.push(timedOutStepResult(seqStep.agent, seqStep.context));
4875
+ results.push(timedOutStepResult(seqStep.agent, seqStep.context, seqStep.sessionName));
4845
4876
  flatIndex++;
4846
4877
  continue;
4847
4878
  }
4848
4879
  if (stopped) {
4849
- results.push(stoppedStepResult(seqStep.agent, seqStep.context));
4880
+ results.push(stoppedStepResult(seqStep.agent, seqStep.context, seqStep.sessionName));
4850
4881
  flatIndex++;
4851
4882
  continue;
4852
4883
  }
@@ -4856,7 +4887,7 @@ async function runSubagent(
4856
4887
  continue;
4857
4888
  }
4858
4889
  if (interrupted) {
4859
- results.push(pausedStepResult(seqStep.agent, seqStep.context));
4890
+ results.push(pausedStepResult(seqStep.agent, seqStep.context, seqStep.sessionName));
4860
4891
  flatIndex++;
4861
4892
  continue;
4862
4893
  }
@@ -4869,17 +4900,27 @@ async function runSubagent(
4869
4900
  ? omitUndefinedProperties({ hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs })
4870
4901
  : undefined,
4871
4902
  baseDir: config.worktreeBaseDir,
4903
+ beforeCreate: (plannedSetup) => {
4904
+ const worktree = plannedSetup.worktrees[0];
4905
+ if (worktree) {
4906
+ setStatusWorktreeReference(requiredStatusStep(statusPayload, flatIndex), worktree);
4907
+ }
4908
+ const pendingHandoff = writePendingParallelHandoff({
4909
+ manifestPath: parallelHandoffPath(asyncDir),
4910
+ runId: id,
4911
+ mode: "single",
4912
+ source: "async",
4913
+ cwd,
4914
+ stepIndex,
4915
+ flatStartIndex: flatIndex,
4916
+ setup: plannedSetup,
4917
+ laneBindings: handoffWorkflowKey || config.lane ? [{ index: flatIndex, taskIndex: 0, ...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}), ...(handoffChildRunId ? { runId: handoffChildRunId } : {}), ...(config.lane ? { lane: config.lane } : {}) }] : undefined,
4918
+ });
4919
+ statusPayload.parallelHandoff = pendingHandoff;
4920
+ statusPayload.lastUpdate = Date.now();
4921
+ writeStatusPayload();
4922
+ },
4872
4923
  }));
4873
- writePendingParallelHandoff({
4874
- manifestPath: parallelHandoffPath(asyncDir),
4875
- runId: id,
4876
- mode: "single",
4877
- source: "async",
4878
- cwd,
4879
- stepIndex,
4880
- flatStartIndex: flatIndex,
4881
- setup: singleWorktreeSetup,
4882
- });
4883
4924
  } catch (error) {
4884
4925
  if (singleWorktreeSetup) cleanupWorktrees(singleWorktreeSetup);
4885
4926
  throw error;
@@ -4932,19 +4973,18 @@ async function runSubagent(
4932
4973
  registerInterrupt: (interrupt) => registerStepInterrupt(flatIndex, interrupt),
4933
4974
  registerTimeout: (interrupt) => registerStepTimeout(flatIndex, interrupt),
4934
4975
  registerStop: (stop) => registerStepStop(flatIndex, stop),
4935
- registerTurnBudgetAbort: (abort) => registerStepTurnBudgetAbort(flatIndex, abort),
4936
4976
  timeoutSignal: timeoutAbortController.signal,
4937
4977
  stopSignal: stopAbortController.signal,
4938
4978
  timeoutMessage,
4939
4979
  stopMessage,
4940
- turnBudget: config.turnBudget,
4941
4980
  toolTimeoutMs: seqStep.toolTimeoutMs ?? config.toolTimeoutMs,
4942
- onAttemptStart: (attempt) => updateStepModel(flatIndex, attempt.model, attempt.thinking),
4981
+ onAttemptStart: (attempt) => updateStepModel(flatIndex, attempt.model, attempt.thinking, attempt.contextLimit),
4943
4982
  onChildEvent: (event) => updateStepFromChildEvent(flatIndex, event),
4944
4983
  onWriterProcess,
4945
4984
  onExternalProcess: (process) => updateExternalProcess(flatIndex, process),
4946
4985
  onExternalJob: (externalJob) => updateExternalJob(flatIndex, externalJob),
4947
4986
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(flatIndex),
4987
+ usageBudgetExhausted: () => refreshUsageBudget()?.exhausted === true,
4948
4988
  orcaProgressTab,
4949
4989
  }), config.deadlineAt);
4950
4990
  } catch (error) {
@@ -4959,6 +4999,7 @@ async function runSubagent(
4959
4999
  const childStopped = singleResult.stopped === true;
4960
5000
  results.push(omitUndefinedProperties({
4961
5001
  agent: singleResult.agent,
5002
+ ...(singleResult.sessionName ? { sessionName: singleResult.sessionName } : {}),
4962
5003
  context: singleResult.context,
4963
5004
  agentContract: singleResult.agentContract,
4964
5005
  launchContractDigest: singleResult.launchContractDigest,
@@ -4977,6 +5018,7 @@ async function runSubagent(
4977
5018
  modelAttempts: singleResult.modelAttempts,
4978
5019
  contextOverflow: singleResult.contextOverflow,
4979
5020
  totalCost: singleResult.totalCost,
5021
+ usage: singleResult.usage,
4980
5022
  artifactPaths: singleResult.artifactPaths,
4981
5023
  transcriptPath: singleResult.transcriptPath,
4982
5024
  transcriptError: singleResult.transcriptError,
@@ -4994,9 +5036,6 @@ async function runSubagent(
4994
5036
  interrupted: singleResult.interrupted,
4995
5037
  timedOut: timedOut || singleResult.timedOut ? true : undefined,
4996
5038
  stopped: stopped || childStopped ? true : undefined,
4997
- turnBudget: singleResult.turnBudget,
4998
- turnBudgetExceeded: singleResult.turnBudgetExceeded,
4999
- wrapUpRequested: singleResult.wrapUpRequested,
5000
5039
  toolBudget: singleResult.toolBudget,
5001
5040
  toolBudgetBlocked: singleResult.toolBudgetBlocked,
5002
5041
  runner: singleResult.runner,
@@ -5051,16 +5090,11 @@ async function runSubagent(
5051
5090
  requiredStatusStep(statusPayload, flatIndex).exitCode = stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
5052
5091
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "timedOut", timedOut || singleResult.timedOut ? true : undefined);
5053
5092
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "stopped", stopped || childStopped ? true : undefined);
5054
- setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "turnBudget", singleResult.turnBudget);
5055
- setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "turnBudgetExceeded", singleResult.turnBudgetExceeded);
5056
- setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "wrapUpRequested", singleResult.wrapUpRequested);
5057
5093
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "toolBudget", singleResult.toolBudget);
5058
5094
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "toolBudgetBlocked", singleResult.toolBudgetBlocked);
5059
5095
  if (singleResult.toolBudget) statusPayload.toolBudget = singleResult.toolBudget;
5060
5096
  if (singleResult.toolBudgetBlocked) statusPayload.toolBudgetBlocked = true;
5061
- if (singleResult.turnBudget) statusPayload.turnBudget = singleResult.turnBudget;
5062
- if (singleResult.turnBudgetExceeded) statusPayload.turnBudgetExceeded = true;
5063
- if (singleResult.wrapUpRequested) statusPayload.wrapUpRequested = true;
5097
+ setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "sessionName", singleResult.sessionName);
5064
5098
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "model", singleResult.model);
5065
5099
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "thinking", resolveEffectiveThinking(singleResult.model, requiredStatusStep(statusPayload, flatIndex).thinking));
5066
5100
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "attemptedModels", singleResult.attemptedModels);
@@ -5121,6 +5155,9 @@ async function runSubagent(
5121
5155
  diffs,
5122
5156
  results: [{
5123
5157
  agent: singleResult.agent,
5158
+ ...(handoffWorkflowKey ? { workflowKey: handoffWorkflowKey } : {}),
5159
+ ...(handoffChildRunId ? { runId: handoffChildRunId } : {}),
5160
+ ...(config.lane ? { lane: config.lane } : {}),
5124
5161
  status: singleResult.stopped ? "stopped" as const : singleResult.interrupted ? "paused" as const : singleResult.exitCode === 0 ? "completed" as const : "failed" as const,
5125
5162
  summary: singleResult.output || singleResult.error || "(no output)",
5126
5163
  ...(singleResult.artifactPaths?.outputPath ? { outputPath: singleResult.artifactPaths.outputPath } : {}),
@@ -5241,14 +5278,14 @@ async function runSubagent(
5241
5278
  if (!timedOut && !stopped && !interrupted && config.timeoutMs !== undefined && results.some((result) => result.timedOut === true && result.error === timeoutMessage)) {
5242
5279
  timedOut = true;
5243
5280
  }
5244
- const signalTerminated = !stopped && !timedOut && !turnBudgetExceeded && !interrupted && results.some((result) => result.exitCode !== 0 && isUnexplainedProcessSignal(omitUndefinedProperties({
5281
+ const signalTerminated = !stopped && !timedOut && !interrupted && results.some((result) => result.exitCode !== 0 && isUnexplainedProcessSignal(omitUndefinedProperties({
5245
5282
  processSignal: result.processSignal,
5246
5283
  interrupted: result.interrupted,
5247
5284
  timedOut: result.timedOut,
5248
5285
  stopped: result.stopped,
5249
- turnBudgetExceeded: result.turnBudgetExceeded,
5250
5286
  })));
5251
- statusPayload.state = stopped || signalTerminated ? "stopped" : timedOut || turnBudgetExceeded || usageBudgetExceeded || statusPayload.error ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed";
5287
+ const partialWithEvidence = !stopped && !signalTerminated && !timedOut && !usageBudgetExceeded && !interrupted && results.some(partialEvidenceResult) && !results.some(concreteFailureResult);
5288
+ statusPayload.state = stopped || signalTerminated ? "stopped" : timedOut || usageBudgetExceeded ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : partialWithEvidence ? "partial" : "failed";
5252
5289
  closeSteerInbox(asyncDir, statusPayload.state, (filePath, payload) => runPersistence.write(filePath, payload));
5253
5290
  disposeControlInbox();
5254
5291
  for (const request of consumeSteerRequests(asyncDir)) deliverSteerRequest(request);
@@ -5278,13 +5315,17 @@ async function runSubagent(
5278
5315
  statusPayload.timedOut = true;
5279
5316
  statusPayload.error = timeoutMessage ?? "Subagent timed out.";
5280
5317
  }
5281
- if (turnBudgetExceeded && !statusPayload.error) {
5282
- const budget = statusPayload.turnBudget;
5283
- statusPayload.error = budget ? turnBudgetExceededMessage(budget, budget.turnCount) : "Subagent exceeded turn budget.";
5284
- }
5285
5318
  if (usageBudgetExceeded && statusPayload.usageBudget && !statusPayload.error) {
5286
5319
  statusPayload.error = usageBudgetExceededMessage(statusPayload.usageBudget);
5287
5320
  }
5321
+ if (partialWithEvidence) {
5322
+ const partialResult = results.find(partialEvidenceResult);
5323
+ statusPayload.activityState = "needs_attention";
5324
+ statusPayload.error = partialResult?.error ?? statusPayload.error;
5325
+ for (const step of statusPayload.steps) {
5326
+ if (step.status === "failed") step.activityState = "needs_attention";
5327
+ }
5328
+ }
5288
5329
  statusPayload.endedAt = runEndedAt;
5289
5330
  statusPayload.lastUpdate = runEndedAt;
5290
5331
  setOptionalProperty(statusPayload, "sessionFile", effectiveSessionFile);
@@ -5294,8 +5335,10 @@ async function runSubagent(
5294
5335
  setOptionalProperty(statusPayload, "shareUrl", shareUrl);
5295
5336
  setOptionalProperty(statusPayload, "gistUrl", gistUrl);
5296
5337
  setOptionalProperty(statusPayload, "shareError", shareError);
5297
- if (statusPayload.state === "failed" && !statusPayload.error) {
5298
- const failedStep = statusPayload.steps.find((s) => s.status === "failed");
5338
+ if ((statusPayload.state === "failed" || statusPayload.state === "partial") && !statusPayload.error) {
5339
+ const concreteFailure = results.find(concreteFailureResult);
5340
+ const failedStep = concreteFailure ? undefined : statusPayload.steps.find((s) => s.status === "failed");
5341
+ if (concreteFailure?.error) statusPayload.error = concreteFailure.error;
5299
5342
  if (failedStep?.agent) {
5300
5343
  statusPayload.error = `Step failed: ${failedStep.agent}`;
5301
5344
  }
@@ -5307,20 +5350,18 @@ async function runSubagent(
5307
5350
  id,
5308
5351
  agent: agentName,
5309
5352
  mode: resultMode,
5310
- success: !stopped && !timedOut && !turnBudgetExceeded && !usageBudgetExceeded && !interrupted && !signalTerminated && results.every((r) => r.success),
5311
- state: stopped || signalTerminated ? "stopped" : timedOut || turnBudgetExceeded || usageBudgetExceeded ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed",
5312
- summary: stopped ? stopMessage : signalTerminated ? (statusPayload.error ?? "Subagent process terminated by signal.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? (statusPayload.error ?? "Subagent exceeded turn budget.") : usageBudgetExceeded ? (statusPayload.error ?? "Usage budget exhausted.") : interrupted ? "Paused after interrupt. Waiting for explicit next action." : summary,
5353
+ success: statusPayload.state === "complete",
5354
+ state: statusPayload.state,
5355
+ summary: stopped ? stopMessage : signalTerminated ? (statusPayload.error ?? "Subagent process terminated by signal.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : usageBudgetExceeded ? (statusPayload.error ?? "Usage budget exhausted.") : interrupted ? "Paused after interrupt. Waiting for explicit next action." : statusPayload.state === "partial" ? (statusPayload.error ?? summary) : summary,
5313
5356
  ...(config.timeoutMs !== undefined ? { timeoutMs: config.timeoutMs } : {}),
5314
5357
  ...(config.deadlineAt !== undefined ? { deadlineAt: config.deadlineAt } : {}),
5315
- ...(statusPayload.turnBudget ? { turnBudget: statusPayload.turnBudget } : {}),
5316
- ...(statusPayload.turnBudgetExceeded ? { turnBudgetExceeded: true } : {}),
5317
- ...(statusPayload.wrapUpRequested ? { wrapUpRequested: true } : {}),
5318
5358
  ...(statusPayload.toolBudget ? { toolBudget: statusPayload.toolBudget } : {}),
5319
5359
  ...(statusPayload.toolBudgetBlocked ? { toolBudgetBlocked: true } : {}),
5320
5360
  ...(statusPayload.usageBudget ? { usageBudget: statusPayload.usageBudget } : {}),
5321
- ...(stopped ? { stopped: true, error: stopMessage } : timedOut ? { timedOut: true, error: timeoutMessage ?? "Subagent timed out." } : turnBudgetExceeded ? { error: statusPayload.error ?? "Subagent exceeded turn budget." } : usageBudgetExceeded ? { error: statusPayload.error ?? "Usage budget exhausted." } : {}),
5361
+ ...(stopped ? { stopped: true, error: stopMessage } : timedOut ? { timedOut: true, error: timeoutMessage ?? "Subagent timed out." } : usageBudgetExceeded ? { error: statusPayload.error ?? "Usage budget exhausted." } : {}),
5322
5362
  results: results.map((r) => omitUndefinedProperties({
5323
5363
  agent: r.agent,
5364
+ ...(r.sessionName ? { sessionName: r.sessionName } : {}),
5324
5365
  context: r.context,
5325
5366
  output: r.output,
5326
5367
  outputState: r.outputState,
@@ -5332,9 +5373,6 @@ async function runSubagent(
5332
5373
  timedOut: r.timedOut || undefined,
5333
5374
  stopped: r.stopped || undefined,
5334
5375
  processSignal: r.processSignal || undefined,
5335
- turnBudget: r.turnBudget,
5336
- turnBudgetExceeded: r.turnBudgetExceeded || undefined,
5337
- wrapUpRequested: r.wrapUpRequested || undefined,
5338
5376
  toolBudget: r.toolBudget,
5339
5377
  toolBudgetBlocked: r.toolBudgetBlocked || undefined,
5340
5378
  sessionFile: r.sessionFile,
@@ -5344,6 +5382,7 @@ async function runSubagent(
5344
5382
  modelAttempts: r.modelAttempts,
5345
5383
  contextOverflow: r.contextOverflow,
5346
5384
  totalCost: r.totalCost,
5385
+ usage: r.usage,
5347
5386
  artifactPaths: r.artifactPaths,
5348
5387
  outputSaveError: r.outputSaveError,
5349
5388
  metadataSaveError: r.metadataSaveError,
@@ -5376,7 +5415,7 @@ async function runSubagent(
5376
5415
  capabilityAudit: statusPayload.capabilityAudit,
5377
5416
  ...(config.parentWorkflowRunId ? { parentWorkflowRunId: config.parentWorkflowRunId } : {}),
5378
5417
  ...(config.workflowKey ? { workflowKey: config.workflowKey } : {}),
5379
- exitCode: stopped || timedOut || turnBudgetExceeded || usageBudgetExceeded ? 1 : interrupted || results.every((r) => r.success) ? 0 : 1,
5418
+ exitCode: statusPayload.state === "complete" || statusPayload.state === "paused" ? 0 : 1,
5380
5419
  timestamp: runEndedAt,
5381
5420
  durationMs: runEndedAt - overallStartTime,
5382
5421
  totalTokens: statusPayload.totalTokens,