pi-subagents 0.49.0 → 0.51.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 (117) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/agents/gpt-pro.md +17 -0
  3. package/agents/oracle.md +7 -5
  4. package/agents/researcher.md +1 -1
  5. package/agents/reviewer.md +2 -2
  6. package/agents/scout.md +1 -1
  7. package/agents/worker.md +1 -1
  8. package/async-retention-discovery-worker.mjs +180 -0
  9. package/docs/agents.md +37 -2
  10. package/docs/configuration.md +76 -14
  11. package/docs/extension-api.md +78 -1
  12. package/docs/missions.md +1 -1
  13. package/docs/observability.md +20 -4
  14. package/docs/tool-reference.md +55 -39
  15. package/docs/workflows.md +171 -5
  16. package/package.json +4 -2
  17. package/skills/pi-subagents/SKILL.md +5 -4
  18. package/skills/pi-subagents/references/constraints-and-recipes.md +9 -6
  19. package/skills/pi-subagents/references/execution-controls.md +22 -18
  20. package/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  21. package/skills/pi-subagents/references/prompting-and-roles.md +33 -17
  22. package/src/agents/agent-management.ts +100 -345
  23. package/src/agents/agent-serializer.ts +2 -0
  24. package/src/agents/agents.ts +135 -25
  25. package/src/api/external-job-provider.ts +185 -0
  26. package/src/api/external-runs.ts +174 -84
  27. package/src/api/preflight.ts +42 -11
  28. package/src/api/shared-types.ts +2 -0
  29. package/src/extension/config.ts +36 -3
  30. package/src/extension/doctor.ts +3 -6
  31. package/src/extension/fanout-child.ts +2 -2
  32. package/src/extension/index.ts +210 -90
  33. package/src/extension/public-execution.ts +31 -2
  34. package/src/extension/rpc.ts +5 -1
  35. package/src/extension/schemas.ts +14 -36
  36. package/src/extension/tool-description.ts +37 -24
  37. package/src/inspectors/herdr/actions.ts +5 -9
  38. package/src/inspectors/herdr/inspector-runner.ts +2 -1
  39. package/src/inspectors/herdr/project-panes.ts +4 -8
  40. package/src/inspectors/herdr/shell-command.ts +16 -0
  41. package/src/intercom/intercom-bridge.ts +2 -3
  42. package/src/intercom/native-supervisor-channel.ts +49 -51
  43. package/src/missions/goal-driver.ts +3 -1
  44. package/src/missions/lifecycle.ts +6 -1
  45. package/src/missions/store.ts +4 -9
  46. package/src/profiles/profiles.ts +3 -1
  47. package/src/runs/background/active-run-index.ts +94 -1
  48. package/src/runs/background/async-execution.ts +98 -38
  49. package/src/runs/background/async-job-tracker.ts +21 -4
  50. package/src/runs/background/async-resume.ts +30 -17
  51. package/src/runs/background/async-retention.ts +888 -0
  52. package/src/runs/background/async-status-snapshot.ts +277 -0
  53. package/src/runs/background/async-status.ts +47 -56
  54. package/src/runs/background/chain-append.ts +3 -33
  55. package/src/runs/background/chain-root-attachment.ts +2 -2
  56. package/src/runs/background/completion-replay.ts +11 -1
  57. package/src/runs/background/control-channel.ts +14 -68
  58. package/src/runs/background/fleet-view.ts +3 -1
  59. package/src/runs/background/index-segment.ts +59 -0
  60. package/src/runs/background/notify.ts +3 -1
  61. package/src/runs/background/result-files.ts +505 -0
  62. package/src/runs/background/result-watcher.ts +250 -51
  63. package/src/runs/background/retained-children.ts +79 -20
  64. package/src/runs/background/run-id-query.ts +7 -0
  65. package/src/runs/background/run-id-resolver.ts +37 -29
  66. package/src/runs/background/run-status.ts +34 -20
  67. package/src/runs/background/scheduled-runs.ts +71 -27
  68. package/src/runs/background/stale-run-reconciler.ts +33 -16
  69. package/src/runs/background/steering.ts +11 -1
  70. package/src/runs/background/subagent-runner.ts +535 -161
  71. package/src/runs/background/subagent-wait.ts +9 -7
  72. package/src/runs/background/terminal-run-index.ts +129 -0
  73. package/src/runs/background/wait-completions.ts +22 -2
  74. package/src/runs/background/wait-subscriptions.ts +80 -1
  75. package/src/runs/foreground/async-dismiss-action.ts +2 -1
  76. package/src/runs/foreground/async-steering-action.ts +21 -14
  77. package/src/runs/foreground/execution.ts +221 -15
  78. package/src/runs/foreground/subagent-executor.ts +529 -1519
  79. package/src/runs/foreground/workflow-foreground-steering.ts +6 -5
  80. package/src/runs/shared/chain-outputs.ts +1 -3
  81. package/src/runs/shared/completion-guard.ts +96 -6
  82. package/src/runs/shared/external-cli-runner.ts +4 -0
  83. package/src/runs/shared/external-job-bridge.ts +450 -0
  84. package/src/runs/shared/external-job-runner.ts +286 -0
  85. package/src/runs/shared/mcp-direct-tool-allowlist.ts +14 -0
  86. package/src/runs/shared/model-fallback.ts +34 -3
  87. package/src/runs/shared/nested-events.ts +66 -62
  88. package/src/runs/shared/orca-progress-tabs.ts +437 -0
  89. package/src/runs/shared/parallel-handoff.ts +46 -4
  90. package/src/runs/shared/parallel-utils.ts +6 -15
  91. package/src/runs/shared/permissions.ts +5 -1
  92. package/src/runs/shared/pi-args.ts +8 -1
  93. package/src/runs/shared/subagent-control.ts +41 -4
  94. package/src/runs/shared/subagent-prompt-runtime.ts +13 -15
  95. package/src/runs/shared/subagent-startup-retry.ts +12 -0
  96. package/src/runs/shared/tool-timeout.ts +93 -0
  97. package/src/runs/shared/workflow-graph.ts +1 -23
  98. package/src/runs/shared/worktree.ts +12 -1
  99. package/src/shared/atomic-json.ts +22 -2
  100. package/src/shared/capacity-resilient-json.ts +102 -0
  101. package/src/shared/completion-owner.ts +14 -0
  102. package/src/shared/file-system-retry.ts +49 -1
  103. package/src/shared/fork-context.ts +42 -0
  104. package/src/shared/prompt-resources.ts +0 -40
  105. package/src/shared/settings.ts +3 -27
  106. package/src/shared/types.ts +88 -27
  107. package/src/shared/utils.ts +8 -0
  108. package/src/shared/watch-strategy.ts +10 -0
  109. package/src/slash/slash-commands.ts +45 -28
  110. package/src/slash/slash-live-state.ts +3 -0
  111. package/src/tui/fleet-status.ts +160 -45
  112. package/src/tui/fleet.ts +186 -28
  113. package/src/tui/render.ts +41 -10
  114. package/src/workflows/chat-progress.ts +7 -5
  115. package/src/workflows/scripted-workflow.ts +424 -125
  116. package/src/runs/foreground/chain-clarify.ts +0 -1354
  117. package/src/runs/foreground/chain-execution.ts +0 -1565
@@ -5,8 +5,11 @@ import * as path from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
6
  import type { Message } from "@earendil-works/pi-ai";
7
7
  import { writeAtomicJson } from "../../shared/atomic-json.ts";
8
+ import { writeAsyncResultFile, writePendingAsyncResultFile } from "./result-files.ts";
8
9
  import { createFileCoalescer } from "../../shared/file-coalescer.ts";
9
- import { isActiveAsyncState, updateActiveRunIndex } from "./active-run-index.ts";
10
+ import { createCapacityResilientJsonWriter } from "../../shared/capacity-resilient-json.ts";
11
+ import { isStorageCapacityError } from "../../shared/file-system-retry.ts";
12
+ import { updateActiveRunIndex } from "./active-run-index.ts";
10
13
  import { createChildTranscriptWriter, type ChildTranscriptWriter } from "../../shared/child-transcript.ts";
11
14
  import { closeSteerInbox, consumeInterruptRequest, consumeSteerRequests, deliverInterruptRequest, deliverStopRequest, deliverTimeoutRequest, enqueueStepSteer, steerAcksDir, steerCapabilityPath, stepSteerInboxDir, watchAsyncControlInbox, type SteerAck, type SteerCapability, type SteerRequest } from "./control-channel.ts";
12
15
  import { appendJsonl as appendRawJsonl, formatOutputArtifactContent, getArtifactPaths, writeArtifact, writeMetadata } from "../../shared/artifacts.ts";
@@ -16,6 +19,8 @@ import {
16
19
  type ActivityState,
17
20
  type ArtifactConfig,
18
21
  type ExternalCliRunnerStatus,
22
+ type ExternalJobRunnerStatus,
23
+ type ExternalJobStatus,
19
24
  type ExternalProcessStatus,
20
25
  type ArtifactPaths,
21
26
  type AsyncParallelGroupStatus,
@@ -57,11 +62,11 @@ import {
57
62
  claimControlNotification,
58
63
  formatControlIntercomMessage,
59
64
  formatControlNoticeMessage,
65
+ shouldEmitOpenToolAttention,
60
66
  } from "../shared/subagent-control.ts";
61
67
  import {
62
68
  type RunnerSubagentStep as SubagentStep,
63
69
  type RunnerStep,
64
- isCheckpointRunnerStep,
65
70
  isDynamicRunnerGroup,
66
71
  isParallelGroup,
67
72
  flattenSteps,
@@ -83,6 +88,7 @@ import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDi
83
88
  import { formatModelAttemptNote, isRetryableModelFailure } from "../shared/model-fallback.ts";
84
89
  import {
85
90
  SUBAGENT_STARTUP_RETRY_DELAYS_MS,
91
+ formatSubagentExtensionConflictError,
86
92
  formatSubagentStartupRetryExhaustedError,
87
93
  formatSubagentStartupRetryNote,
88
94
  isRetryableSubagentStartupFailure,
@@ -126,12 +132,15 @@ import { waitForImportedAsyncRoot } from "./chain-root-attachment.ts";
126
132
  import { appendRunnerStepsToStatus, consumeChainAppendRequests, countPendingChainAppendRequests, statusStepDescription } from "./chain-append.ts";
127
133
  import { appendTurnBudgetSystemPrompt, formatTurnBudgetOutput, initialTurnBudgetState, turnBudgetDecision, turnBudgetDeferredNote, turnBudgetDeferredState, turnBudgetExceededMessage, turnBudgetSoftNote, turnBudgetState } from "../shared/turn-budget.ts";
128
134
  import { initialToolBudgetState, toolBudgetState } from "../shared/tool-budget.ts";
135
+ import { effectiveToolTimeoutMs, formatToolTimeoutMessage, toolTimeoutCallKey } from "../shared/tool-timeout.ts";
129
136
  import { usageBudgetExceededMessage, usageBudgetState } from "../shared/usage-budget.ts";
130
137
  import { formatParallelHandoffError, formatParallelHandoffReference, parallelHandoffPath, writeParallelHandoffGroup, writePendingParallelHandoff } from "../shared/parallel-handoff.ts";
131
138
  import { resolveWatchdogConfig } from "../../watchdog/settings.ts";
132
139
  import { createBoundedByteTail, createBoundedLineReader, formatProtocolOutputLimit, MAX_CHILD_STDERR_BYTES, PI_AGGREGATE_EVENT_PROJECTOR, projectChildLifecycle, type ChildLifecycleAction, type ProtocolOutputLimit } from "../shared/child-protocol.ts";
133
140
  import { acquireSessionLease, type SessionLeaseRequest } from "../shared/session-lease.ts";
134
141
  import { buildExternalCliPrompt, runExternalCli } from "../shared/external-cli-runner.ts";
142
+ import { runExternalJob } from "../shared/external-job-runner.ts";
143
+ import { createOrcaProgressTab, type OrcaProgressTab } from "../shared/orca-progress-tabs.ts";
135
144
  import { decodeSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
136
145
  import {
137
146
  CHILD_WATCHDOG_CONFIG_ENV,
@@ -160,6 +169,7 @@ interface SubagentRunConfig {
160
169
  sessionDir?: string;
161
170
  asyncDir: string;
162
171
  sessionId?: string | null;
172
+ completionOwnerId?: string;
163
173
  piPackageRoot?: string;
164
174
  piArgv1?: string;
165
175
  worktreeSetupHook?: string;
@@ -176,6 +186,8 @@ interface SubagentRunConfig {
176
186
  nestedSelf?: { parentRunId: string; parentStepIndex?: number; depth: number; path?: Array<{ runId: string; stepIndex?: number; agent?: string }> };
177
187
  timeoutMs?: number;
178
188
  deadlineAt?: number;
189
+ /** Resolved configured hard per-tool-call timeout (ms); fast tools still have a default when undefined. */
190
+ toolTimeoutMs?: number;
179
191
  turnBudget?: ResolvedTurnBudget;
180
192
  toolBudget?: ResolvedToolBudget;
181
193
  usageBudget?: UsageBudgetConfig;
@@ -210,6 +222,7 @@ interface StepResult {
210
222
  savedOutputPath?: string;
211
223
  skipped?: boolean;
212
224
  interrupted?: boolean;
225
+ detached?: boolean;
213
226
  timedOut?: boolean;
214
227
  stopped?: boolean;
215
228
  processSignal?: string | null;
@@ -242,8 +255,9 @@ interface StepResult {
242
255
  watchdog?: import("../../shared/types.ts").ChildWatchdogProgress;
243
256
  writerProcesses?: PiWriterProcessInstanceExitV1[];
244
257
  writerAttemptCount?: number;
245
- runner?: ExternalCliRunnerStatus;
258
+ runner?: ExternalCliRunnerStatus | ExternalJobRunnerStatus;
246
259
  externalProcess?: ExternalProcessStatus;
260
+ externalJob?: ExternalJobStatus;
247
261
  }
248
262
 
249
263
  function persistStepArtifacts(input: {
@@ -541,6 +555,9 @@ function runPiStreaming(
541
555
  stopMessage?: string,
542
556
  registerTurnBudgetAbort?: (abort: ((message: string, state?: TurnBudgetState) => void) | undefined) => void,
543
557
  onWriterProcess?: (writer: { state: "none" | "spawning" } | { state: "running"; pid: number }) => void,
558
+ toolTimeoutMs?: number,
559
+ runDeadlineAt?: number,
560
+ orcaProgressTab?: OrcaProgressTab,
544
561
  ): Promise<RunPiStreamingResult> {
545
562
  return new Promise((resolve) => {
546
563
  const startedAt = Date.now();
@@ -597,6 +614,7 @@ function runPiStreaming(
597
614
  const writeOutputLine = (line: string) => {
598
615
  if (!line.trim()) return;
599
616
  outputStream.write(`${line}\n`);
617
+ orcaProgressTab?.append(`${line}\n`);
600
618
  };
601
619
 
602
620
  const writeOutputText = (text: string) => {
@@ -674,8 +692,14 @@ function runPiStreaming(
674
692
 
675
693
  onChildEvent?.(event);
676
694
 
695
+ if (event.type === "tool_execution_end") {
696
+ clearActiveToolTimeout(event);
697
+ return;
698
+ }
699
+
677
700
  if (event.type === "tool_execution_start" && event.toolName) {
678
701
  toolCount += 1;
702
+ armToolTimeout({ toolCallId: (event as { toolCallId?: unknown }).toolCallId, toolName: event.toolName });
679
703
  if (event.toolName === "structured_output") {
680
704
  structuredOutputToolInvoked = true;
681
705
  structuredOutputMessageStartIndex = messages.length;
@@ -771,6 +795,7 @@ function runPiStreaming(
771
795
  stderrTail.push(chunk);
772
796
  stderrReader.push(chunk);
773
797
  outputStream.write(chunk);
798
+ orcaProgressTab?.append(chunk.toString("utf-8"));
774
799
  });
775
800
  registerInterrupt?.(() => {
776
801
  if (settled || timedOut || stopped) return;
@@ -781,14 +806,60 @@ function runPiStreaming(
781
806
  if (!settled && !timedOut && !stopped) trySignalChild(child, "SIGTERM");
782
807
  }, 1000).unref?.();
783
808
  });
784
- registerTimeout?.(() => {
809
+ const terminateForTimeout = (message: string): void => {
785
810
  if (settled || timedOut || stopped) return;
786
811
  timedOut = true;
812
+ // runPiStreaming's terminal result derives the timeout error from this
813
+ // message, so retain the tool-specific reason through finalization.
814
+ timeoutMessage = message;
787
815
  interrupted = false;
788
- error = timeoutMessage ?? "Subagent timed out.";
816
+ error = message;
789
817
  if (processTreeController) void processTreeController.terminate();
790
818
  else trySignalChild(child, "SIGTERM");
791
- });
819
+ };
820
+ let toolTimeoutSequence = 0;
821
+ const activeToolTimeouts = new Map<string, { toolName: string; timer: ReturnType<typeof setTimeout> }>();
822
+ const activeToolTimeoutKeysByName = new Map<string, string[]>();
823
+ const removeToolTimeoutKey = (key: string): void => {
824
+ const active = activeToolTimeouts.get(key);
825
+ if (!active) return;
826
+ clearTimeout(active.timer);
827
+ activeToolTimeouts.delete(key);
828
+ const keys = activeToolTimeoutKeysByName.get(active.toolName)?.filter((candidate) => candidate !== key) ?? [];
829
+ if (keys.length > 0) activeToolTimeoutKeysByName.set(active.toolName, keys);
830
+ else activeToolTimeoutKeysByName.delete(active.toolName);
831
+ };
832
+ const clearActiveToolTimeout = (event: { toolCallId?: unknown; toolName?: unknown }): void => {
833
+ const key = typeof event.toolCallId === "string" && event.toolCallId.length > 0
834
+ ? `id:${event.toolCallId}`
835
+ : typeof event.toolName === "string"
836
+ ? activeToolTimeoutKeysByName.get(event.toolName)?.[0]
837
+ : activeToolTimeouts.size === 1
838
+ ? [...activeToolTimeouts.keys()][0]
839
+ : undefined;
840
+ if (key) removeToolTimeoutKey(key);
841
+ };
842
+ const clearAllToolTimeouts = (): void => {
843
+ for (const key of [...activeToolTimeouts.keys()]) removeToolTimeoutKey(key);
844
+ };
845
+ const armToolTimeout = (event: { toolCallId?: unknown; toolName: string }): void => {
846
+ const timeoutForTool = effectiveToolTimeoutMs(event.toolName, toolTimeoutMs);
847
+ if (timeoutForTool === undefined) return;
848
+ const runRemaining = runDeadlineAt === undefined ? undefined : Math.max(0, runDeadlineAt - Date.now());
849
+ if (runRemaining !== undefined && timeoutForTool >= runRemaining) return;
850
+ const key = toolTimeoutCallKey(event, ++toolTimeoutSequence);
851
+ const toolName = event.toolName;
852
+ const timer = setTimeout(() => {
853
+ removeToolTimeoutKey(key);
854
+ terminateForTimeout(formatToolTimeoutMessage(toolName, timeoutForTool));
855
+ }, timeoutForTool);
856
+ timer.unref?.();
857
+ activeToolTimeouts.set(key, { toolName, timer });
858
+ const keys = activeToolTimeoutKeysByName.get(toolName) ?? [];
859
+ keys.push(key);
860
+ activeToolTimeoutKeysByName.set(toolName, keys);
861
+ };
862
+ registerTimeout?.(() => terminateForTimeout(timeoutMessage ?? "Subagent timed out."));
792
863
  registerStop?.(() => {
793
864
  if (settled || timedOut || stopped) return;
794
865
  stopped = true;
@@ -815,6 +886,7 @@ function runPiStreaming(
815
886
  turnBudgetHardKillTimer.unref?.();
816
887
  });
817
888
  const clearDrainTimers = () => {
889
+ clearAllToolTimeouts();
818
890
  if (finalDrainTimer) {
819
891
  clearTimeout(finalDrainTimer);
820
892
  finalDrainTimer = undefined;
@@ -1044,6 +1116,7 @@ function writeRunLog(
1044
1116
  shareUrl?: string;
1045
1117
  shareError?: string;
1046
1118
  },
1119
+ write: (filePath: string, content: string) => void = (filePath, content) => fs.writeFileSync(filePath, content, "utf-8"),
1047
1120
  ): void {
1048
1121
  const lines: string[] = [];
1049
1122
  lines.push(`# Subagent run ${input.id}`);
@@ -1073,7 +1146,7 @@ function writeRunLog(
1073
1146
  }
1074
1147
  lines.push(input.summary.trim() || "(no output)");
1075
1148
  lines.push("");
1076
- fs.writeFileSync(logPath, lines.join("\n"), "utf-8");
1149
+ write(logPath, lines.join("\n"));
1077
1150
  }
1078
1151
 
1079
1152
  /** Context for running a single step */
@@ -1104,6 +1177,10 @@ interface SingleStepContext {
1104
1177
  stopSignal?: AbortSignal;
1105
1178
  timeoutMessage?: string;
1106
1179
  stopMessage?: string;
1180
+ /** Resolved configured hard per-tool-call timeout (ms); fast tools still have a default when undefined. */
1181
+ toolTimeoutMs?: number;
1182
+ /** Effective step deadline (Date.now() + effective timeout) when a run budget exists. */
1183
+ deadlineAt?: number;
1107
1184
  turnBudget?: ResolvedTurnBudget;
1108
1185
  childIntercomTarget?: string;
1109
1186
  orchestratorIntercomTarget?: string;
@@ -1114,11 +1191,13 @@ interface SingleStepContext {
1114
1191
  onChildEvent?: (event: ChildEvent) => void;
1115
1192
  onWriterProcess?: (writer: { state: "none" | "spawning" } | { state: "running"; pid: number }) => void;
1116
1193
  onExternalProcess?: (process: ExternalProcessStatus) => void;
1194
+ onExternalJob?: (status: ExternalJobStatus) => void;
1117
1195
  skipAcceptance?: () => boolean;
1196
+ orcaProgressTab?: OrcaProgressTab;
1118
1197
  }
1119
1198
 
1120
1199
  /** Run a single pi agent step, returning output and metadata */
1121
- async function runSingleStep(
1200
+ async function runSingleStepInner(
1122
1201
  step: SubagentStep,
1123
1202
  ctx: SingleStepContext,
1124
1203
  ): Promise<StepResult & { completionGuardTriggered?: boolean }> {
@@ -1247,6 +1326,8 @@ async function runSingleStep(
1247
1326
  timeoutMessage: ctx.timeoutMessage,
1248
1327
  stopMessage: ctx.stopMessage,
1249
1328
  onProcess: ctx.onExternalProcess,
1329
+ onStdout: (chunk) => ctx.orcaProgressTab?.append(chunk.toString("utf-8")),
1330
+ onStderr: (chunk) => ctx.orcaProgressTab?.append(chunk.toString("utf-8")),
1250
1331
  }));
1251
1332
  try { fs.writeFileSync(ctx.outputFile, external.output, "utf-8"); } catch { /* Observability output is best-effort. */ }
1252
1333
  const resolvedOutput = step.outputPath && external.exitCode === 0
@@ -1288,6 +1369,69 @@ async function runSingleStep(
1288
1369
  });
1289
1370
  }
1290
1371
 
1372
+ if (step.runner?.type === "external-job") {
1373
+ const runner: ExternalJobRunnerStatus = {
1374
+ type: "external-job",
1375
+ provider: step.runner.provider,
1376
+ options: step.runner.options ?? {},
1377
+ capabilities: { stop: false, steer: false, resume: false, structuredOutput: false, toolEvents: false },
1378
+ };
1379
+ const outputSnapshot = captureSingleOutputSnapshot(step.outputPath);
1380
+ const external = await runExternalJob(omitUndefinedProperties({
1381
+ provider: runner.provider,
1382
+ options: runner.options,
1383
+ cwd: step.cwd ?? ctx.cwd,
1384
+ prompt: buildExternalCliPrompt(step.systemPrompt ?? "", task),
1385
+ asyncDir: path.dirname(ctx.outputFile),
1386
+ stepIndex: ctx.flatIndex,
1387
+ runId: ctx.id,
1388
+ agent: step.agent,
1389
+ sessionId: step.parentSessionId,
1390
+ registerTimeout: ctx.registerTimeout,
1391
+ registerStop: ctx.registerStop,
1392
+ timeoutMessage: ctx.timeoutMessage,
1393
+ stopMessage: ctx.stopMessage,
1394
+ onExternalJob: ctx.onExternalJob,
1395
+ }));
1396
+ try { fs.writeFileSync(ctx.outputFile, external.output, "utf-8"); } catch { /* Observability output is best-effort. */ }
1397
+ const resolvedOutput = step.outputPath && external.exitCode === 0
1398
+ ? resolveSingleOutput(step.outputPath, external.output, outputSnapshot)
1399
+ : { fullOutput: external.output };
1400
+ const outputReference = resolvedOutput.savedPath ? formatSavedOutputReference(resolvedOutput.savedPath, resolvedOutput.fullOutput) : undefined;
1401
+ const finalizedOutput = finalizeSingleOutput(omitUndefinedProperties({
1402
+ fullOutput: resolvedOutput.fullOutput,
1403
+ outputPath: step.outputPath,
1404
+ outputMode: step.outputMode,
1405
+ exitCode: external.exitCode,
1406
+ savedPath: resolvedOutput.savedPath,
1407
+ outputReference,
1408
+ saveError: resolvedOutput.saveError,
1409
+ }));
1410
+ const artifactErrors = artifactPaths && ctx.artifactConfig?.enabled !== false
1411
+ ? persistStepArtifacts({
1412
+ artifactPaths,
1413
+ artifactConfig: ctx.artifactConfig,
1414
+ output: formatOutputArtifactContent(omitUndefinedProperties({ output: resolvedOutput.fullOutput, error: external.error, metadataPath: ctx.artifactConfig?.includeMetadata === false ? undefined : artifactPaths.metadataPath })),
1415
+ metadata: { runId: ctx.id, agent: step.agent, task: PROMPT_REDACTED, runner, externalJob: external.externalJob, exitCode: external.exitCode, error: external.error, timestamp: Date.now() },
1416
+ })
1417
+ : {};
1418
+ return omitUndefinedProperties({
1419
+ agent: step.agent,
1420
+ context: step.context,
1421
+ output: finalizedOutput.displayOutput,
1422
+ outputState: external.output.trim() ? "present" : "absent",
1423
+ exitCode: external.exitCode,
1424
+ error: external.error,
1425
+ timedOut: external.timedOut,
1426
+ stopped: external.stopped,
1427
+ artifactPaths,
1428
+ outputSaveError: artifactErrors.outputSaveError,
1429
+ metadataSaveError: artifactErrors.metadataSaveError,
1430
+ runner,
1431
+ externalJob: external.externalJob,
1432
+ });
1433
+ }
1434
+
1291
1435
  const candidates = step.modelCandidates && step.modelCandidates.length > 0
1292
1436
  ? step.modelCandidates
1293
1437
  : step.model
@@ -1433,6 +1577,9 @@ async function runSingleStep(
1433
1577
  ctx.stopMessage,
1434
1578
  ctx.registerTurnBudgetAbort,
1435
1579
  ctx.onWriterProcess,
1580
+ ctx.toolTimeoutMs,
1581
+ ctx.deadlineAt,
1582
+ ctx.orcaProgressTab,
1436
1583
  );
1437
1584
  if (run.processCloseObservedAt !== undefined) {
1438
1585
  writerProcesses.push({
@@ -1540,15 +1687,21 @@ async function runSingleStep(
1540
1687
  stopped: run.stopped,
1541
1688
  turnBudgetExceeded: run.turnBudgetExceeded,
1542
1689
  })) ? formatProcessSignalError(run.processSignal!) : undefined;
1543
- const error = toolAvailabilityError
1544
- ?? completionGuardError
1545
- ?? structuredError
1546
- ?? emptyOutputError
1547
- ?? (hiddenError?.hasError
1548
- ? hiddenError.details
1549
- ? `${hiddenError.errorType} failed (exit ${effectiveExitCode}): ${hiddenError.details}`
1550
- : `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
1551
- : run.error || signalError || (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined));
1690
+ const error = formatSubagentExtensionConflictError(
1691
+ toolAvailabilityError
1692
+ ?? completionGuardError
1693
+ ?? structuredError
1694
+ ?? emptyOutputError
1695
+ ?? (hiddenError?.hasError
1696
+ ? hiddenError.details
1697
+ ? `${hiddenError.errorType} failed (exit ${effectiveExitCode}): ${hiddenError.details}`
1698
+ : `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
1699
+ : run.error || signalError || (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined)),
1700
+ {
1701
+ agent: step.agent,
1702
+ ambientExtensionsEnabled: launchResolvedExtensions?.disableAmbientExtensions === false,
1703
+ },
1704
+ );
1552
1705
  const attempt: ModelAttempt = omitUndefinedProperties({
1553
1706
  model: candidate ?? run.model ?? step.model ?? "default",
1554
1707
  success: effectiveExitCode === 0 && !error,
@@ -1699,7 +1852,7 @@ async function runSingleStep(
1699
1852
  const effectiveFinalError = stoppedAfterAcceptance
1700
1853
  ? ctx.stopMessage ?? "Subagent stopped by user."
1701
1854
  : timedOutAfterAcceptance
1702
- ? ctx.timeoutMessage ?? "Subagent timed out."
1855
+ ? finalResult?.error ?? ctx.timeoutMessage ?? "Subagent timed out."
1703
1856
  : turnBudgetExceeded
1704
1857
  ? finalResult?.error ?? (turnBudget ? turnBudgetExceededMessage(turnBudget, turnBudget.turnCount) : "Subagent exceeded turn budget.")
1705
1858
  : acceptanceCanFailRun
@@ -1784,20 +1937,51 @@ async function runSingleStep(
1784
1937
  return isAgentContractV1(step.agentContract) ? attachContractProjections(result as unknown as import("../../shared/types.ts").SingleResult) as unknown as typeof result : result;
1785
1938
  }
1786
1939
 
1940
+ async function runSingleStep(
1941
+ step: SubagentStep,
1942
+ ctx: SingleStepContext,
1943
+ ): Promise<StepResult & { completionGuardTriggered?: boolean }> {
1944
+ if (step.importAsyncRoot) return runSingleStepInner(step, ctx);
1945
+ const orcaProgressTab = createOrcaProgressTab({
1946
+ cwd: step.cwd ?? ctx.cwd,
1947
+ runId: ctx.id,
1948
+ agent: step.agent,
1949
+ index: ctx.flatIndex,
1950
+ });
1951
+ try {
1952
+ const result = await runSingleStepInner(step, { ...ctx, orcaProgressTab });
1953
+ orcaProgressTab?.finish(result.stopped ? "stopped" : result.exitCode === 0 && !result.error ? "completed" : "failed", result.sessionFile);
1954
+ return result;
1955
+ } catch (error) {
1956
+ orcaProgressTab?.finish("failed");
1957
+ throw error;
1958
+ }
1959
+ }
1960
+
1787
1961
  type RunnerStatusStep = NonNullable<AsyncStatus["steps"]>[number] & {
1788
1962
  exitCode?: number | null;
1789
1963
  description?: string;
1790
1964
  };
1791
1965
 
1792
- function externalRunnerStatus(runner: SubagentStep["runner"]): ExternalCliRunnerStatus | undefined {
1793
- if (runner?.type !== "external-cli") return undefined;
1794
- return {
1795
- type: "external-cli",
1796
- command: runner.command,
1797
- args: runner.args ?? [],
1798
- promptDelivery: runner.promptDelivery ?? "stdin",
1799
- capabilities: { stop: true, steer: false, resume: false, structuredOutput: false, toolEvents: false },
1800
- };
1966
+ function externalRunnerStatus(runner: SubagentStep["runner"]): ExternalCliRunnerStatus | ExternalJobRunnerStatus | undefined {
1967
+ if (runner?.type === "external-cli") {
1968
+ return {
1969
+ type: "external-cli",
1970
+ command: runner.command,
1971
+ args: runner.args ?? [],
1972
+ promptDelivery: runner.promptDelivery ?? "stdin",
1973
+ capabilities: { stop: true, steer: false, resume: false, structuredOutput: false, toolEvents: false },
1974
+ };
1975
+ }
1976
+ if (runner?.type === "external-job") {
1977
+ return {
1978
+ type: "external-job",
1979
+ provider: runner.provider,
1980
+ options: runner.options ?? {},
1981
+ capabilities: { stop: false, steer: false, resume: false, structuredOutput: false, toolEvents: false },
1982
+ };
1983
+ }
1984
+ return undefined;
1801
1985
  }
1802
1986
 
1803
1987
  function appendCapabilityCeilingAppliedEvent(eventsPath: string, runId: string, stepIndex: number, agent: string, result: StepResult): void {
@@ -1846,6 +2030,7 @@ function markParallelGroupSetupFailure(input: {
1846
2030
  asyncDir: string;
1847
2031
  runId: string;
1848
2032
  stepIndex: number;
2033
+ writeStatus: (status: RunnerStatusPayload) => void;
1849
2034
  }): void {
1850
2035
  for (let taskIndex = 0; taskIndex < input.group.parallel.length; taskIndex++) {
1851
2036
  const flatTaskIndex = input.groupStartFlatIndex + taskIndex;
@@ -1862,7 +2047,7 @@ function markParallelGroupSetupFailure(input: {
1862
2047
  input.statusPayload.currentStep = input.groupStartFlatIndex;
1863
2048
  input.statusPayload.lastUpdate = input.failedAt;
1864
2049
  input.statusPayload.outputFile = path.join(input.asyncDir, `output-${input.groupStartFlatIndex}.log`);
1865
- writeAtomicJson(input.statusPath, input.statusPayload);
2050
+ input.writeStatus(input.statusPayload);
1866
2051
  appendJsonl(input.eventsPath, JSON.stringify({
1867
2052
  type: "subagent.parallel.completed",
1868
2053
  ts: input.failedAt,
@@ -1882,6 +2067,7 @@ function markParallelGroupRunning(input: {
1882
2067
  asyncDir: string;
1883
2068
  runId: string;
1884
2069
  stepIndex: number;
2070
+ writeStatus: (status: RunnerStatusPayload) => void;
1885
2071
  }): void {
1886
2072
  for (let taskIndex = 0; taskIndex < input.group.parallel.length; taskIndex++) {
1887
2073
  const flatTaskIndex = input.groupStartFlatIndex + taskIndex;
@@ -1899,7 +2085,7 @@ function markParallelGroupRunning(input: {
1899
2085
  input.statusPayload.lastActivityAt = input.groupStartTime;
1900
2086
  input.statusPayload.lastUpdate = input.groupStartTime;
1901
2087
  input.statusPayload.outputFile = path.join(input.asyncDir, `output-${input.groupStartFlatIndex}.log`);
1902
- writeAtomicJson(input.statusPath, input.statusPayload);
2088
+ input.writeStatus(input.statusPayload);
1903
2089
  appendJsonl(input.eventsPath, JSON.stringify({
1904
2090
  type: "subagent.parallel.started",
1905
2091
  ts: input.groupStartTime,
@@ -2008,6 +2194,7 @@ async function runSingleStepWithTimeout(
2008
2194
  return await runSingleStep(step, {
2009
2195
  ...ctx,
2010
2196
  registerTimeout,
2197
+ deadlineAt: Date.now() + timeoutMs,
2011
2198
  timeoutSignal: combinedAbortSignal([ctx.timeoutSignal, timeoutController.signal]),
2012
2199
  timeoutMessage,
2013
2200
  });
@@ -2048,9 +2235,6 @@ async function runSubagent(
2048
2235
  let stopped = false;
2049
2236
  let turnBudgetExceeded = false;
2050
2237
  let usageBudgetExceeded = false;
2051
- let checkpointRejected = false;
2052
- let pendingCheckpointDecision: "approved" | "rejected" | undefined;
2053
- let wakeCheckpointDecision: (() => void) | undefined;
2054
2238
  const timeoutMessage = config.timeoutMs !== undefined ? `Subagent timed out after ${config.timeoutMs}ms.` : undefined;
2055
2239
  const stopMessage = "Subagent stopped by user.";
2056
2240
  const timeoutAbortController = new AbortController();
@@ -2065,19 +2249,6 @@ async function runSubagent(
2065
2249
  let flatStepCount = 0;
2066
2250
  for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) {
2067
2251
  const step = steps[stepIndex]!;
2068
- if (isCheckpointRunnerStep(step)) {
2069
- initialStatusSteps.push(omitUndefinedProperties({
2070
- agent: `checkpoint:${step.checkpoint}`,
2071
- phase: step.phase,
2072
- label: step.label ?? step.checkpoint,
2073
- status: "pending",
2074
- checkpoint: { name: step.checkpoint, ...(step.message ? { message: step.message } : {}), status: "pending", stepIndex },
2075
- recentTools: [],
2076
- recentOutput: [],
2077
- }));
2078
- flatStepCount++;
2079
- continue;
2080
- }
2081
2252
  if (isParallelGroup(step)) {
2082
2253
  parallelGroups.push({ start: flatStepCount, count: step.parallel.length, stepIndex });
2083
2254
  for (const task of step.parallel) {
@@ -2169,8 +2340,9 @@ async function runSubagent(
2169
2340
  lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
2170
2341
  runId: id,
2171
2342
  ...(config.sessionId ? { sessionId: config.sessionId } : {}),
2343
+ ...(config.completionOwnerId ? { completionOwnerId: config.completionOwnerId } : {}),
2172
2344
  mode: config.resultMode ?? (flatSteps.length > 1 ? "chain" : "single"),
2173
- ...(config.nestedRoute ? { isNested: true } : {}),
2345
+ ...(config.nestedSelf ? { isNested: true } : {}),
2174
2346
  state: "running",
2175
2347
  steering: createSteeringStatus(),
2176
2348
  lastActivityAt: overallStartTime,
@@ -2200,9 +2372,37 @@ async function runSubagent(
2200
2372
  outputFile: path.join(asyncDir, "output-0.log"),
2201
2373
  });
2202
2374
 
2203
- fs.mkdirSync(asyncDir, { recursive: true });
2204
- writeAtomicJson(statusPath, statusPayload);
2205
- updateActiveRunIndex(asyncDir, statusPayload.state);
2375
+ let lastIndexedStatusState: AsyncStatus["state"] | undefined;
2376
+ const indexPersistence = createCapacityResilientJsonWriter({
2377
+ keepAlive: true,
2378
+ onSuccess: (_filePath, payload) => {
2379
+ lastIndexedStatusState = (payload as { state: AsyncStatus["state"] }).state;
2380
+ },
2381
+ onError: (error, filePath) => console.error(`Failed to update async run index '${filePath}':`, error),
2382
+ });
2383
+ const queueActiveRunIndex = (): void => {
2384
+ const state = statusPayload.state;
2385
+ if (state === lastIndexedStatusState && indexPersistence.pendingCount() === 0) return;
2386
+ indexPersistence.write(asyncDir, { state, toolCallId: statusPayload.toolCallId }, (_filePath, payload) => {
2387
+ const indexPayload = payload as { state: AsyncStatus["state"]; toolCallId?: string };
2388
+ updateActiveRunIndex(asyncDir, indexPayload.state, indexPayload.toolCallId, { retryCapacityErrors: true });
2389
+ });
2390
+ };
2391
+ const runPersistence = createCapacityResilientJsonWriter({
2392
+ keepAlive: true,
2393
+ onSuccess: (filePath) => {
2394
+ if (filePath === statusPath) queueActiveRunIndex();
2395
+ },
2396
+ onError: (error, filePath) => console.error(`Failed to persist async run state '${filePath}':`, error),
2397
+ });
2398
+ try {
2399
+ fs.mkdirSync(asyncDir, { recursive: true });
2400
+ } catch (error) {
2401
+ if (!isStorageCapacityError(error)) throw error;
2402
+ console.error(`Failed to prepare async run storage '${asyncDir}' while storage is full:`, error);
2403
+ }
2404
+ runPersistence.write(statusPath, statusPayload);
2405
+
2206
2406
  let pendingParallelUsageCost: CostSummary = { inputTokens: 0, outputTokens: 0, costUsd: 0 };
2207
2407
  const currentUsageTotals = (): CostSummary => {
2208
2408
  const cost = results.reduce<CostSummary>((sum, result) => ({
@@ -2257,7 +2457,6 @@ async function runSubagent(
2257
2457
  node.status = normalize(step.status);
2258
2458
  setOptionalProperty(node, "error", step.error);
2259
2459
  setOptionalProperty(node, "acceptanceStatus", step.acceptance?.status);
2260
- setOptionalProperty(node, "checkpoint", step.checkpoint);
2261
2460
  }
2262
2461
  if (statusPayload.currentStep === node.flatIndex) graph.currentNodeId = node.id;
2263
2462
  }
@@ -2275,15 +2474,65 @@ async function runSubagent(
2275
2474
  for (const node of graph.nodes) updateNode(node);
2276
2475
  statusPayload.workflowGraph = graph;
2277
2476
  };
2278
- let lastIndexedActiveState = isActiveAsyncState(statusPayload.state);
2477
+ let finalResultCommitted = false;
2478
+ const statusResultState = (): AsyncStatus["state"] | undefined => {
2479
+ if (statusPayload.state === "running" || statusPayload.state === "queued") return undefined;
2480
+ return statusPayload.state;
2481
+ };
2482
+ const statusResultSummary = (state: AsyncStatus["state"]): string => {
2483
+ if (statusPayload.error) return statusPayload.error;
2484
+ if (state === "paused") return "Paused after interrupt. Waiting for explicit next action.";
2485
+ if (state === "stopped") return stopMessage;
2486
+ if (state === "rejected") return "Subagent rejected.";
2487
+ return state === "complete" ? "Subagent completed." : "Subagent failed.";
2488
+ };
2489
+ const statusResultSuccess = (state: AsyncStatus["state"], step: RunnerStatusStep): boolean | undefined => {
2490
+ if (step.status === "complete" || step.status === "completed") return true;
2491
+ if (step.status === "failed" || step.status === "stopped" || step.status === "rejected") return false;
2492
+ if (state === "complete") return true;
2493
+ if (state === "failed" || state === "stopped" || state === "rejected") return false;
2494
+ return undefined;
2495
+ };
2496
+ const writeRecoverableStatusResult = (): void => {
2497
+ const state = statusResultState();
2498
+ if (!state || finalResultCommitted || !config.sessionId) return;
2499
+ const now = statusPayload.endedAt ?? statusPayload.lastUpdate ?? Date.now();
2500
+ const summary = statusResultSummary(state);
2501
+ runPersistence.write(resultPath, omitUndefinedProperties({
2502
+ lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
2503
+ id,
2504
+ runId: id,
2505
+ agent: statusPayload.steps.length === 1 ? statusPayload.steps[0]!.agent : statusPayload.mode === "parallel" ? `parallel:${statusPayload.steps.map((step) => step.agent).join("+")}` : `chain:${statusPayload.steps.map((step) => step.agent).join("->")}`,
2506
+ mode: statusPayload.mode,
2507
+ success: state === "complete",
2508
+ state,
2509
+ summary,
2510
+ error: state === "failed" || state === "stopped" || state === "rejected" ? summary : undefined,
2511
+ stopped: state === "stopped" ? true : undefined,
2512
+ results: statusPayload.steps.map((step) => omitUndefinedProperties({
2513
+ agent: step.agent,
2514
+ output: step.status === "complete" || step.status === "completed" ? "" : step.error ?? summary,
2515
+ error: step.error,
2516
+ success: statusResultSuccess(state, step),
2517
+ sessionFile: step.sessionFile,
2518
+ model: step.model,
2519
+ attemptedModels: step.attemptedModels,
2520
+ modelAttempts: step.modelAttempts,
2521
+ })),
2522
+ exitCode: state === "complete" || state === "paused" ? 0 : 1,
2523
+ timestamp: now,
2524
+ durationMs: Math.max(0, now - overallStartTime),
2525
+ asyncDir,
2526
+ cwd,
2527
+ sessionId: config.sessionId,
2528
+ completionOwnerId: config.completionOwnerId,
2529
+ sessionFile: statusPayload.sessionFile ?? latestSessionFile,
2530
+ }), (filePath, payload) => writePendingAsyncResultFile(filePath, payload as Record<string, unknown>));
2531
+ };
2279
2532
  const writeStatusPayloadNow = (): void => {
2280
2533
  refreshWorkflowGraph();
2281
- writeAtomicJson(statusPath, statusPayload);
2282
- const activeState = isActiveAsyncState(statusPayload.state);
2283
- if (activeState && activeState !== lastIndexedActiveState) {
2284
- updateActiveRunIndex(asyncDir, statusPayload.state);
2285
- }
2286
- lastIndexedActiveState = activeState;
2534
+ writeRecoverableStatusResult();
2535
+ runPersistence.write(statusPath, statusPayload);
2287
2536
  emitNestedSelfEvent(statusPayload.state === "running" || statusPayload.state === "queued" ? "subagent.nested.updated" : "subagent.nested.completed");
2288
2537
  };
2289
2538
  const statusWriteCoalescer = createFileCoalescer(writeStatusPayloadNow, 100);
@@ -2299,6 +2548,11 @@ async function runSubagent(
2299
2548
  statusPayload.lastUpdate = Date.now();
2300
2549
  writeStatusPayload();
2301
2550
  };
2551
+ const updateExternalJob = (index: number, externalJob: ExternalJobStatus): void => {
2552
+ requiredStatusStep(statusPayload, index).externalJob = externalJob;
2553
+ statusPayload.lastUpdate = Date.now();
2554
+ writeStatusPayload();
2555
+ };
2302
2556
  const registerStepInterrupt = (flatIndex: number, interrupt: (() => void) | undefined): void => {
2303
2557
  if (!interrupt) {
2304
2558
  activeChildInterrupts.delete(flatIndex);
@@ -2526,10 +2780,84 @@ async function runSubagent(
2526
2780
  const activeLongRunningSteps = new Set<number>();
2527
2781
  const mutatingFailureStates = initialStatusSteps.map(() => createMutatingFailureState());
2528
2782
  const pendingToolResults: Array<{ tool: string; path?: string; mutates: boolean; startedAt?: number } | undefined> = initialStatusSteps.map(() => undefined);
2783
+ type ActiveToolCall = { key: string; tool: string; args: string; startedAt: number; path?: string; blocksSupervisor: boolean };
2784
+ const activeToolCalls = initialStatusSteps.map(() => new Map<string, ActiveToolCall>());
2785
+ const activeToolKeysByName = initialStatusSteps.map(() => new Map<string, string[]>());
2786
+ const activeToolSequences = initialStatusSteps.map(() => 0);
2787
+ const latestActiveToolCall = (flatIndex: number): ActiveToolCall | undefined => [...(activeToolCalls[flatIndex]?.values() ?? [])].sort((left, right) => right.startedAt - left.startedAt)[0];
2788
+ const refreshStepCurrentTool = (flatIndex: number): void => {
2789
+ const step = statusPayload.steps[flatIndex];
2790
+ if (!step) return;
2791
+ const active = latestActiveToolCall(flatIndex);
2792
+ if (!active) {
2793
+ delete step.currentTool;
2794
+ delete step.currentToolArgs;
2795
+ delete step.currentToolStartedAt;
2796
+ delete step.currentPath;
2797
+ return;
2798
+ }
2799
+ step.currentTool = active.tool;
2800
+ step.currentToolArgs = active.args;
2801
+ step.currentToolStartedAt = active.startedAt;
2802
+ setOptionalProperty(step, "currentPath", active.path);
2803
+ };
2804
+ const recordActiveToolCall = (flatIndex: number, event: { toolCallId?: unknown; toolName: string }, input: { argsPreview: string; currentPath?: string; blocksSupervisor: boolean; now: number }): ActiveToolCall => {
2805
+ const sequence = (activeToolSequences[flatIndex] ?? 0) + 1;
2806
+ activeToolSequences[flatIndex] = sequence;
2807
+ const key = toolTimeoutCallKey(event, sequence);
2808
+ const active: ActiveToolCall = {
2809
+ key,
2810
+ tool: event.toolName,
2811
+ args: input.argsPreview,
2812
+ startedAt: input.now,
2813
+ blocksSupervisor: input.blocksSupervisor,
2814
+ ...(input.currentPath !== undefined ? { path: input.currentPath } : {}),
2815
+ };
2816
+ activeToolCalls[flatIndex]?.set(key, active);
2817
+ const keysByName = activeToolKeysByName[flatIndex];
2818
+ const keys = keysByName?.get(active.tool) ?? [];
2819
+ keys.push(key);
2820
+ keysByName?.set(active.tool, keys);
2821
+ refreshStepCurrentTool(flatIndex);
2822
+ return active;
2823
+ };
2824
+ const removeActiveToolCallKey = (flatIndex: number, key: string): ActiveToolCall | undefined => {
2825
+ const calls = activeToolCalls[flatIndex];
2826
+ const active = calls?.get(key);
2827
+ if (!active) return undefined;
2828
+ calls?.delete(key);
2829
+ const keysByName = activeToolKeysByName[flatIndex];
2830
+ const keys = keysByName?.get(active.tool)?.filter((candidate) => candidate !== key) ?? [];
2831
+ if (keys.length > 0) keysByName?.set(active.tool, keys);
2832
+ else keysByName?.delete(active.tool);
2833
+ return active;
2834
+ };
2835
+ const removeActiveToolCall = (flatIndex: number, event: { toolCallId?: unknown; toolName?: unknown }): ActiveToolCall | undefined => {
2836
+ const calls = activeToolCalls[flatIndex];
2837
+ const key = typeof event.toolCallId === "string" && event.toolCallId.length > 0
2838
+ ? `id:${event.toolCallId}`
2839
+ : typeof event.toolName === "string"
2840
+ ? activeToolKeysByName[flatIndex]?.get(event.toolName)?.[0]
2841
+ : calls?.size === 1
2842
+ ? [...calls.keys()][0]
2843
+ : undefined;
2844
+ return key ? removeActiveToolCallKey(flatIndex, key) : undefined;
2845
+ };
2846
+ const openToolAttentionTarget = (flatIndex: number, now: number): ActiveToolCall | undefined => [...(activeToolCalls[flatIndex]?.values() ?? [])]
2847
+ .filter((active) => shouldEmitOpenToolAttention({ config: controlConfig, currentTool: active.tool, currentToolStartedAt: active.startedAt, now }))
2848
+ .sort((left, right) => left.startedAt - right.startedAt)[0];
2529
2849
  const supervisorAttentionSteps = new Map<number, ActivityState | undefined>();
2530
2850
  const mutatingFailureWindowMs = 5 * 60_000;
2531
- const appendControlEvent = (event: ReturnType<typeof buildControlEvent>) => {
2851
+ const appendControlEvent = (rawEvent: ReturnType<typeof buildControlEvent>) => {
2532
2852
  if (!controlConfig.enabled) return;
2853
+ const contextStep = statusPayload.steps[rawEvent.index ?? statusPayload.currentStep ?? 0];
2854
+ const event = {
2855
+ ...rawEvent,
2856
+ ...(contextStep?.workflowKey ?? statusPayload.workflowKey ? { workflowKey: contextStep?.workflowKey ?? statusPayload.workflowKey } : {}),
2857
+ ...(contextStep?.phase ? { phase: contextStep.phase } : {}),
2858
+ ...(contextStep?.label ? { label: contextStep.label } : {}),
2859
+ ...(contextStep?.description ? { taskPreview: contextStep.description } : {}),
2860
+ };
2533
2861
  const childIntercomTarget = config.childIntercomTargets?.[event.index ?? statusPayload.currentStep];
2534
2862
  const channels = event.type === "active_long_running"
2535
2863
  ? controlConfig.notifyChannels.filter((channel) => channel !== "intercom")
@@ -2566,6 +2894,34 @@ async function runSubagent(
2566
2894
  currentActivityState = nextRunState;
2567
2895
  setOptionalProperty(statusPayload, "activityState", nextRunState);
2568
2896
  };
2897
+ const maybeEmitOpenToolAttention = (flatIndex: number, now: number): boolean => {
2898
+ const step = statusPayload.steps[flatIndex];
2899
+ if (!step || step.status !== "running" || step.activityState === "needs_attention") return false;
2900
+ const target = openToolAttentionTarget(flatIndex, now);
2901
+ if (!target) return false;
2902
+ const previous = step.activityState;
2903
+ step.activityState = "needs_attention";
2904
+ statusPayload.activityState = "needs_attention";
2905
+ const toolDurationMs = Math.max(0, now - target.startedAt);
2906
+ appendControlEvent(buildControlEvent(omitUndefinedProperties({
2907
+ type: "needs_attention",
2908
+ from: previous,
2909
+ to: "needs_attention",
2910
+ runId: id,
2911
+ agent: step.agent,
2912
+ index: flatIndex,
2913
+ ts: now,
2914
+ message: `${step.agent} has had tool '${target.tool}' open for ${Math.floor(toolDurationMs / 1000)}s`,
2915
+ reason: "tool_open_threshold",
2916
+ turns: step.turnCount,
2917
+ tokens: step.tokens?.total,
2918
+ toolCount: step.toolCount,
2919
+ currentTool: target.tool,
2920
+ currentToolDurationMs: toolDurationMs,
2921
+ currentPath: target.path,
2922
+ })));
2923
+ return true;
2924
+ };
2569
2925
  const maybeEmitActiveLongRunning = (flatIndex: number, now: number): boolean => {
2570
2926
  if (!controlConfig.enabled || activeLongRunningSteps.has(flatIndex)) return false;
2571
2927
  const step = statusPayload.steps[flatIndex];
@@ -2826,20 +3182,19 @@ async function runSubagent(
2826
3182
  if (event.type === "tool_execution_start" && event.toolName) {
2827
3183
  const mutates = isMutatingTool(event.toolName, event.args);
2828
3184
  const currentPath = resolveCurrentPath(event.toolName, event.args);
3185
+ const argsPreview = extractToolArgsPreview(event.args ?? {});
3186
+ const blocksSupervisor = isBlockingSupervisorTool(event.toolName, event.args);
2829
3187
  step.toolCount = (step.toolCount ?? 0) + 1;
2830
3188
  const configuredToolBudget = flatSteps[flatIndex]?.toolBudget;
2831
3189
  if (configuredToolBudget) {
2832
3190
  step.toolBudget = toolBudgetState(configuredToolBudget, step.toolCount);
2833
3191
  statusPayload.toolBudget = step.toolBudget;
2834
3192
  }
2835
- step.currentTool = event.toolName;
2836
- step.currentToolArgs = extractToolArgsPreview(event.args ?? {});
2837
- step.currentToolStartedAt = now;
2838
- setOptionalProperty(step, "currentPath", currentPath);
3193
+ recordActiveToolCall(flatIndex, { toolCallId: (event as { toolCallId?: unknown }).toolCallId, toolName: event.toolName }, { argsPreview, currentPath, blocksSupervisor, now });
2839
3194
  pendingToolResults[flatIndex] = omitUndefinedProperties({ tool: event.toolName, path: currentPath, mutates, startedAt: now });
2840
3195
  statusPayload.toolCount = (statusPayload.toolCount ?? 0) + 1;
2841
3196
  syncTopLevelCurrentTool();
2842
- if (controlConfig.enabled && isBlockingSupervisorTool(event.toolName, event.args) && step.activityState !== "needs_attention") {
3197
+ if (controlConfig.enabled && blocksSupervisor && step.activityState !== "needs_attention") {
2843
3198
  const previous = step.activityState;
2844
3199
  step.activityState = "needs_attention";
2845
3200
  supervisorAttentionSteps.set(flatIndex, previous);
@@ -2864,16 +3219,15 @@ async function runSubagent(
2864
3219
  })));
2865
3220
  }
2866
3221
  } else if (event.type === "tool_execution_end") {
2867
- if (step.currentTool) {
3222
+ const endedTool = removeActiveToolCall(flatIndex, event);
3223
+ if (endedTool) {
2868
3224
  step.recentTools ??= [];
2869
- step.recentTools.push({ tool: step.currentTool, args: step.currentToolArgs || "", endMs: now });
3225
+ step.recentTools.push({ tool: endedTool.tool, args: endedTool.args, endMs: now });
2870
3226
  }
3227
+ refreshStepCurrentTool(flatIndex);
2871
3228
  const supervisorPreviousActivity = supervisorAttentionSteps.get(flatIndex);
2872
- const clearedSupervisorAttention = supervisorAttentionSteps.delete(flatIndex);
2873
- delete step.currentTool;
2874
- delete step.currentToolArgs;
2875
- delete step.currentToolStartedAt;
2876
- delete step.currentPath;
3229
+ const stillBlockingSupervisor = [...(activeToolCalls[flatIndex]?.values() ?? [])].some((active) => active.blocksSupervisor);
3230
+ const clearedSupervisorAttention = endedTool?.blocksSupervisor && !stillBlockingSupervisor ? supervisorAttentionSteps.delete(flatIndex) : false;
2877
3231
  if (clearedSupervisorAttention && step.activityState === "needs_attention") {
2878
3232
  setOptionalProperty(step, "activityState", supervisorPreviousActivity);
2879
3233
  syncAggregateActivityState();
@@ -2993,6 +3347,8 @@ async function runSubagent(
2993
3347
  })));
2994
3348
  changed = true;
2995
3349
  }
3350
+ } else if (maybeEmitOpenToolAttention(index, now)) {
3351
+ changed = true;
2996
3352
  } else if (maybeEmitActiveLongRunning(index, now)) {
2997
3353
  changed = true;
2998
3354
  }
@@ -3124,10 +3480,6 @@ async function runSubagent(
3124
3480
  onInterrupt: interruptRunner,
3125
3481
  onTimeout: timeoutRunner,
3126
3482
  onStop: stopRunner,
3127
- onCheckpointDecision: (decision) => {
3128
- pendingCheckpointDecision = decision;
3129
- wakeCheckpointDecision?.();
3130
- },
3131
3483
  onSteer: (request) => {
3132
3484
  const targetStep = request.targetIndex !== undefined ? statusPayload.steps[request.targetIndex] : undefined;
3133
3485
  if (targetStep?.status === "pending") {
@@ -3178,20 +3530,6 @@ async function runSubagent(
3178
3530
 
3179
3531
  let flatIndex = 0;
3180
3532
  let stepCursor = 0;
3181
- const waitForCheckpointDecision = async (): Promise<"approved" | "rejected" | undefined> => {
3182
- while (!pendingCheckpointDecision && !interrupted && !timedOut && !stopped) {
3183
- await new Promise<void>((resolve) => {
3184
- wakeCheckpointDecision = resolve;
3185
- const timer = setTimeout(resolve, POLL_INTERVAL_MS);
3186
- timer.unref?.();
3187
- });
3188
- wakeCheckpointDecision = undefined;
3189
- }
3190
- const decision = pendingCheckpointDecision;
3191
- pendingCheckpointDecision = undefined;
3192
- return decision;
3193
- };
3194
-
3195
3533
  while (true) {
3196
3534
  if (interrupted || timedOut || stopped || turnBudgetExceeded) break;
3197
3535
  consumePendingAppendRequests();
@@ -3209,64 +3547,6 @@ async function runSubagent(
3209
3547
  const stepIndex = stepCursor++;
3210
3548
  const step = steps[stepIndex]!;
3211
3549
 
3212
- if (isCheckpointRunnerStep(step)) {
3213
- const now = Date.now();
3214
- const statusStep = statusPayload.steps[flatIndex];
3215
- const checkpoint = { name: step.checkpoint, ...(step.message ? { message: step.message } : {}), status: "pending" as const, stepIndex };
3216
- statusPayload.state = "paused";
3217
- statusPayload.currentStep = flatIndex;
3218
- statusPayload.checkpoint = checkpoint;
3219
- delete statusPayload.activityState;
3220
- statusPayload.lastUpdate = now;
3221
- if (statusStep) {
3222
- statusStep.status = "paused";
3223
- statusStep.startedAt = now;
3224
- statusStep.checkpoint = checkpoint;
3225
- }
3226
- writeStatusPayload();
3227
- appendJsonl(eventsPath, JSON.stringify({ type: "subagent.checkpoint.paused", ts: now, runId: id, stepIndex, checkpoint }));
3228
- const decision = await waitForCheckpointDecision();
3229
- if (decision === "approved") {
3230
- const approvedAt = Date.now();
3231
- const approved = { ...checkpoint, status: "approved" as const, approvedAt };
3232
- statusPayload.state = "running";
3233
- statusPayload.checkpoint = approved;
3234
- if (statusStep) {
3235
- statusStep.status = "complete";
3236
- statusStep.endedAt = approvedAt;
3237
- statusStep.durationMs = approvedAt - now;
3238
- statusStep.exitCode = 0;
3239
- statusStep.checkpoint = approved;
3240
- }
3241
- statusPayload.lastUpdate = approvedAt;
3242
- writeStatusPayload();
3243
- appendJsonl(eventsPath, JSON.stringify({ type: "subagent.checkpoint.approved", ts: approvedAt, runId: id, stepIndex, checkpoint: approved }));
3244
- flatIndex++;
3245
- continue;
3246
- }
3247
- if (decision === "rejected") {
3248
- const rejectedAt = Date.now();
3249
- const rejected = { ...checkpoint, status: "rejected" as const, rejectedAt };
3250
- checkpointRejected = true;
3251
- statusPayload.state = "rejected";
3252
- statusPayload.error = `Checkpoint '${step.checkpoint}' rejected.`;
3253
- statusPayload.checkpoint = rejected;
3254
- if (statusStep) {
3255
- statusStep.status = "rejected";
3256
- statusStep.error = statusPayload.error;
3257
- statusStep.endedAt = rejectedAt;
3258
- statusStep.durationMs = rejectedAt - now;
3259
- statusStep.exitCode = 1;
3260
- statusStep.checkpoint = rejected;
3261
- }
3262
- statusPayload.lastUpdate = rejectedAt;
3263
- writeStatusPayload();
3264
- appendJsonl(eventsPath, JSON.stringify({ type: "subagent.checkpoint.rejected", ts: rejectedAt, runId: id, stepIndex, checkpoint: rejected }));
3265
- break;
3266
- }
3267
- break;
3268
- }
3269
-
3270
3550
  if (isDynamicRunnerGroup(step)) {
3271
3551
  const groupStartFlatIndex = flatIndex;
3272
3552
  let materialized: ReturnType<typeof materializeDynamicParallelStep>;
@@ -3555,10 +3835,12 @@ async function runSubagent(
3555
3835
  timeoutMessage,
3556
3836
  stopMessage,
3557
3837
  turnBudget: config.turnBudget,
3838
+ toolTimeoutMs: task.toolTimeoutMs ?? config.toolTimeoutMs,
3558
3839
  onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking),
3559
3840
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
3560
3841
  onWriterProcess,
3561
3842
  onExternalProcess: (process) => updateExternalProcess(fi, process),
3843
+ onExternalJob: (externalJob) => updateExternalJob(fi, externalJob),
3562
3844
  skipAcceptance: () => timedOut || stopped,
3563
3845
  }), config.deadlineAt);
3564
3846
  const taskEndTime = Date.now();
@@ -3784,6 +4066,7 @@ async function runSubagent(
3784
4066
  asyncDir,
3785
4067
  runId: id,
3786
4068
  stepIndex,
4069
+ writeStatus: () => writeStatusPayload(),
3787
4070
  });
3788
4071
  flatIndex += group.parallel.length;
3789
4072
  break;
@@ -3811,6 +4094,7 @@ async function runSubagent(
3811
4094
  asyncDir,
3812
4095
  runId: id,
3813
4096
  stepIndex,
4097
+ writeStatus: () => writeStatusPayload(),
3814
4098
  });
3815
4099
  flatIndex += group.parallel.length;
3816
4100
  break;
@@ -3842,6 +4126,7 @@ async function runSubagent(
3842
4126
  asyncDir,
3843
4127
  runId: id,
3844
4128
  stepIndex,
4129
+ writeStatus: () => writeStatusPayload(),
3845
4130
  });
3846
4131
  const parallelResults = await mapConcurrent(
3847
4132
  group.parallel,
@@ -3938,10 +4223,12 @@ async function runSubagent(
3938
4223
  timeoutMessage,
3939
4224
  stopMessage,
3940
4225
  turnBudget: config.turnBudget,
4226
+ toolTimeoutMs: taskForRun.toolTimeoutMs ?? config.toolTimeoutMs,
3941
4227
  onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking),
3942
4228
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
3943
4229
  onWriterProcess,
3944
4230
  onExternalProcess: (process) => updateExternalProcess(fi, process),
4231
+ onExternalJob: (externalJob) => updateExternalJob(fi, externalJob),
3945
4232
  skipAcceptance: () => timedOut || stopped,
3946
4233
  }), config.deadlineAt);
3947
4234
  if (task.sessionFile) {
@@ -4179,6 +4466,32 @@ async function runSubagent(
4179
4466
  }
4180
4467
  } else {
4181
4468
  const seqStep = step as SubagentStep;
4469
+ let singleWorktreeSetup: WorktreeSetup | undefined;
4470
+ if (seqStep.worktree) {
4471
+ try {
4472
+ singleWorktreeSetup = createWorktrees(cwd, `${id}-s${stepIndex}`, 1, omitUndefinedProperties({
4473
+ agents: [seqStep.agent],
4474
+ setupHook: config.worktreeSetupHook
4475
+ ? omitUndefinedProperties({ hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs })
4476
+ : undefined,
4477
+ baseDir: config.worktreeBaseDir,
4478
+ }));
4479
+ writePendingParallelHandoff({
4480
+ manifestPath: parallelHandoffPath(asyncDir),
4481
+ runId: id,
4482
+ mode: "single",
4483
+ source: "async",
4484
+ cwd,
4485
+ stepIndex,
4486
+ flatStartIndex: flatIndex,
4487
+ setup: singleWorktreeSetup,
4488
+ });
4489
+ } catch (error) {
4490
+ if (singleWorktreeSetup) cleanupWorktrees(singleWorktreeSetup);
4491
+ throw error;
4492
+ }
4493
+ }
4494
+ const singleCwd = singleWorktreeSetup?.worktrees[0]?.agentCwd ?? cwd;
4182
4495
  const stepStartTime = Date.now();
4183
4496
  statusPayload.currentStep = flatIndex;
4184
4497
  requiredStatusStep(statusPayload, flatIndex).status = "running";
@@ -4202,8 +4515,11 @@ async function runSubagent(
4202
4515
  }));
4203
4516
 
4204
4517
  flushPendingStepSteers(flatIndex);
4205
- const singleResult = await runSingleStepWithTimeout(seqStep, compactOptional<SingleStepContext>({
4206
- previousOutput, placeholder, cwd, sessionEnabled,
4518
+ const executionStep = singleWorktreeSetup ? { ...seqStep, cwd: singleCwd } : seqStep;
4519
+ let singleResult: Awaited<ReturnType<typeof runSingleStepWithTimeout>>;
4520
+ try {
4521
+ singleResult = await runSingleStepWithTimeout(executionStep, compactOptional<SingleStepContext>({
4522
+ previousOutput, placeholder, cwd: singleCwd, sessionEnabled,
4207
4523
  outputs: statusPayload.mode === "single" ? undefined : outputs,
4208
4524
  sessionDir: config.sessionDir,
4209
4525
  artifactsDir, artifactConfig, id,
@@ -4228,12 +4544,18 @@ async function runSubagent(
4228
4544
  timeoutMessage,
4229
4545
  stopMessage,
4230
4546
  turnBudget: config.turnBudget,
4547
+ toolTimeoutMs: seqStep.toolTimeoutMs ?? config.toolTimeoutMs,
4231
4548
  onAttemptStart: (attempt) => updateStepModel(flatIndex, attempt.model, attempt.thinking),
4232
4549
  onChildEvent: (event) => updateStepFromChildEvent(flatIndex, event),
4233
4550
  onWriterProcess,
4234
4551
  onExternalProcess: (process) => updateExternalProcess(flatIndex, process),
4552
+ onExternalJob: (externalJob) => updateExternalJob(flatIndex, externalJob),
4235
4553
  skipAcceptance: () => timedOut || stopped,
4236
- }), config.deadlineAt);
4554
+ }), config.deadlineAt);
4555
+ } catch (error) {
4556
+ if (singleWorktreeSetup) cleanupWorktrees(singleWorktreeSetup);
4557
+ throw error;
4558
+ }
4237
4559
  if (seqStep.sessionFile) {
4238
4560
  latestSessionFile = seqStep.sessionFile;
4239
4561
  }
@@ -4282,6 +4604,7 @@ async function runSubagent(
4282
4604
  toolBudgetBlocked: singleResult.toolBudgetBlocked,
4283
4605
  runner: singleResult.runner,
4284
4606
  externalProcess: singleResult.externalProcess,
4607
+ externalJob: singleResult.externalJob,
4285
4608
  }));
4286
4609
  if (seqStep.outputName) {
4287
4610
  outputs[seqStep.outputName] = outputEntryFromAsyncResult({
@@ -4372,6 +4695,48 @@ async function runSubagent(
4372
4695
  durationMs: stepEndTime - stepStartTime,
4373
4696
  tokens: stepTokens,
4374
4697
  }));
4698
+ if (singleWorktreeSetup && !singleResult.detached) {
4699
+ const diffs = diffWorktrees(singleWorktreeSetup, [seqStep.agent], path.join(asyncDir, "worktree-diffs", `step-${stepIndex}`));
4700
+ const diffSummary = formatWorktreeDiffSummary(diffs);
4701
+ const manifestPath = parallelHandoffPath(asyncDir);
4702
+ const handoff = {
4703
+ manifestPath,
4704
+ runId: id,
4705
+ mode: "single" as const,
4706
+ source: "async" as const,
4707
+ cwd,
4708
+ stepIndex,
4709
+ flatStartIndex: flatIndex,
4710
+ setup: singleWorktreeSetup,
4711
+ diffs,
4712
+ results: [{
4713
+ agent: singleResult.agent,
4714
+ status: singleResult.stopped ? "stopped" as const : singleResult.interrupted ? "paused" as const : singleResult.exitCode === 0 ? "completed" as const : "failed" as const,
4715
+ summary: singleResult.output || singleResult.error || "(no output)",
4716
+ ...(singleResult.artifactPaths?.outputPath ? { outputPath: singleResult.artifactPaths.outputPath } : {}),
4717
+ ...(singleResult.structuredOutput !== undefined ? { structuredOutput: singleResult.structuredOutput } : {}),
4718
+ ...(singleResult.structuredOutputPath ? { structuredOutputPath: singleResult.structuredOutputPath } : {}),
4719
+ ...(singleResult.sessionFile ? { sessionPath: singleResult.sessionFile } : {}),
4720
+ }],
4721
+ };
4722
+ try {
4723
+ writeParallelHandoffGroup(handoff);
4724
+ const cleanup = cleanupWorktrees(singleWorktreeSetup, {
4725
+ kind: "preserve",
4726
+ capturedDiffs: diffs,
4727
+ handoffManifestPath: manifestPath,
4728
+ ...(config.parentWorkflowRunId && singleResult.sessionFile && fs.existsSync(singleResult.sessionFile) && !singleResult.stopped
4729
+ ? { cleanupBlocker: "retained child resume requires managed worktree cwd" }
4730
+ : {}),
4731
+ });
4732
+ statusPayload.parallelHandoff = writeParallelHandoffGroup({ ...handoff, cleanup });
4733
+ previousOutput = [previousOutput, diffSummary, formatParallelHandoffReference(statusPayload.parallelHandoff)].filter(Boolean).join("\n\n");
4734
+ } catch (error) {
4735
+ previousOutput = [previousOutput, diffSummary, formatParallelHandoffError(error)].filter(Boolean).join("\n\n");
4736
+ }
4737
+ writeStatusPayload();
4738
+ }
4739
+
4375
4740
  if (singleResult.completionGuardTriggered) {
4376
4741
  const event = buildControlEvent(omitUndefinedProperties({
4377
4742
  from: requiredStatusStep(statusPayload, flatIndex).activityState,
@@ -4473,8 +4838,8 @@ async function runSubagent(
4473
4838
  stopped: result.stopped,
4474
4839
  turnBudgetExceeded: result.turnBudgetExceeded,
4475
4840
  })));
4476
- statusPayload.state = checkpointRejected ? "rejected" : stopped || signalTerminated ? "stopped" : timedOut || turnBudgetExceeded || usageBudgetExceeded || statusPayload.error ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed";
4477
- closeSteerInbox(asyncDir, statusPayload.state);
4841
+ statusPayload.state = stopped || signalTerminated ? "stopped" : timedOut || turnBudgetExceeded || usageBudgetExceeded || statusPayload.error ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed";
4842
+ closeSteerInbox(asyncDir, statusPayload.state, (filePath, payload) => runPersistence.write(filePath, payload));
4478
4843
  disposeControlInbox();
4479
4844
  for (const request of consumeSteerRequests(asyncDir)) deliverSteerRequest(request);
4480
4845
  const effectiveSessionFile = sessionFile ?? latestSessionFile;
@@ -4525,16 +4890,15 @@ async function runSubagent(
4525
4890
  statusPayload.error = `Step failed: ${failedStep.agent}`;
4526
4891
  }
4527
4892
  }
4528
- writeStatusPayload();
4529
4893
  try {
4530
- writeAtomicJson(resultPath, {
4894
+ runPersistence.write(resultPath, {
4531
4895
  lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
4532
4896
  id,
4533
4897
  agent: agentName,
4534
4898
  mode: resultMode,
4535
- success: !checkpointRejected && !stopped && !timedOut && !turnBudgetExceeded && !usageBudgetExceeded && !interrupted && !signalTerminated && results.every((r) => r.success),
4536
- state: checkpointRejected ? "rejected" : stopped || signalTerminated ? "stopped" : timedOut || turnBudgetExceeded || usageBudgetExceeded ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed",
4537
- summary: checkpointRejected ? (statusPayload.error ?? "Checkpoint rejected.") : 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,
4899
+ success: !stopped && !timedOut && !turnBudgetExceeded && !usageBudgetExceeded && !interrupted && !signalTerminated && results.every((r) => r.success),
4900
+ state: stopped || signalTerminated ? "stopped" : timedOut || turnBudgetExceeded || usageBudgetExceeded ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed",
4901
+ 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,
4538
4902
  ...(config.timeoutMs !== undefined ? { timeoutMs: config.timeoutMs } : {}),
4539
4903
  ...(config.deadlineAt !== undefined ? { deadlineAt: config.deadlineAt } : {}),
4540
4904
  ...(statusPayload.turnBudget ? { turnBudget: statusPayload.turnBudget } : {}),
@@ -4543,7 +4907,6 @@ async function runSubagent(
4543
4907
  ...(statusPayload.toolBudget ? { toolBudget: statusPayload.toolBudget } : {}),
4544
4908
  ...(statusPayload.toolBudgetBlocked ? { toolBudgetBlocked: true } : {}),
4545
4909
  ...(statusPayload.usageBudget ? { usageBudget: statusPayload.usageBudget } : {}),
4546
- ...(statusPayload.checkpoint ? { checkpoint: statusPayload.checkpoint } : {}),
4547
4910
  ...(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." } : {}),
4548
4911
  results: results.map((r) => omitUndefinedProperties({
4549
4912
  agent: r.agent,
@@ -4581,6 +4944,7 @@ async function runSubagent(
4581
4944
  runtimeAcknowledgedExtensions: r.runtimeAcknowledgedExtensions,
4582
4945
  runner: r.runner,
4583
4946
  externalProcess: r.externalProcess,
4947
+ externalJob: r.externalJob,
4584
4948
  execution: r.execution,
4585
4949
  review: r.review,
4586
4950
  effects: r.effects,
@@ -4599,13 +4963,12 @@ async function runSubagent(
4599
4963
  capabilityAudit: statusPayload.capabilityAudit,
4600
4964
  ...(config.parentWorkflowRunId ? { parentWorkflowRunId: config.parentWorkflowRunId } : {}),
4601
4965
  ...(config.workflowKey ? { workflowKey: config.workflowKey } : {}),
4602
- exitCode: checkpointRejected || stopped || timedOut || turnBudgetExceeded || usageBudgetExceeded ? 1 : interrupted || results.every((r) => r.success) ? 0 : 1,
4966
+ exitCode: stopped || timedOut || turnBudgetExceeded || usageBudgetExceeded ? 1 : interrupted || results.every((r) => r.success) ? 0 : 1,
4603
4967
  timestamp: runEndedAt,
4604
4968
  durationMs: runEndedAt - overallStartTime,
4605
4969
  totalTokens: statusPayload.totalTokens,
4606
4970
  totalCost: finalTotalCost,
4607
4971
  usageBudget: statusPayload.usageBudget,
4608
- checkpoint: statusPayload.checkpoint,
4609
4972
  truncated,
4610
4973
  artifactsDir,
4611
4974
  cwd,
@@ -4614,6 +4977,7 @@ async function runSubagent(
4614
4977
  launchResolvedExtensions: config.launchResolvedExtensions,
4615
4978
  runtimeAcknowledgedExtensions: singleRuntimeAcknowledgedExtensions,
4616
4979
  sessionId: config.sessionId,
4980
+ completionOwnerId: config.completionOwnerId,
4617
4981
  sessionFile: effectiveSessionFile,
4618
4982
  intercomTarget: config.controlIntercomTarget,
4619
4983
  shareUrl,
@@ -4621,10 +4985,16 @@ async function runSubagent(
4621
4985
  shareError,
4622
4986
  ...(taskIndex !== undefined && { taskIndex }),
4623
4987
  ...(totalTasks !== undefined && { totalTasks }),
4624
- });
4988
+ }, (filePath, payload) => { writeAsyncResultFile(filePath, payload as Record<string, unknown>); });
4989
+ finalResultCommitted = true;
4625
4990
  } catch (err) {
4626
- console.error(`Failed to write result file ${resultPath}:`, err);
4991
+ const message = `Failed to write result file ${resultPath}: ${err instanceof Error ? err.message : String(err)}`;
4992
+ console.error(message, err);
4993
+ statusPayload.state = "failed";
4994
+ statusPayload.error = message;
4995
+ statusPayload.lastUpdate = Date.now();
4627
4996
  }
4997
+ writeStatusPayload();
4628
4998
  appendJsonl(
4629
4999
  eventsPath,
4630
5000
  JSON.stringify({
@@ -4656,7 +5026,11 @@ async function runSubagent(
4656
5026
  sessionFile: effectiveSessionFile,
4657
5027
  shareUrl,
4658
5028
  shareError,
5029
+ }), (filePath, content) => runPersistence.write(filePath, { content }, (_path, payload) => {
5030
+ fs.writeFileSync(_path, (payload as { content: string }).content, "utf-8");
4659
5031
  }));
5032
+ if (runPersistence.pendingCount() === 0) runPersistence.dispose();
5033
+ if (indexPersistence.pendingCount() === 0) indexPersistence.dispose();
4660
5034
  if (config.runnerProcessInstanceId) {
4661
5035
  const writers: Record<string, PiWriterProcessInstanceExitV1[]> = {};
4662
5036
  const expectedWriters: Record<string, number> = {};