pi-subagents 0.31.1 → 0.32.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 (39) hide show
  1. package/CHANGELOG.md +40 -4
  2. package/README.md +97 -7
  3. package/package.json +1 -1
  4. package/skills/pi-subagents/SKILL.md +6 -1
  5. package/src/agents/agent-management.ts +6 -1
  6. package/src/agents/agents.ts +55 -11
  7. package/src/extension/companion-suggestions.ts +359 -0
  8. package/src/extension/config.ts +27 -4
  9. package/src/extension/doctor.ts +2 -0
  10. package/src/extension/fanout-child.ts +1 -0
  11. package/src/extension/index.ts +58 -4
  12. package/src/extension/schemas.ts +2 -2
  13. package/src/runs/background/async-execution.ts +138 -33
  14. package/src/runs/background/async-job-tracker.ts +77 -1
  15. package/src/runs/background/async-status.ts +41 -9
  16. package/src/runs/background/chain-root-attachment.ts +34 -4
  17. package/src/runs/background/control-channel.ts +50 -0
  18. package/src/runs/background/run-status.ts +1 -0
  19. package/src/runs/background/stale-run-reconciler.ts +28 -1
  20. package/src/runs/background/subagent-runner.ts +454 -115
  21. package/src/runs/foreground/chain-execution.ts +29 -7
  22. package/src/runs/foreground/execution.ts +24 -6
  23. package/src/runs/foreground/subagent-executor.ts +209 -35
  24. package/src/runs/shared/acceptance.ts +45 -22
  25. package/src/runs/shared/dynamic-fanout.ts +1 -1
  26. package/src/runs/shared/model-fallback.ts +4 -0
  27. package/src/runs/shared/nested-events.ts +58 -0
  28. package/src/runs/shared/parallel-utils.ts +49 -1
  29. package/src/runs/shared/pi-args.ts +5 -3
  30. package/src/runs/shared/pi-spawn.ts +52 -20
  31. package/src/runs/shared/single-output.ts +2 -0
  32. package/src/runs/shared/subagent-prompt-runtime.ts +2 -2
  33. package/src/runs/shared/worktree.ts +28 -5
  34. package/src/shared/artifacts.ts +15 -1
  35. package/src/shared/fork-context.ts +133 -22
  36. package/src/shared/types.ts +81 -3
  37. package/src/shared/utils.ts +99 -14
  38. package/src/slash/slash-commands.ts +117 -0
  39. package/src/tui/render.ts +16 -4
@@ -4,7 +4,7 @@ import * as path from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import type { Message } from "@earendil-works/pi-ai";
6
6
  import { writeAtomicJson } from "../../shared/atomic-json.ts";
7
- import { consumeInterruptRequest, watchAsyncControlInbox } from "./control-channel.ts";
7
+ import { consumeInterruptRequest, deliverInterruptRequest, deliverTimeoutRequest, watchAsyncControlInbox } from "./control-channel.ts";
8
8
  import { appendJsonl as appendRawJsonl, getArtifactPaths } from "../../shared/artifacts.ts";
9
9
  import { PI_CODING_AGENT_PACKAGE, getPiSpawnCommand, resolveInstalledPiPackageRoot } from "../shared/pi-spawn.ts";
10
10
  import { captureSingleOutputSnapshot, finalizeSingleOutput, formatSavedOutputReference, resolveSingleOutput, type SingleOutputSnapshot } from "../shared/single-output.ts";
@@ -15,14 +15,17 @@ import {
15
15
  type AsyncParallelGroupStatus,
16
16
  type AsyncStatus,
17
17
  type ChainOutputMap,
18
+ type CostSummary,
18
19
  type ModelAttempt,
19
20
  type NestedRouteInfo,
21
+ type NestedRunSummary,
20
22
  type ResolvedControlConfig,
21
23
  type SubagentRunMode,
22
24
  type Usage,
23
25
  type WorkflowGraphSnapshot,
24
26
  DEFAULT_MAX_OUTPUT,
25
27
  type MaxOutputConfig,
28
+ SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
26
29
  truncateOutput,
27
30
  getSubagentDepthEnv,
28
31
  } from "../../shared/types.ts";
@@ -43,15 +46,17 @@ import {
43
46
  mapConcurrent,
44
47
  aggregateParallelOutputs,
45
48
  MAX_PARALLEL_CONCURRENCY,
49
+ DEFAULT_GLOBAL_CONCURRENCY_LIMIT,
50
+ Semaphore,
46
51
  } from "../shared/parallel-utils.ts";
47
- import { buildPiArgs, cleanupTempDir } from "../shared/pi-args.ts";
52
+ import { applyThinkingSuffix, buildPiArgs, cleanupTempDir } from "../shared/pi-args.ts";
48
53
  import { outputEntryFromAsyncResult, resolveOutputReferences } from "../shared/chain-outputs.ts";
49
54
  import { createStructuredOutputRuntime, readStructuredOutput } from "../shared/structured-output.ts";
50
55
  import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection } from "../shared/dynamic-fanout.ts";
51
- import { nestedSummaryFromAsyncStatus, writeNestedEvent } from "../shared/nested-events.ts";
56
+ import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDir, writeNestedEvent } from "../shared/nested-events.ts";
52
57
  import { formatModelAttemptNote, isRetryableModelFailure } from "../shared/model-fallback.ts";
53
58
  import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
54
- import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput } from "../../shared/utils.ts";
59
+ import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput, readStatus } from "../../shared/utils.ts";
55
60
  import { evaluateCompletionMutationGuard } from "../shared/completion-guard.ts";
56
61
  import {
57
62
  createMutatingFailureState,
@@ -101,6 +106,7 @@ interface SubagentRunConfig {
101
106
  piArgv1?: string;
102
107
  worktreeSetupHook?: string;
103
108
  worktreeSetupHookTimeoutMs?: number;
109
+ worktreeBaseDir?: string;
104
110
  controlConfig?: ResolvedControlConfig;
105
111
  controlIntercomTarget?: string;
106
112
  childIntercomTargets?: Array<string | undefined>;
@@ -109,6 +115,10 @@ interface SubagentRunConfig {
109
115
  workflowGraph?: WorkflowGraphSnapshot;
110
116
  nestedRoute?: NestedRouteInfo;
111
117
  nestedSelf?: { parentRunId: string; parentStepIndex?: number; depth: number; path?: Array<{ runId: string; stepIndex?: number; agent?: string }> };
118
+ timeoutMs?: number;
119
+ deadlineAt?: number;
120
+ /** Global cap on simultaneously-running subagent tasks within this run. */
121
+ globalConcurrencyLimit?: number;
112
122
  }
113
123
 
114
124
  interface StepResult {
@@ -118,11 +128,14 @@ interface StepResult {
118
128
  success: boolean;
119
129
  exitCode?: number | null;
120
130
  skipped?: boolean;
131
+ interrupted?: boolean;
132
+ timedOut?: boolean;
121
133
  sessionFile?: string;
122
134
  intercomTarget?: string;
123
135
  model?: string;
124
136
  attemptedModels?: string[];
125
137
  modelAttempts?: ModelAttempt[];
138
+ totalCost?: CostSummary;
126
139
  artifactPaths?: ArtifactPaths;
127
140
  truncated?: boolean;
128
141
  structuredOutput?: unknown;
@@ -237,6 +250,21 @@ function tokenUsageFromAttempts(attempts: ModelAttempt[] | undefined): TokenUsag
237
250
  return total > 0 ? { input, output, total } : null;
238
251
  }
239
252
 
253
+ function costSummaryFromAttempts(attempts: ModelAttempt[] | undefined): CostSummary | undefined {
254
+ if (!attempts || attempts.length === 0) return undefined;
255
+ let inputTokens = 0;
256
+ let outputTokens = 0;
257
+ let costUsd = 0;
258
+ for (const attempt of attempts) {
259
+ inputTokens += attempt.usage?.input ?? 0;
260
+ outputTokens += attempt.usage?.output ?? 0;
261
+ costUsd += attempt.usage?.cost ?? 0;
262
+ }
263
+ return inputTokens > 0 || outputTokens > 0 || costUsd > 0
264
+ ? { inputTokens, outputTokens, costUsd }
265
+ : undefined;
266
+ }
267
+
240
268
  function appendRecentStepOutput(step: RunnerStatusStep, lines: string[]): void {
241
269
  const nonEmpty = lines.filter((line) => line.trim());
242
270
  if (nonEmpty.length === 0) return;
@@ -295,6 +323,7 @@ interface RunPiStreamingResult {
295
323
  error?: string;
296
324
  finalOutput: string;
297
325
  interrupted?: boolean;
326
+ timedOut?: boolean;
298
327
  observedMutationAttempt?: boolean;
299
328
  }
300
329
 
@@ -309,6 +338,8 @@ function runPiStreaming(
309
338
  childEventContext?: ChildEventContext,
310
339
  registerInterrupt?: (interrupt: (() => void) | undefined) => void,
311
340
  onChildEvent?: (event: ChildEvent) => void,
341
+ registerTimeout?: (interrupt: (() => void) | undefined) => void,
342
+ timeoutMessage?: string,
312
343
  ): Promise<RunPiStreamingResult> {
313
344
  return new Promise((resolve) => {
314
345
  const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
@@ -332,6 +363,7 @@ function runPiStreaming(
332
363
  let error: string | undefined;
333
364
  let assistantError: string | undefined;
334
365
  let interrupted = false;
366
+ let timedOut = false;
335
367
  let observedMutationAttempt = false;
336
368
  const rawStdoutLines: string[] = [];
337
369
 
@@ -430,11 +462,13 @@ function runPiStreaming(
430
462
  // a lingering stdio holder after `exit`, or a child that never exits.
431
463
  const FINAL_STOP_GRACE_MS = 1000;
432
464
  const HARD_KILL_MS = 3000;
465
+ const TIMEOUT_HARD_KILL_MS = 3000;
433
466
  let childExited = false;
434
467
  let forcedTerminationSignal = false;
435
468
  let cleanTerminalAssistantStopReceived = false;
436
469
  let finalDrainTimer: NodeJS.Timeout | undefined;
437
470
  let finalHardKillTimer: NodeJS.Timeout | undefined;
471
+ let timeoutHardKillTimer: NodeJS.Timeout | undefined;
438
472
  let settled = false;
439
473
  const clearStdioGuard = attachPostExitStdioGuard(child, { idleMs: 2000, hardMs: 8000 });
440
474
  child.stdout.on("data", (chunk: Buffer) => {
@@ -449,14 +483,25 @@ function runPiStreaming(
449
483
  processStderrText(chunk.toString());
450
484
  });
451
485
  registerInterrupt?.(() => {
452
- if (settled) return;
486
+ if (settled || timedOut) return;
453
487
  interrupted = true;
454
488
  if (!error) error = "Interrupted. Waiting for explicit next action.";
455
489
  trySignalChild(child, "SIGINT");
456
490
  setTimeout(() => {
457
- if (!settled) trySignalChild(child, "SIGTERM");
491
+ if (!settled && !timedOut) trySignalChild(child, "SIGTERM");
458
492
  }, 1000).unref?.();
459
493
  });
494
+ registerTimeout?.(() => {
495
+ if (settled || timedOut) return;
496
+ timedOut = true;
497
+ interrupted = false;
498
+ error = timeoutMessage ?? "Subagent timed out.";
499
+ trySignalChild(child, "SIGTERM");
500
+ timeoutHardKillTimer = setTimeout(() => {
501
+ if (!settled) trySignalChild(child, "SIGKILL");
502
+ }, TIMEOUT_HARD_KILL_MS);
503
+ timeoutHardKillTimer.unref?.();
504
+ });
460
505
  const clearDrainTimers = () => {
461
506
  if (finalDrainTimer) {
462
507
  clearTimeout(finalDrainTimer);
@@ -466,6 +511,10 @@ function runPiStreaming(
466
511
  clearTimeout(finalHardKillTimer);
467
512
  finalHardKillTimer = undefined;
468
513
  }
514
+ if (timeoutHardKillTimer) {
515
+ clearTimeout(timeoutHardKillTimer);
516
+ timeoutHardKillTimer = undefined;
517
+ }
469
518
  };
470
519
  function startFinalDrain(): void {
471
520
  if (childExited || finalDrainTimer || settled) return;
@@ -492,6 +541,7 @@ function runPiStreaming(
492
541
  child.on("close", (exitCode, signal) => {
493
542
  settled = true;
494
543
  registerInterrupt?.(undefined);
544
+ registerTimeout?.(undefined);
495
545
  clearDrainTimers();
496
546
  clearStdioGuard();
497
547
  if (stdoutBuf.trim()) processStdoutLine(stdoutBuf);
@@ -502,13 +552,14 @@ function runPiStreaming(
502
552
  const forcedDrainAfterFinalSuccess = forcedTerminationSignal && cleanTerminalAssistantStopReceived && !finalError;
503
553
  resolve({
504
554
  stderr,
505
- exitCode: interrupted || forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
555
+ exitCode: timedOut ? 1 : interrupted || forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
506
556
  messages,
507
557
  usage,
508
558
  model,
509
- error: interrupted || forcedDrainAfterFinalSuccess ? undefined : finalError,
510
- finalOutput,
559
+ error: timedOut ? (timeoutMessage ?? "Subagent timed out.") : interrupted || forcedDrainAfterFinalSuccess ? undefined : finalError,
560
+ finalOutput: timedOut && !finalOutput.trim() ? (timeoutMessage ?? "Subagent timed out.") : finalOutput,
511
561
  interrupted,
562
+ timedOut,
512
563
  observedMutationAttempt,
513
564
  });
514
565
  });
@@ -516,12 +567,13 @@ function runPiStreaming(
516
567
  child.on("error", (spawnError) => {
517
568
  settled = true;
518
569
  registerInterrupt?.(undefined);
570
+ registerTimeout?.(undefined);
519
571
  clearDrainTimers();
520
572
  clearStdioGuard();
521
573
  outputStream.end();
522
574
  const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
523
575
  const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
524
- resolve({ stderr, exitCode: 1, messages, usage, model, error: error ?? assistantError ?? spawnErrorMessage, finalOutput, observedMutationAttempt });
576
+ resolve({ stderr, exitCode: 1, messages, usage, model, error: timedOut ? (timeoutMessage ?? "Subagent timed out.") : error ?? assistantError ?? spawnErrorMessage, finalOutput: timedOut && !finalOutput.trim() ? (timeoutMessage ?? "Subagent timed out.") : finalOutput, timedOut, observedMutationAttempt });
525
577
  });
526
578
  });
527
579
  }
@@ -649,11 +701,15 @@ interface SingleStepContext {
649
701
  piPackageRoot?: string;
650
702
  piArgv1?: string;
651
703
  registerInterrupt?: (interrupt: (() => void) | undefined) => void;
704
+ registerTimeout?: (interrupt: (() => void) | undefined) => void;
705
+ timeoutSignal?: AbortSignal;
706
+ timeoutMessage?: string;
652
707
  childIntercomTarget?: string;
653
708
  orchestratorIntercomTarget?: string;
654
709
  nestedRoute?: NestedRouteInfo;
655
710
  onAttemptStart?: (attempt: { model?: string; thinking?: string }) => void;
656
711
  onChildEvent?: (event: ChildEvent) => void;
712
+ skipAcceptance?: () => boolean;
657
713
  }
658
714
 
659
715
  /** Run a single pi agent step, returning output and metadata */
@@ -670,6 +726,7 @@ async function runSingleStep(
670
726
  modelAttempts?: ModelAttempt[];
671
727
  artifactPaths?: ArtifactPaths;
672
728
  interrupted?: boolean;
729
+ timedOut?: boolean;
673
730
  sessionFile?: string;
674
731
  intercomTarget?: string;
675
732
  completionGuardTriggered?: boolean;
@@ -679,27 +736,52 @@ async function runSingleStep(
679
736
  acceptance?: import("../../shared/types.ts").AcceptanceLedger;
680
737
  }> {
681
738
  if (step.importAsyncRoot) {
682
- const imported = await waitForImportedAsyncRoot(step.importAsyncRoot);
739
+ let importTimedOut = false;
740
+ ctx.registerTimeout?.(() => {
741
+ importTimedOut = true;
742
+ let pid: number | undefined;
743
+ try {
744
+ pid = readStatus(step.importAsyncRoot!.asyncDir)?.pid;
745
+ } catch {
746
+ pid = undefined;
747
+ }
748
+ try {
749
+ deliverTimeoutRequest({ asyncDir: step.importAsyncRoot!.asyncDir, pid, source: "ancestor-timeout" });
750
+ } catch {
751
+ // The parent runner's own timeout result is authoritative for the attached step.
752
+ }
753
+ });
683
754
  try {
684
- fs.writeFileSync(ctx.outputFile, imported.output, "utf-8");
685
- } catch {
686
- // Output files are observability only for imported roots.
755
+ const imported = await waitForImportedAsyncRoot(step.importAsyncRoot, {
756
+ shouldAbort: () => importTimedOut || ctx.timeoutSignal?.aborted === true || ctx.skipAcceptance?.() === true,
757
+ timeoutMessage: ctx.timeoutMessage,
758
+ });
759
+ try {
760
+ fs.writeFileSync(ctx.outputFile, imported.output, "utf-8");
761
+ } catch {
762
+ // Output files are observability only for imported roots.
763
+ }
764
+ const timedOut = importTimedOut || imported.timedOut === true || ctx.timeoutSignal?.aborted === true || ctx.skipAcceptance?.() === true;
765
+ return {
766
+ agent: imported.agent,
767
+ output: timedOut ? ctx.timeoutMessage ?? "Subagent timed out." : imported.output,
768
+ exitCode: timedOut ? 1 : imported.exitCode,
769
+ error: timedOut ? ctx.timeoutMessage ?? "Subagent timed out." : imported.error,
770
+ timedOut: timedOut ? true : undefined,
771
+ sessionFile: imported.sessionFile,
772
+ intercomTarget: imported.intercomTarget,
773
+ model: imported.model,
774
+ attemptedModels: imported.attemptedModels,
775
+ modelAttempts: imported.modelAttempts,
776
+ totalCost: imported.totalCost,
777
+ structuredOutput: timedOut ? undefined : imported.structuredOutput,
778
+ structuredOutputPath: timedOut ? undefined : imported.structuredOutputPath,
779
+ structuredOutputSchemaPath: timedOut ? undefined : imported.structuredOutputSchemaPath,
780
+ acceptance: timedOut ? undefined : imported.acceptance,
781
+ };
782
+ } finally {
783
+ ctx.registerTimeout?.(undefined);
687
784
  }
688
- return {
689
- agent: imported.agent,
690
- output: imported.output,
691
- exitCode: imported.exitCode,
692
- error: imported.error,
693
- sessionFile: imported.sessionFile,
694
- intercomTarget: imported.intercomTarget,
695
- model: imported.model,
696
- attemptedModels: imported.attemptedModels,
697
- modelAttempts: imported.modelAttempts,
698
- structuredOutput: imported.structuredOutput,
699
- structuredOutputPath: imported.structuredOutputPath,
700
- structuredOutputSchemaPath: imported.structuredOutputSchemaPath,
701
- acceptance: imported.acceptance,
702
- };
703
785
  }
704
786
 
705
787
  const effectiveStructuredOutput = step.structuredOutput ?? (step.structuredOutputSchema
@@ -740,6 +822,7 @@ async function runSingleStep(
740
822
  let completionGuardTriggeredFinal = false;
741
823
 
742
824
  for (let index = 0; index < candidates.length; index++) {
825
+ if (ctx.timeoutSignal?.aborted || ctx.skipAcceptance?.()) break;
743
826
  const candidate = candidates[index];
744
827
  ctx.onAttemptStart?.({ model: candidate, thinking: resolveEffectiveThinking(candidate, step.thinking) });
745
828
  const outputSnapshot = captureSingleOutputSnapshot(step.outputPath);
@@ -791,13 +874,21 @@ async function runSingleStep(
791
874
  { eventsPath, runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
792
875
  ctx.registerInterrupt,
793
876
  ctx.onChildEvent,
877
+ ctx.registerTimeout,
878
+ ctx.timeoutMessage,
794
879
  );
795
880
  cleanupTempDir(tempDir);
796
881
 
797
882
  const hiddenError = run.exitCode === 0 && !run.error ? detectSubagentError(run.messages) : null;
883
+ const missingStructuredOutput = effectiveStructuredOutput
884
+ ? !fs.existsSync(effectiveStructuredOutput.outputPath)
885
+ : false;
886
+ const emptyOutputError = run.exitCode === 0 && !run.error && !hiddenError?.hasError && !run.finalOutput.trim() && (!effectiveStructuredOutput || missingStructuredOutput)
887
+ ? "Subagent produced no output (possible model cold-start or empty response)."
888
+ : undefined;
798
889
  let structuredOutput: unknown;
799
890
  let structuredError: string | undefined;
800
- if (effectiveStructuredOutput && run.exitCode === 0 && !run.error && !hiddenError?.hasError) {
891
+ if (effectiveStructuredOutput && run.exitCode === 0 && !run.error && !hiddenError?.hasError && !emptyOutputError) {
801
892
  const structured = readStructuredOutput({
802
893
  schema: effectiveStructuredOutput.schema,
803
894
  schemaPath: effectiveStructuredOutput.schemaPath,
@@ -806,7 +897,7 @@ async function runSingleStep(
806
897
  if (structured.error) structuredError = structured.error;
807
898
  else structuredOutput = structured.value;
808
899
  }
809
- const completionGuard = run.exitCode === 0 && !run.error && !hiddenError?.hasError && step.completionGuard !== false
900
+ const completionGuard = run.exitCode === 0 && !run.error && !hiddenError?.hasError && !emptyOutputError && step.completionGuard !== false
810
901
  ? evaluateCompletionMutationGuard({
811
902
  agent: step.agent,
812
903
  task: taskForCompletionGuard,
@@ -825,16 +916,18 @@ async function runSingleStep(
825
916
  ? 1
826
917
  : hiddenError?.hasError
827
918
  ? (hiddenError.exitCode ?? 1)
828
- : run.error && run.exitCode === 0
919
+ : emptyOutputError
829
920
  ? 1
830
- : run.exitCode;
921
+ : run.error && run.exitCode === 0
922
+ ? 1
923
+ : run.exitCode;
831
924
  const error = completionGuardError
832
925
  ?? structuredError
833
926
  ?? (hiddenError?.hasError
834
927
  ? hiddenError.details
835
928
  ? `${hiddenError.errorType} failed (exit ${effectiveExitCode}): ${hiddenError.details}`
836
929
  : `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
837
- : run.error || (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined));
930
+ : emptyOutputError ?? (run.error || (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined)));
838
931
  const attempt: ModelAttempt = {
839
932
  model: candidate ?? run.model ?? step.model ?? "default",
840
933
  success: effectiveExitCode === 0 && !error,
@@ -847,6 +940,7 @@ async function runSingleStep(
847
940
  completionGuardTriggeredFinal = completionGuardTriggered;
848
941
  finalOutputSnapshot = outputSnapshot;
849
942
  finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error, structuredOutput } as RunPiStreamingResult & { structuredOutput?: unknown };
943
+ if (run.timedOut || ctx.timeoutSignal?.aborted || ctx.skipAcceptance?.()) break;
850
944
  if (attempt.success || completionGuardTriggered) break;
851
945
  if (!isRetryableModelFailure(error) || index === candidates.length - 1) break;
852
946
  attemptNotes.push(formatModelAttemptNote(attempt, candidates[index + 1]));
@@ -874,19 +968,25 @@ async function runSingleStep(
874
968
  saveError: resolvedOutput.saveError,
875
969
  });
876
970
  outputForSummary = finalizedOutput.displayOutput;
877
- const acceptance = step.effectiveAcceptance
971
+ const acceptance = step.effectiveAcceptance && !ctx.timeoutSignal?.aborted && !ctx.skipAcceptance?.()
878
972
  ? await evaluateAcceptance({
879
973
  acceptance: step.effectiveAcceptance,
880
974
  output: outputForAcceptance,
881
975
  cwd: step.cwd ?? ctx.cwd,
976
+ signal: ctx.timeoutSignal,
977
+ abortMessage: ctx.timeoutMessage ?? "Subagent timed out.",
882
978
  })
883
979
  : undefined;
884
- const acceptanceFailure = acceptance ? acceptanceFailureMessage(acceptance) : undefined;
885
- const acceptanceCanFailRun = acceptanceFailure && acceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted;
886
- const effectiveFinalExitCode = acceptanceCanFailRun ? 1 : finalResult?.exitCode ?? 1;
887
- const effectiveFinalError = acceptanceCanFailRun
888
- ? (finalResult?.error ? `${finalResult.error}\n${acceptanceFailure}` : acceptanceFailure)
889
- : finalResult?.error;
980
+ const timedOutAfterAcceptance = finalResult?.timedOut === true || ctx.timeoutSignal?.aborted === true || ctx.skipAcceptance?.() === true;
981
+ const effectiveAcceptance = timedOutAfterAcceptance ? undefined : acceptance;
982
+ const acceptanceFailure = effectiveAcceptance ? acceptanceFailureMessage(effectiveAcceptance) : undefined;
983
+ const acceptanceCanFailRun = acceptanceFailure && effectiveAcceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted && !timedOutAfterAcceptance;
984
+ const effectiveFinalExitCode = timedOutAfterAcceptance ? 1 : acceptanceCanFailRun ? 1 : finalResult?.exitCode ?? 1;
985
+ const effectiveFinalError = timedOutAfterAcceptance
986
+ ? ctx.timeoutMessage ?? "Subagent timed out."
987
+ : acceptanceCanFailRun
988
+ ? (finalResult?.error ? `${finalResult.error}\n${acceptanceFailure}` : acceptanceFailure)
989
+ : finalResult?.error;
890
990
 
891
991
  if (artifactPaths && ctx.artifactConfig?.enabled !== false) {
892
992
  if (ctx.artifactConfig?.includeOutput !== false) {
@@ -921,13 +1021,15 @@ async function runSingleStep(
921
1021
  model: finalResult?.model,
922
1022
  attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
923
1023
  modelAttempts,
1024
+ totalCost: costSummaryFromAttempts(modelAttempts),
924
1025
  artifactPaths,
925
- interrupted: finalResult?.interrupted,
1026
+ interrupted: timedOutAfterAcceptance ? false : finalResult?.interrupted,
1027
+ timedOut: timedOutAfterAcceptance ? true : finalResult?.timedOut,
926
1028
  completionGuardTriggered: completionGuardTriggeredFinal,
927
- structuredOutput: (finalResult as (RunPiStreamingResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput,
928
- structuredOutputPath: effectiveStructuredOutput?.outputPath,
929
- structuredOutputSchemaPath: effectiveStructuredOutput?.schemaPath,
930
- acceptance,
1029
+ structuredOutput: timedOutAfterAcceptance ? undefined : (finalResult as (RunPiStreamingResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput,
1030
+ structuredOutputPath: timedOutAfterAcceptance ? undefined : effectiveStructuredOutput?.outputPath,
1031
+ structuredOutputSchemaPath: timedOutAfterAcceptance ? undefined : effectiveStructuredOutput?.schemaPath,
1032
+ acceptance: effectiveAcceptance,
931
1033
  };
932
1034
  }
933
1035
 
@@ -1056,9 +1158,12 @@ function ensureParallelProgressFile(cwd: string, group: Extract<RunnerStep, { pa
1056
1158
  writeInitialProgressFile(cwd);
1057
1159
  }
1058
1160
 
1161
+ type SingleStepResult = Awaited<ReturnType<typeof runSingleStep>>;
1162
+
1059
1163
  async function runSubagent(config: SubagentRunConfig): Promise<void> {
1060
1164
  const { id, steps, resultPath, cwd, placeholder, taskIndex, totalTasks, maxOutput, artifactsDir, artifactConfig } =
1061
1165
  config;
1166
+ const globalSemaphore = new Semaphore(config.globalConcurrencyLimit ?? DEFAULT_GLOBAL_CONCURRENCY_LIMIT);
1062
1167
  let previousOutput = "";
1063
1168
  const outputs: ChainOutputMap = {};
1064
1169
  const results: StepResult[] = [];
@@ -1069,10 +1174,15 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1069
1174
  const eventsPath = path.join(asyncDir, "events.jsonl");
1070
1175
  const logPath = path.join(asyncDir, `subagent-log-${id}.md`);
1071
1176
  const controlConfig = config.controlConfig ?? DEFAULT_CONTROL_CONFIG;
1072
- let activeChildInterrupt: (() => void) | undefined;
1177
+ const activeChildInterrupts = new Map<number, () => void>();
1178
+ const activeChildTimeouts = new Map<number, () => void>();
1073
1179
  let interrupted = false;
1074
1180
  let currentActivityState: ActivityState | undefined;
1075
1181
  let activityTimer: NodeJS.Timeout | undefined;
1182
+ let timeoutTimer: NodeJS.Timeout | undefined;
1183
+ let timedOut = false;
1184
+ const timeoutMessage = config.timeoutMs !== undefined ? `Subagent timed out after ${config.timeoutMs}ms.` : undefined;
1185
+ const timeoutAbortController = new AbortController();
1076
1186
  let previousCumulativeTokens: TokenUsage = { input: 0, output: 0, total: 0 };
1077
1187
  let latestSessionFile: string | undefined;
1078
1188
 
@@ -1138,6 +1248,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1138
1248
  || shareEnabled
1139
1249
  || flatSteps.some((step) => Boolean(step.sessionFile));
1140
1250
  const statusPayload: RunnerStatusPayload = {
1251
+ lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
1141
1252
  runId: id,
1142
1253
  ...(config.sessionId ? { sessionId: config.sessionId } : {}),
1143
1254
  mode: config.resultMode ?? (flatSteps.length > 1 ? "chain" : "single"),
@@ -1145,6 +1256,8 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1145
1256
  lastActivityAt: overallStartTime,
1146
1257
  startedAt: overallStartTime,
1147
1258
  lastUpdate: overallStartTime,
1259
+ ...(config.timeoutMs !== undefined ? { timeoutMs: config.timeoutMs } : {}),
1260
+ ...(config.deadlineAt !== undefined ? { deadlineAt: config.deadlineAt } : {}),
1148
1261
  pid: process.pid,
1149
1262
  cwd,
1150
1263
  currentStep: 0,
@@ -1216,6 +1329,110 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1216
1329
  writeAtomicJson(statusPath, statusPayload);
1217
1330
  emitNestedSelfEvent(statusPayload.state === "running" || statusPayload.state === "queued" ? "subagent.nested.updated" : "subagent.nested.completed");
1218
1331
  };
1332
+ const registerStepInterrupt = (flatIndex: number, interrupt: (() => void) | undefined): void => {
1333
+ if (!interrupt) {
1334
+ activeChildInterrupts.delete(flatIndex);
1335
+ return;
1336
+ }
1337
+ activeChildInterrupts.set(flatIndex, interrupt);
1338
+ if (interrupted) interrupt();
1339
+ };
1340
+ const registerStepTimeout = (flatIndex: number, interrupt: (() => void) | undefined): void => {
1341
+ if (!interrupt) {
1342
+ activeChildTimeouts.delete(flatIndex);
1343
+ return;
1344
+ }
1345
+ activeChildTimeouts.set(flatIndex, interrupt);
1346
+ if (timedOut) interrupt();
1347
+ };
1348
+ const interruptActiveChildren = (): void => {
1349
+ for (const interrupt of [...activeChildInterrupts.values()]) interrupt();
1350
+ };
1351
+ const timeoutActiveChildren = (): void => {
1352
+ for (const interrupt of [...activeChildTimeouts.values()]) interrupt();
1353
+ };
1354
+ const nestedRuns = function* (children: NestedRunSummary[] | undefined): Generator<NestedRunSummary> {
1355
+ for (const child of children ?? []) {
1356
+ yield child;
1357
+ yield* nestedRuns(child.children);
1358
+ yield* nestedRuns(child.steps?.flatMap((step) => step.children ?? []));
1359
+ }
1360
+ };
1361
+ const interruptNestedAsyncDescendants = (): void => {
1362
+ if (!config.nestedRoute) return;
1363
+ let registry: ReturnType<typeof projectNestedEvents>;
1364
+ try {
1365
+ registry = projectNestedEvents(config.nestedRoute);
1366
+ } catch (error) {
1367
+ appendJsonl(eventsPath, JSON.stringify({
1368
+ type: "subagent.nested.interrupt_failed",
1369
+ ts: Date.now(),
1370
+ runId: id,
1371
+ message: error instanceof Error ? error.message : String(error),
1372
+ }));
1373
+ return;
1374
+ }
1375
+ for (const run of nestedRuns(registry.children)) {
1376
+ if (run.state !== "running" && run.state !== "queued") continue;
1377
+ const nestedAsyncDir = run.asyncDir ?? resolveNestedAsyncDir(config.nestedRoute.rootRunId, run);
1378
+ if (!nestedAsyncDir) continue;
1379
+ try {
1380
+ deliverInterruptRequest({ asyncDir: nestedAsyncDir, pid: run.pid, source: "ancestor-interrupt" });
1381
+ } catch (error) {
1382
+ appendJsonl(eventsPath, JSON.stringify({
1383
+ type: "subagent.nested.interrupt_failed",
1384
+ ts: Date.now(),
1385
+ runId: id,
1386
+ targetRunId: run.id,
1387
+ message: error instanceof Error ? error.message : String(error),
1388
+ }));
1389
+ }
1390
+ }
1391
+ };
1392
+ const timeoutNestedAsyncDescendants = (): void => {
1393
+ if (!config.nestedRoute) return;
1394
+ let registry: ReturnType<typeof projectNestedEvents>;
1395
+ try {
1396
+ registry = projectNestedEvents(config.nestedRoute);
1397
+ } catch (error) {
1398
+ appendJsonl(eventsPath, JSON.stringify({
1399
+ type: "subagent.nested.timeout_failed",
1400
+ ts: Date.now(),
1401
+ runId: id,
1402
+ message: error instanceof Error ? error.message : String(error),
1403
+ }));
1404
+ return;
1405
+ }
1406
+ for (const run of nestedRuns(registry.children)) {
1407
+ if (run.state !== "running" && run.state !== "queued") continue;
1408
+ const nestedAsyncDir = run.asyncDir ?? resolveNestedAsyncDir(config.nestedRoute.rootRunId, run);
1409
+ if (!nestedAsyncDir) continue;
1410
+ try {
1411
+ deliverTimeoutRequest({ asyncDir: nestedAsyncDir, pid: run.pid, source: "ancestor-timeout" });
1412
+ } catch (error) {
1413
+ appendJsonl(eventsPath, JSON.stringify({
1414
+ type: "subagent.nested.timeout_failed",
1415
+ ts: Date.now(),
1416
+ runId: id,
1417
+ targetRunId: run.id,
1418
+ message: error instanceof Error ? error.message : String(error),
1419
+ }));
1420
+ }
1421
+ }
1422
+ };
1423
+ const pausedStepResult = (agent: string): SingleStepResult => ({
1424
+ agent,
1425
+ output: "Paused after interrupt. Waiting for explicit next action.",
1426
+ exitCode: 0,
1427
+ interrupted: true,
1428
+ });
1429
+ const timedOutStepResult = (agent: string): SingleStepResult => ({
1430
+ agent,
1431
+ output: timeoutMessage ?? "Subagent timed out.",
1432
+ error: timeoutMessage ?? "Subagent timed out.",
1433
+ exitCode: 1,
1434
+ timedOut: true,
1435
+ });
1219
1436
  const consumePendingAppendRequests = (): void => {
1220
1437
  if (statusPayload.mode !== "chain" || statusPayload.state !== "running") return;
1221
1438
  const requests = consumeChainAppendRequests(asyncDir);
@@ -1531,17 +1748,59 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1531
1748
  ts: now,
1532
1749
  runId: id,
1533
1750
  }));
1534
- activeChildInterrupt?.();
1751
+ interruptNestedAsyncDescendants();
1752
+ interruptActiveChildren();
1753
+ };
1754
+ const timeoutRunner = () => {
1755
+ if (timedOut || interrupted || statusPayload.state !== "running") return;
1756
+ timedOut = true;
1757
+ const now = Date.now();
1758
+ const message = timeoutMessage ?? "Subagent timed out.";
1759
+ statusPayload.state = "failed";
1760
+ statusPayload.timedOut = true;
1761
+ statusPayload.error = message;
1762
+ currentActivityState = undefined;
1763
+ statusPayload.activityState = undefined;
1764
+ statusPayload.lastUpdate = now;
1765
+ for (const step of statusPayload.steps) {
1766
+ if (step.status !== "running" && step.status !== "pending") continue;
1767
+ step.status = "failed";
1768
+ step.error = message;
1769
+ step.exitCode = 1;
1770
+ step.timedOut = true;
1771
+ step.activityState = undefined;
1772
+ step.endedAt = now;
1773
+ step.durationMs = step.startedAt ? now - step.startedAt : 0;
1774
+ step.lastActivityAt = now;
1775
+ }
1776
+ writeStatusPayload();
1777
+ appendJsonl(eventsPath, JSON.stringify({
1778
+ type: "subagent.run.timed_out",
1779
+ ts: now,
1780
+ runId: id,
1781
+ timeoutMs: config.timeoutMs,
1782
+ deadlineAt: config.deadlineAt,
1783
+ message,
1784
+ }));
1785
+ timeoutAbortController.abort();
1786
+ timeoutNestedAsyncDescendants();
1787
+ timeoutActiveChildren();
1535
1788
  };
1536
1789
  process.on(ASYNC_INTERRUPT_SIGNAL, interruptRunner);
1537
1790
  // Portable control inbox: the parent drops an interrupt request file here when
1538
1791
  // it cannot deliver the OS signal (e.g. ENOSYS on Windows). Routes into the
1539
1792
  // same graceful interruptRunner() so stop/steer work on every platform.
1540
- const disposeControlInbox = watchAsyncControlInbox(asyncDir, { onInterrupt: interruptRunner });
1793
+ const disposeControlInbox = watchAsyncControlInbox(asyncDir, { onInterrupt: interruptRunner, onTimeout: timeoutRunner });
1794
+ if (config.deadlineAt !== undefined) {
1795
+ const remainingMs = Math.max(0, config.deadlineAt - Date.now());
1796
+ timeoutTimer = setTimeout(timeoutRunner, remainingMs);
1797
+ timeoutTimer.unref?.();
1798
+ }
1541
1799
  appendJsonl(
1542
1800
  eventsPath,
1543
1801
  JSON.stringify({
1544
1802
  type: "subagent.run.started",
1803
+ lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
1545
1804
  ts: overallStartTime,
1546
1805
  runId: id,
1547
1806
  mode: statusPayload.mode,
@@ -1554,7 +1813,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1554
1813
  let stepCursor = 0;
1555
1814
 
1556
1815
  while (true) {
1557
- if (interrupted) break;
1816
+ if (interrupted || timedOut) break;
1558
1817
  consumePendingAppendRequests();
1559
1818
  if (stepCursor >= steps.length) break;
1560
1819
  const stepIndex = stepCursor++;
@@ -1606,7 +1865,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1606
1865
  placeholder.durationMs = 0;
1607
1866
  }
1608
1867
  previousOutput = "Dynamic fanout produced 0 results.";
1609
- const groupAcceptance = step.effectiveAcceptance?.explicit
1868
+ const groupAcceptance = step.effectiveAcceptance?.explicit && !timedOut
1610
1869
  ? await evaluateAcceptance({
1611
1870
  acceptance: step.effectiveAcceptance,
1612
1871
  output: "",
@@ -1615,38 +1874,55 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1615
1874
  notes: "Dynamic fanout produced 0 results.",
1616
1875
  }),
1617
1876
  cwd,
1877
+ signal: timeoutAbortController.signal,
1878
+ abortMessage: timeoutMessage ?? "Subagent timed out.",
1618
1879
  })
1619
1880
  : undefined;
1620
- if (placeholder && groupAcceptance) placeholder.acceptance = groupAcceptance;
1621
- const groupAcceptanceFailure = groupAcceptance ? acceptanceFailureMessage(groupAcceptance) : undefined;
1622
- if (groupAcceptanceFailure) {
1881
+ const groupTimedOut = timedOut || timeoutAbortController.signal.aborted;
1882
+ const effectiveGroupAcceptance = groupTimedOut ? undefined : groupAcceptance;
1883
+ if (placeholder && effectiveGroupAcceptance) placeholder.acceptance = effectiveGroupAcceptance;
1884
+ const groupAcceptanceFailure = effectiveGroupAcceptance ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
1885
+ if (groupTimedOut || groupAcceptanceFailure) {
1886
+ const errorMessage = groupTimedOut ? timeoutMessage ?? "Subagent timed out." : groupAcceptanceFailure!;
1623
1887
  statusPayload.state = "failed";
1624
- statusPayload.error = groupAcceptanceFailure;
1888
+ statusPayload.error = errorMessage;
1625
1889
  if (placeholder) {
1626
1890
  placeholder.status = "failed";
1627
- placeholder.error = groupAcceptanceFailure;
1891
+ placeholder.error = errorMessage;
1628
1892
  placeholder.exitCode = 1;
1893
+ placeholder.timedOut = groupTimedOut ? true : undefined;
1629
1894
  }
1630
- markDynamicGraphGroup(stepIndex, "failed", groupAcceptanceFailure, groupAcceptance);
1631
- statusPayload.lastUpdate = now;
1895
+ markDynamicGraphGroup(stepIndex, "failed", errorMessage, effectiveGroupAcceptance);
1896
+ statusPayload.lastUpdate = Date.now();
1632
1897
  writeStatusPayload();
1633
- results.push({ agent: step.parallel.agent, output: groupAcceptanceFailure, error: groupAcceptanceFailure, success: false, exitCode: 1, acceptance: groupAcceptance });
1898
+ results.push({ agent: step.parallel.agent, output: errorMessage, error: errorMessage, success: false, exitCode: 1, timedOut: groupTimedOut ? true : undefined, acceptance: effectiveGroupAcceptance });
1634
1899
  break;
1635
1900
  }
1636
1901
  flatIndex++;
1637
1902
  statusPayload.lastUpdate = now;
1638
- markDynamicGraphGroup(stepIndex, "completed", undefined, groupAcceptance);
1903
+ markDynamicGraphGroup(stepIndex, "completed", undefined, effectiveGroupAcceptance);
1639
1904
  writeStatusPayload();
1640
1905
  continue;
1641
1906
  }
1642
1907
 
1643
- const dynamicSteps = materialized.parallel.map((task, itemIndex) => ({
1644
- ...step.parallel,
1645
- task: task.task ?? step.parallel.task,
1646
- label: task.label ?? step.parallel.label,
1647
- structuredOutput: undefined,
1648
- structuredOutputSchema: step.parallel.structuredOutputSchema ?? step.parallel.structuredOutput?.schema,
1649
- }));
1908
+ const dynamicSteps = materialized.parallel.map((task, itemIndex) => {
1909
+ const thinkingOverride = step.thinkingOverrides?.[itemIndex];
1910
+ const model = thinkingOverride ? applyThinkingSuffix(step.parallel.model, thinkingOverride, true) : step.parallel.model;
1911
+ const thinking = thinkingOverride ? resolveEffectiveThinking(model, thinkingOverride) : undefined;
1912
+ return {
1913
+ ...step.parallel,
1914
+ task: task.task ?? step.parallel.task,
1915
+ label: task.label ?? step.parallel.label,
1916
+ ...(step.sessionFiles?.[itemIndex] ? { sessionFile: step.sessionFiles[itemIndex] } : {}),
1917
+ ...(thinkingOverride ? {
1918
+ ...(model ? { model } : {}),
1919
+ ...(thinking ? { thinking } : {}),
1920
+ ...(step.parallel.modelCandidates ? { modelCandidates: step.parallel.modelCandidates.map((candidate) => applyThinkingSuffix(candidate, thinkingOverride, true)) } : {}),
1921
+ } : {}),
1922
+ structuredOutput: undefined,
1923
+ structuredOutputSchema: step.parallel.structuredOutputSchema ?? step.parallel.structuredOutput?.schema,
1924
+ };
1925
+ });
1650
1926
  const dynamicStatusSteps: RunnerStatusStep[] = dynamicSteps.map((task) => ({
1651
1927
  agent: task.agent,
1652
1928
  phase: task.phase ?? step.phase,
@@ -1710,6 +1986,8 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1710
1986
  let aborted = false;
1711
1987
  const parallelResults = await mapConcurrent(dynamicSteps, concurrency, async (task, taskIdx) => {
1712
1988
  const fi = groupStartFlatIndex + taskIdx;
1989
+ if (timedOut) return timedOutStepResult(task.agent);
1990
+ if (interrupted) return pausedStepResult(task.agent);
1713
1991
  if (aborted && failFast) {
1714
1992
  const skippedAt = Date.now();
1715
1993
  statusPayload.steps[fi].status = "failed";
@@ -1747,22 +2025,27 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1747
2025
  childIntercomTarget: config.childIntercomTargets?.[fi],
1748
2026
  orchestratorIntercomTarget: config.controlIntercomTarget,
1749
2027
  nestedRoute: config.nestedRoute,
1750
- registerInterrupt: (interrupt) => {
1751
- activeChildInterrupt = interrupt;
1752
- },
2028
+ registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
2029
+ registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
2030
+ timeoutSignal: timeoutAbortController.signal,
2031
+ timeoutMessage,
1753
2032
  onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking),
1754
2033
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
2034
+ skipAcceptance: () => timedOut,
1755
2035
  });
1756
2036
  const taskEndTime = Date.now();
1757
- statusPayload.steps[fi].status = singleResult.exitCode === 0 ? "complete" : "failed";
2037
+ const childInterrupted = singleResult.interrupted === true;
2038
+ statusPayload.steps[fi].status = timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
1758
2039
  statusPayload.steps[fi].endedAt = taskEndTime;
1759
2040
  statusPayload.steps[fi].durationMs = taskEndTime - taskStartTime;
1760
- statusPayload.steps[fi].exitCode = singleResult.exitCode;
2041
+ statusPayload.steps[fi].exitCode = timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
2042
+ statusPayload.steps[fi].timedOut = timedOut || singleResult.timedOut ? true : undefined;
1761
2043
  statusPayload.steps[fi].model = singleResult.model;
1762
2044
  statusPayload.steps[fi].thinking = resolveEffectiveThinking(singleResult.model, statusPayload.steps[fi].thinking);
1763
2045
  statusPayload.steps[fi].attemptedModels = singleResult.attemptedModels;
1764
2046
  statusPayload.steps[fi].modelAttempts = singleResult.modelAttempts;
1765
- statusPayload.steps[fi].error = singleResult.error;
2047
+ statusPayload.steps[fi].totalCost = singleResult.totalCost;
2048
+ statusPayload.steps[fi].error = timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
1766
2049
  statusPayload.steps[fi].structuredOutput = singleResult.structuredOutput;
1767
2050
  statusPayload.steps[fi].structuredOutputPath = singleResult.structuredOutputPath;
1768
2051
  statusPayload.steps[fi].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
@@ -1770,13 +2053,13 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1770
2053
  statusPayload.lastUpdate = taskEndTime;
1771
2054
  writeStatusPayload();
1772
2055
  appendJsonl(eventsPath, JSON.stringify({
1773
- type: singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
2056
+ type: timedOut ? "subagent.step.failed" : childInterrupted ? "subagent.step.paused" : singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
1774
2057
  ts: taskEndTime, runId: id, stepIndex: fi, agent: task.agent,
1775
- exitCode: singleResult.exitCode, durationMs: taskEndTime - taskStartTime,
2058
+ exitCode: timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode, durationMs: taskEndTime - taskStartTime,
1776
2059
  }));
1777
2060
  if (singleResult.exitCode !== 0 && failFast) aborted = true;
1778
- return { ...singleResult, skipped: false };
1779
- });
2061
+ return timedOut ? { ...singleResult, output: timeoutMessage ?? "Subagent timed out.", error: timeoutMessage ?? "Subagent timed out.", exitCode: 1, interrupted: false, timedOut: true, skipped: false } : { ...singleResult, skipped: false };
2062
+ }, globalSemaphore);
1780
2063
 
1781
2064
  flatIndex += dynamicSteps.length;
1782
2065
  for (const pr of parallelResults) {
@@ -1784,14 +2067,17 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1784
2067
  agent: pr.agent,
1785
2068
  output: pr.output,
1786
2069
  error: pr.error,
1787
- success: pr.exitCode === 0,
1788
- exitCode: pr.exitCode,
2070
+ success: pr.interrupted !== true && pr.exitCode === 0,
2071
+ exitCode: pr.interrupted === true ? 0 : pr.exitCode,
1789
2072
  skipped: pr.skipped,
2073
+ interrupted: pr.interrupted,
2074
+ timedOut: pr.timedOut,
1790
2075
  sessionFile: pr.sessionFile,
1791
2076
  intercomTarget: pr.intercomTarget,
1792
2077
  model: pr.model,
1793
2078
  attemptedModels: pr.attemptedModels,
1794
2079
  modelAttempts: pr.modelAttempts,
2080
+ totalCost: pr.totalCost,
1795
2081
  artifactPaths: pr.artifactPaths,
1796
2082
  structuredOutput: pr.structuredOutput,
1797
2083
  structuredOutputPath: pr.structuredOutputPath,
@@ -1811,7 +2097,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1811
2097
  stepIndex,
1812
2098
  };
1813
2099
  statusPayload.outputs = outputs;
1814
- const groupAcceptance = step.effectiveAcceptance
2100
+ const groupAcceptance = step.effectiveAcceptance && !timedOut
1815
2101
  ? await evaluateAcceptance({
1816
2102
  acceptance: step.effectiveAcceptance,
1817
2103
  output: "",
@@ -1820,21 +2106,27 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1820
2106
  notes: `Dynamic fanout collected ${collection.length} result(s) into ${step.collect.as}.`,
1821
2107
  }),
1822
2108
  cwd,
2109
+ signal: timeoutAbortController.signal,
2110
+ abortMessage: timeoutMessage ?? "Subagent timed out.",
1823
2111
  })
1824
2112
  : undefined;
1825
- const groupAcceptanceFailure = groupAcceptance ? acceptanceFailureMessage(groupAcceptance) : undefined;
1826
- markDynamicGraphGroup(stepIndex, groupAcceptanceFailure ? "failed" : "completed", groupAcceptanceFailure, groupAcceptance);
1827
- if (groupAcceptanceFailure) {
2113
+ const groupTimedOut = timedOut || timeoutAbortController.signal.aborted;
2114
+ const effectiveGroupAcceptance = groupTimedOut ? undefined : groupAcceptance;
2115
+ const groupAcceptanceFailure = effectiveGroupAcceptance ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
2116
+ const groupError = groupTimedOut ? timeoutMessage ?? "Subagent timed out." : groupAcceptanceFailure;
2117
+ markDynamicGraphGroup(stepIndex, groupError ? "failed" : "completed", groupError, effectiveGroupAcceptance);
2118
+ if (groupError) {
1828
2119
  results.push({
1829
2120
  agent: step.parallel.agent,
1830
- output: groupAcceptanceFailure,
1831
- error: groupAcceptanceFailure,
2121
+ output: groupError,
2122
+ error: groupError,
1832
2123
  success: false,
1833
2124
  exitCode: 1,
2125
+ timedOut: groupTimedOut ? true : undefined,
1834
2126
  structuredOutput: collection,
1835
- acceptance: groupAcceptance,
2127
+ acceptance: effectiveGroupAcceptance,
1836
2128
  });
1837
- statusPayload.error = groupAcceptanceFailure;
2129
+ statusPayload.error = groupError;
1838
2130
  }
1839
2131
  } catch (error) {
1840
2132
  const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
@@ -1900,6 +2192,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1900
2192
  setupHook: config.worktreeSetupHook
1901
2193
  ? { hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs }
1902
2194
  : undefined,
2195
+ baseDir: config.worktreeBaseDir,
1903
2196
  });
1904
2197
  } catch (error) {
1905
2198
  const setupError = error instanceof Error ? error.message : String(error);
@@ -1941,6 +2234,8 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1941
2234
  concurrency,
1942
2235
  async (task, taskIdx) => {
1943
2236
  const fi = groupStartFlatIndex + taskIdx;
2237
+ if (timedOut) return timedOutStepResult(task.agent);
2238
+ if (interrupted) return pausedStepResult(task.agent);
1944
2239
  if (aborted && failFast) {
1945
2240
  const skippedAt = Date.now();
1946
2241
  statusPayload.steps[fi].status = "failed";
@@ -1994,11 +2289,13 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1994
2289
  childIntercomTarget: config.childIntercomTargets?.[fi],
1995
2290
  orchestratorIntercomTarget: config.controlIntercomTarget,
1996
2291
  nestedRoute: config.nestedRoute,
1997
- registerInterrupt: (interrupt) => {
1998
- activeChildInterrupt = interrupt;
1999
- },
2292
+ registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
2293
+ registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
2294
+ timeoutSignal: timeoutAbortController.signal,
2295
+ timeoutMessage,
2000
2296
  onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking),
2001
2297
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
2298
+ skipAcceptance: () => timedOut,
2002
2299
  });
2003
2300
  if (task.sessionFile) {
2004
2301
  latestSessionFile = task.sessionFile;
@@ -2006,16 +2303,19 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2006
2303
 
2007
2304
  const taskEndTime = Date.now();
2008
2305
  const taskDuration = taskEndTime - taskStartTime;
2306
+ const childInterrupted = singleResult.interrupted === true;
2009
2307
 
2010
- statusPayload.steps[fi].status = singleResult.exitCode === 0 ? "complete" : "failed";
2308
+ statusPayload.steps[fi].status = timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
2011
2309
  statusPayload.steps[fi].endedAt = taskEndTime;
2012
2310
  statusPayload.steps[fi].durationMs = taskDuration;
2013
- statusPayload.steps[fi].exitCode = singleResult.exitCode;
2311
+ statusPayload.steps[fi].exitCode = timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
2312
+ statusPayload.steps[fi].timedOut = timedOut || singleResult.timedOut ? true : undefined;
2014
2313
  statusPayload.steps[fi].model = singleResult.model;
2015
2314
  statusPayload.steps[fi].thinking = resolveEffectiveThinking(singleResult.model, statusPayload.steps[fi].thinking);
2016
2315
  statusPayload.steps[fi].attemptedModels = singleResult.attemptedModels;
2017
2316
  statusPayload.steps[fi].modelAttempts = singleResult.modelAttempts;
2018
- statusPayload.steps[fi].error = singleResult.error;
2317
+ statusPayload.steps[fi].totalCost = singleResult.totalCost;
2318
+ statusPayload.steps[fi].error = timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
2019
2319
  statusPayload.steps[fi].structuredOutput = singleResult.structuredOutput;
2020
2320
  statusPayload.steps[fi].structuredOutputPath = singleResult.structuredOutputPath;
2021
2321
  statusPayload.steps[fi].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
@@ -2024,9 +2324,9 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2024
2324
  writeStatusPayload();
2025
2325
 
2026
2326
  appendJsonl(eventsPath, JSON.stringify({
2027
- type: singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
2327
+ type: timedOut ? "subagent.step.failed" : childInterrupted ? "subagent.step.paused" : singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
2028
2328
  ts: taskEndTime, runId: id, stepIndex: fi, agent: task.agent,
2029
- exitCode: singleResult.exitCode, durationMs: taskDuration,
2329
+ exitCode: timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode, durationMs: taskDuration,
2030
2330
  }));
2031
2331
  if (singleResult.completionGuardTriggered) {
2032
2332
  const event = buildControlEvent({
@@ -2043,8 +2343,9 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2043
2343
  }
2044
2344
 
2045
2345
  if (singleResult.exitCode !== 0 && failFast) aborted = true;
2046
- return { ...singleResult, skipped: false };
2346
+ return timedOut ? { ...singleResult, output: timeoutMessage ?? "Subagent timed out.", error: timeoutMessage ?? "Subagent timed out.", exitCode: 1, interrupted: false, timedOut: true, skipped: false } : { ...singleResult, skipped: false };
2047
2347
  },
2348
+ globalSemaphore,
2048
2349
  );
2049
2350
 
2050
2351
  flatIndex += group.parallel.length;
@@ -2072,14 +2373,17 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2072
2373
  agent: pr.agent,
2073
2374
  output: pr.output,
2074
2375
  error: pr.error,
2075
- success: pr.exitCode === 0,
2076
- exitCode: pr.exitCode,
2376
+ success: pr.interrupted !== true && pr.exitCode === 0,
2377
+ exitCode: pr.interrupted === true ? 0 : pr.exitCode,
2077
2378
  skipped: pr.skipped,
2379
+ interrupted: pr.interrupted,
2380
+ timedOut: pr.timedOut,
2078
2381
  sessionFile: pr.sessionFile,
2079
2382
  intercomTarget: pr.intercomTarget,
2080
2383
  model: pr.model,
2081
2384
  attemptedModels: pr.attemptedModels,
2082
2385
  modelAttempts: pr.modelAttempts,
2386
+ totalCost: pr.totalCost,
2083
2387
  artifactPaths: pr.artifactPaths,
2084
2388
  structuredOutput: pr.structuredOutput,
2085
2389
  structuredOutputPath: pr.structuredOutputPath,
@@ -2159,11 +2463,13 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2159
2463
  childIntercomTarget: config.childIntercomTargets?.[flatIndex],
2160
2464
  orchestratorIntercomTarget: config.controlIntercomTarget,
2161
2465
  nestedRoute: config.nestedRoute,
2162
- registerInterrupt: (interrupt) => {
2163
- activeChildInterrupt = interrupt;
2164
- },
2466
+ registerInterrupt: (interrupt) => registerStepInterrupt(flatIndex, interrupt),
2467
+ registerTimeout: (interrupt) => registerStepTimeout(flatIndex, interrupt),
2468
+ timeoutSignal: timeoutAbortController.signal,
2469
+ timeoutMessage,
2165
2470
  onAttemptStart: (attempt) => updateStepModel(flatIndex, attempt.model, attempt.thinking),
2166
2471
  onChildEvent: (event) => updateStepFromChildEvent(flatIndex, event),
2472
+ skipAcceptance: () => timedOut,
2167
2473
  });
2168
2474
  if (seqStep.sessionFile) {
2169
2475
  latestSessionFile = seqStep.sessionFile;
@@ -2172,20 +2478,23 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2172
2478
  previousOutput = singleResult.output;
2173
2479
  results.push({
2174
2480
  agent: singleResult.agent,
2175
- output: singleResult.output,
2176
- error: singleResult.error,
2177
- success: singleResult.exitCode === 0,
2178
- exitCode: singleResult.exitCode,
2481
+ output: timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.output,
2482
+ error: timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error,
2483
+ success: !timedOut && singleResult.interrupted !== true && singleResult.exitCode === 0,
2484
+ exitCode: timedOut ? 1 : singleResult.interrupted === true ? 0 : singleResult.exitCode,
2179
2485
  sessionFile: singleResult.sessionFile,
2180
2486
  intercomTarget: singleResult.intercomTarget,
2181
2487
  model: singleResult.model,
2182
2488
  attemptedModels: singleResult.attemptedModels,
2183
2489
  modelAttempts: singleResult.modelAttempts,
2490
+ totalCost: singleResult.totalCost,
2184
2491
  artifactPaths: singleResult.artifactPaths,
2185
2492
  structuredOutput: singleResult.structuredOutput,
2186
2493
  structuredOutputPath: singleResult.structuredOutputPath,
2187
2494
  structuredOutputSchemaPath: singleResult.structuredOutputSchemaPath,
2188
2495
  acceptance: singleResult.acceptance,
2496
+ interrupted: singleResult.interrupted,
2497
+ timedOut: timedOut || singleResult.timedOut ? true : undefined,
2189
2498
  });
2190
2499
  if (seqStep.outputName) {
2191
2500
  outputs[seqStep.outputName] = outputEntryFromAsyncResult({
@@ -2218,15 +2527,18 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2218
2527
  }
2219
2528
 
2220
2529
  const stepEndTime = Date.now();
2221
- statusPayload.steps[flatIndex].status = singleResult.exitCode === 0 ? "complete" : "failed";
2530
+ const childInterrupted = singleResult.interrupted === true;
2531
+ statusPayload.steps[flatIndex].status = timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
2222
2532
  statusPayload.steps[flatIndex].endedAt = stepEndTime;
2223
2533
  statusPayload.steps[flatIndex].durationMs = stepEndTime - stepStartTime;
2224
- statusPayload.steps[flatIndex].exitCode = singleResult.exitCode;
2534
+ statusPayload.steps[flatIndex].exitCode = timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
2535
+ statusPayload.steps[flatIndex].timedOut = timedOut || singleResult.timedOut ? true : undefined;
2225
2536
  statusPayload.steps[flatIndex].model = singleResult.model;
2226
2537
  statusPayload.steps[flatIndex].thinking = resolveEffectiveThinking(singleResult.model, statusPayload.steps[flatIndex].thinking);
2227
2538
  statusPayload.steps[flatIndex].attemptedModels = singleResult.attemptedModels;
2228
2539
  statusPayload.steps[flatIndex].modelAttempts = singleResult.modelAttempts;
2229
- statusPayload.steps[flatIndex].error = singleResult.error;
2540
+ statusPayload.steps[flatIndex].totalCost = singleResult.totalCost;
2541
+ statusPayload.steps[flatIndex].error = timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
2230
2542
  statusPayload.steps[flatIndex].structuredOutput = singleResult.structuredOutput;
2231
2543
  statusPayload.steps[flatIndex].structuredOutputPath = singleResult.structuredOutputPath;
2232
2544
  statusPayload.steps[flatIndex].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
@@ -2239,12 +2551,12 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2239
2551
  writeStatusPayload();
2240
2552
 
2241
2553
  appendJsonl(eventsPath, JSON.stringify({
2242
- type: singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
2554
+ type: timedOut ? "subagent.step.failed" : childInterrupted ? "subagent.step.paused" : singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
2243
2555
  ts: stepEndTime,
2244
2556
  runId: id,
2245
2557
  stepIndex: flatIndex,
2246
2558
  agent: seqStep.agent,
2247
- exitCode: singleResult.exitCode,
2559
+ exitCode: timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode,
2248
2560
  durationMs: stepEndTime - stepStartTime,
2249
2561
  tokens: stepTokens,
2250
2562
  }));
@@ -2283,6 +2595,12 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2283
2595
  }
2284
2596
 
2285
2597
  const resultMode = config.resultMode ?? statusPayload.mode;
2598
+ const totalCost = results.reduce<CostSummary>((sum, result) => ({
2599
+ inputTokens: sum.inputTokens + (result.totalCost?.inputTokens ?? 0),
2600
+ outputTokens: sum.outputTokens + (result.totalCost?.outputTokens ?? 0),
2601
+ costUsd: sum.costUsd + (result.totalCost?.costUsd ?? 0),
2602
+ }), { inputTokens: 0, outputTokens: 0, costUsd: 0 });
2603
+ const finalTotalCost = totalCost.inputTokens > 0 || totalCost.outputTokens > 0 || totalCost.costUsd > 0 ? totalCost : undefined;
2286
2604
  const finalFlatAgents = statusPayload.steps.map((step) => step.agent);
2287
2605
  const agentName = finalFlatAgents.length === 1
2288
2606
  ? finalFlatAgents[0]!
@@ -2323,14 +2641,23 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2323
2641
  clearInterval(activityTimer);
2324
2642
  activityTimer = undefined;
2325
2643
  }
2644
+ if (timeoutTimer) {
2645
+ clearTimeout(timeoutTimer);
2646
+ timeoutTimer = undefined;
2647
+ }
2326
2648
  disposeControlInbox();
2327
2649
  const effectiveSessionFile = sessionFile ?? latestSessionFile;
2328
2650
  const runEndedAt = Date.now();
2329
- statusPayload.state = interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed";
2651
+ statusPayload.state = timedOut ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed";
2330
2652
  statusPayload.activityState = undefined;
2653
+ if (timedOut) {
2654
+ statusPayload.timedOut = true;
2655
+ statusPayload.error = timeoutMessage ?? "Subagent timed out.";
2656
+ }
2331
2657
  statusPayload.endedAt = runEndedAt;
2332
2658
  statusPayload.lastUpdate = runEndedAt;
2333
2659
  statusPayload.sessionFile = effectiveSessionFile;
2660
+ statusPayload.totalCost = finalTotalCost;
2334
2661
  statusPayload.shareUrl = shareUrl;
2335
2662
  statusPayload.gistUrl = gistUrl;
2336
2663
  statusPayload.shareError = shareError;
@@ -2345,10 +2672,13 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2345
2672
  eventsPath,
2346
2673
  JSON.stringify({
2347
2674
  type: "subagent.run.completed",
2675
+ lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
2348
2676
  ts: runEndedAt,
2349
2677
  runId: id,
2350
2678
  status: statusPayload.state,
2351
2679
  durationMs: runEndedAt - overallStartTime,
2680
+ totalTokens: statusPayload.totalTokens,
2681
+ totalCost: finalTotalCost,
2352
2682
  }),
2353
2683
  );
2354
2684
  writeRunLog(logPath, {
@@ -2372,23 +2702,30 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2372
2702
 
2373
2703
  try {
2374
2704
  writeAtomicJson(resultPath, {
2705
+ lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
2375
2706
  id,
2376
2707
  agent: agentName,
2377
2708
  mode: resultMode,
2378
- success: !interrupted && results.every((r) => r.success),
2379
- state: interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed",
2380
- summary: interrupted ? "Paused after interrupt. Waiting for explicit next action." : summary,
2709
+ success: !timedOut && !interrupted && results.every((r) => r.success),
2710
+ state: timedOut ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed",
2711
+ summary: timedOut ? (timeoutMessage ?? "Subagent timed out.") : interrupted ? "Paused after interrupt. Waiting for explicit next action." : summary,
2712
+ ...(config.timeoutMs !== undefined ? { timeoutMs: config.timeoutMs } : {}),
2713
+ ...(config.deadlineAt !== undefined ? { deadlineAt: config.deadlineAt } : {}),
2714
+ ...(timedOut ? { timedOut: true, error: timeoutMessage ?? "Subagent timed out." } : {}),
2381
2715
  results: results.map((r) => ({
2382
2716
  agent: r.agent,
2383
2717
  output: r.output,
2384
2718
  error: r.error,
2385
2719
  success: r.success,
2386
2720
  skipped: r.skipped || undefined,
2721
+ interrupted: r.interrupted || undefined,
2722
+ timedOut: r.timedOut || undefined,
2387
2723
  sessionFile: r.sessionFile,
2388
2724
  intercomTarget: r.intercomTarget,
2389
2725
  model: r.model,
2390
2726
  attemptedModels: r.attemptedModels,
2391
2727
  modelAttempts: r.modelAttempts,
2728
+ totalCost: r.totalCost,
2392
2729
  artifactPaths: r.artifactPaths,
2393
2730
  truncated: r.truncated,
2394
2731
  structuredOutput: r.structuredOutput,
@@ -2398,9 +2735,11 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2398
2735
  })),
2399
2736
  outputs,
2400
2737
  workflowGraph: statusPayload.workflowGraph,
2401
- exitCode: interrupted || results.every((r) => r.success) ? 0 : 1,
2738
+ exitCode: timedOut ? 1 : interrupted || results.every((r) => r.success) ? 0 : 1,
2402
2739
  timestamp: runEndedAt,
2403
2740
  durationMs: runEndedAt - overallStartTime,
2741
+ totalTokens: statusPayload.totalTokens,
2742
+ totalCost: finalTotalCost,
2404
2743
  truncated,
2405
2744
  artifactsDir,
2406
2745
  cwd,