pi-subagents 0.31.0 → 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 (42) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +170 -8
  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 +69 -4
  12. package/src/extension/schemas.ts +2 -2
  13. package/src/intercom/intercom-bridge.ts +25 -1
  14. package/src/profiles/profiles.ts +637 -0
  15. package/src/runs/background/async-execution.ts +138 -33
  16. package/src/runs/background/async-job-tracker.ts +77 -1
  17. package/src/runs/background/async-resume.ts +11 -13
  18. package/src/runs/background/async-status.ts +41 -9
  19. package/src/runs/background/chain-root-attachment.ts +34 -4
  20. package/src/runs/background/control-channel.ts +227 -0
  21. package/src/runs/background/run-status.ts +1 -0
  22. package/src/runs/background/stale-run-reconciler.ts +28 -1
  23. package/src/runs/background/subagent-runner.ts +459 -113
  24. package/src/runs/foreground/chain-execution.ts +29 -7
  25. package/src/runs/foreground/execution.ts +24 -6
  26. package/src/runs/foreground/subagent-executor.ts +240 -44
  27. package/src/runs/shared/acceptance.ts +45 -22
  28. package/src/runs/shared/dynamic-fanout.ts +1 -1
  29. package/src/runs/shared/model-fallback.ts +4 -0
  30. package/src/runs/shared/nested-events.ts +58 -0
  31. package/src/runs/shared/parallel-utils.ts +49 -1
  32. package/src/runs/shared/pi-args.ts +5 -3
  33. package/src/runs/shared/pi-spawn.ts +52 -20
  34. package/src/runs/shared/single-output.ts +2 -0
  35. package/src/runs/shared/subagent-prompt-runtime.ts +3 -2
  36. package/src/runs/shared/worktree.ts +28 -5
  37. package/src/shared/artifacts.ts +15 -1
  38. package/src/shared/fork-context.ts +133 -22
  39. package/src/shared/types.ts +82 -3
  40. package/src/shared/utils.ts +99 -14
  41. package/src/slash/slash-commands.ts +726 -40
  42. package/src/tui/render.ts +16 -4
@@ -4,6 +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, deliverInterruptRequest, deliverTimeoutRequest, watchAsyncControlInbox } from "./control-channel.ts";
7
8
  import { appendJsonl as appendRawJsonl, getArtifactPaths } from "../../shared/artifacts.ts";
8
9
  import { PI_CODING_AGENT_PACKAGE, getPiSpawnCommand, resolveInstalledPiPackageRoot } from "../shared/pi-spawn.ts";
9
10
  import { captureSingleOutputSnapshot, finalizeSingleOutput, formatSavedOutputReference, resolveSingleOutput, type SingleOutputSnapshot } from "../shared/single-output.ts";
@@ -14,14 +15,17 @@ import {
14
15
  type AsyncParallelGroupStatus,
15
16
  type AsyncStatus,
16
17
  type ChainOutputMap,
18
+ type CostSummary,
17
19
  type ModelAttempt,
18
20
  type NestedRouteInfo,
21
+ type NestedRunSummary,
19
22
  type ResolvedControlConfig,
20
23
  type SubagentRunMode,
21
24
  type Usage,
22
25
  type WorkflowGraphSnapshot,
23
26
  DEFAULT_MAX_OUTPUT,
24
27
  type MaxOutputConfig,
28
+ SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
25
29
  truncateOutput,
26
30
  getSubagentDepthEnv,
27
31
  } from "../../shared/types.ts";
@@ -42,15 +46,17 @@ import {
42
46
  mapConcurrent,
43
47
  aggregateParallelOutputs,
44
48
  MAX_PARALLEL_CONCURRENCY,
49
+ DEFAULT_GLOBAL_CONCURRENCY_LIMIT,
50
+ Semaphore,
45
51
  } from "../shared/parallel-utils.ts";
46
- import { buildPiArgs, cleanupTempDir } from "../shared/pi-args.ts";
52
+ import { applyThinkingSuffix, buildPiArgs, cleanupTempDir } from "../shared/pi-args.ts";
47
53
  import { outputEntryFromAsyncResult, resolveOutputReferences } from "../shared/chain-outputs.ts";
48
54
  import { createStructuredOutputRuntime, readStructuredOutput } from "../shared/structured-output.ts";
49
55
  import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection } from "../shared/dynamic-fanout.ts";
50
- import { nestedSummaryFromAsyncStatus, writeNestedEvent } from "../shared/nested-events.ts";
56
+ import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDir, writeNestedEvent } from "../shared/nested-events.ts";
51
57
  import { formatModelAttemptNote, isRetryableModelFailure } from "../shared/model-fallback.ts";
52
58
  import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
53
- import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput } from "../../shared/utils.ts";
59
+ import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput, readStatus } from "../../shared/utils.ts";
54
60
  import { evaluateCompletionMutationGuard } from "../shared/completion-guard.ts";
55
61
  import {
56
62
  createMutatingFailureState,
@@ -100,6 +106,7 @@ interface SubagentRunConfig {
100
106
  piArgv1?: string;
101
107
  worktreeSetupHook?: string;
102
108
  worktreeSetupHookTimeoutMs?: number;
109
+ worktreeBaseDir?: string;
103
110
  controlConfig?: ResolvedControlConfig;
104
111
  controlIntercomTarget?: string;
105
112
  childIntercomTargets?: Array<string | undefined>;
@@ -108,6 +115,10 @@ interface SubagentRunConfig {
108
115
  workflowGraph?: WorkflowGraphSnapshot;
109
116
  nestedRoute?: NestedRouteInfo;
110
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;
111
122
  }
112
123
 
113
124
  interface StepResult {
@@ -117,11 +128,14 @@ interface StepResult {
117
128
  success: boolean;
118
129
  exitCode?: number | null;
119
130
  skipped?: boolean;
131
+ interrupted?: boolean;
132
+ timedOut?: boolean;
120
133
  sessionFile?: string;
121
134
  intercomTarget?: string;
122
135
  model?: string;
123
136
  attemptedModels?: string[];
124
137
  modelAttempts?: ModelAttempt[];
138
+ totalCost?: CostSummary;
125
139
  artifactPaths?: ArtifactPaths;
126
140
  truncated?: boolean;
127
141
  structuredOutput?: unknown;
@@ -236,6 +250,21 @@ function tokenUsageFromAttempts(attempts: ModelAttempt[] | undefined): TokenUsag
236
250
  return total > 0 ? { input, output, total } : null;
237
251
  }
238
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
+
239
268
  function appendRecentStepOutput(step: RunnerStatusStep, lines: string[]): void {
240
269
  const nonEmpty = lines.filter((line) => line.trim());
241
270
  if (nonEmpty.length === 0) return;
@@ -294,6 +323,7 @@ interface RunPiStreamingResult {
294
323
  error?: string;
295
324
  finalOutput: string;
296
325
  interrupted?: boolean;
326
+ timedOut?: boolean;
297
327
  observedMutationAttempt?: boolean;
298
328
  }
299
329
 
@@ -308,6 +338,8 @@ function runPiStreaming(
308
338
  childEventContext?: ChildEventContext,
309
339
  registerInterrupt?: (interrupt: (() => void) | undefined) => void,
310
340
  onChildEvent?: (event: ChildEvent) => void,
341
+ registerTimeout?: (interrupt: (() => void) | undefined) => void,
342
+ timeoutMessage?: string,
311
343
  ): Promise<RunPiStreamingResult> {
312
344
  return new Promise((resolve) => {
313
345
  const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
@@ -331,6 +363,7 @@ function runPiStreaming(
331
363
  let error: string | undefined;
332
364
  let assistantError: string | undefined;
333
365
  let interrupted = false;
366
+ let timedOut = false;
334
367
  let observedMutationAttempt = false;
335
368
  const rawStdoutLines: string[] = [];
336
369
 
@@ -429,11 +462,13 @@ function runPiStreaming(
429
462
  // a lingering stdio holder after `exit`, or a child that never exits.
430
463
  const FINAL_STOP_GRACE_MS = 1000;
431
464
  const HARD_KILL_MS = 3000;
465
+ const TIMEOUT_HARD_KILL_MS = 3000;
432
466
  let childExited = false;
433
467
  let forcedTerminationSignal = false;
434
468
  let cleanTerminalAssistantStopReceived = false;
435
469
  let finalDrainTimer: NodeJS.Timeout | undefined;
436
470
  let finalHardKillTimer: NodeJS.Timeout | undefined;
471
+ let timeoutHardKillTimer: NodeJS.Timeout | undefined;
437
472
  let settled = false;
438
473
  const clearStdioGuard = attachPostExitStdioGuard(child, { idleMs: 2000, hardMs: 8000 });
439
474
  child.stdout.on("data", (chunk: Buffer) => {
@@ -448,14 +483,25 @@ function runPiStreaming(
448
483
  processStderrText(chunk.toString());
449
484
  });
450
485
  registerInterrupt?.(() => {
451
- if (settled) return;
486
+ if (settled || timedOut) return;
452
487
  interrupted = true;
453
488
  if (!error) error = "Interrupted. Waiting for explicit next action.";
454
489
  trySignalChild(child, "SIGINT");
455
490
  setTimeout(() => {
456
- if (!settled) trySignalChild(child, "SIGTERM");
491
+ if (!settled && !timedOut) trySignalChild(child, "SIGTERM");
457
492
  }, 1000).unref?.();
458
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
+ });
459
505
  const clearDrainTimers = () => {
460
506
  if (finalDrainTimer) {
461
507
  clearTimeout(finalDrainTimer);
@@ -465,6 +511,10 @@ function runPiStreaming(
465
511
  clearTimeout(finalHardKillTimer);
466
512
  finalHardKillTimer = undefined;
467
513
  }
514
+ if (timeoutHardKillTimer) {
515
+ clearTimeout(timeoutHardKillTimer);
516
+ timeoutHardKillTimer = undefined;
517
+ }
468
518
  };
469
519
  function startFinalDrain(): void {
470
520
  if (childExited || finalDrainTimer || settled) return;
@@ -491,6 +541,7 @@ function runPiStreaming(
491
541
  child.on("close", (exitCode, signal) => {
492
542
  settled = true;
493
543
  registerInterrupt?.(undefined);
544
+ registerTimeout?.(undefined);
494
545
  clearDrainTimers();
495
546
  clearStdioGuard();
496
547
  if (stdoutBuf.trim()) processStdoutLine(stdoutBuf);
@@ -501,13 +552,14 @@ function runPiStreaming(
501
552
  const forcedDrainAfterFinalSuccess = forcedTerminationSignal && cleanTerminalAssistantStopReceived && !finalError;
502
553
  resolve({
503
554
  stderr,
504
- exitCode: interrupted || forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
555
+ exitCode: timedOut ? 1 : interrupted || forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
505
556
  messages,
506
557
  usage,
507
558
  model,
508
- error: interrupted || forcedDrainAfterFinalSuccess ? undefined : finalError,
509
- finalOutput,
559
+ error: timedOut ? (timeoutMessage ?? "Subagent timed out.") : interrupted || forcedDrainAfterFinalSuccess ? undefined : finalError,
560
+ finalOutput: timedOut && !finalOutput.trim() ? (timeoutMessage ?? "Subagent timed out.") : finalOutput,
510
561
  interrupted,
562
+ timedOut,
511
563
  observedMutationAttempt,
512
564
  });
513
565
  });
@@ -515,12 +567,13 @@ function runPiStreaming(
515
567
  child.on("error", (spawnError) => {
516
568
  settled = true;
517
569
  registerInterrupt?.(undefined);
570
+ registerTimeout?.(undefined);
518
571
  clearDrainTimers();
519
572
  clearStdioGuard();
520
573
  outputStream.end();
521
574
  const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
522
575
  const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
523
- 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 });
524
577
  });
525
578
  });
526
579
  }
@@ -648,11 +701,15 @@ interface SingleStepContext {
648
701
  piPackageRoot?: string;
649
702
  piArgv1?: string;
650
703
  registerInterrupt?: (interrupt: (() => void) | undefined) => void;
704
+ registerTimeout?: (interrupt: (() => void) | undefined) => void;
705
+ timeoutSignal?: AbortSignal;
706
+ timeoutMessage?: string;
651
707
  childIntercomTarget?: string;
652
708
  orchestratorIntercomTarget?: string;
653
709
  nestedRoute?: NestedRouteInfo;
654
710
  onAttemptStart?: (attempt: { model?: string; thinking?: string }) => void;
655
711
  onChildEvent?: (event: ChildEvent) => void;
712
+ skipAcceptance?: () => boolean;
656
713
  }
657
714
 
658
715
  /** Run a single pi agent step, returning output and metadata */
@@ -669,6 +726,7 @@ async function runSingleStep(
669
726
  modelAttempts?: ModelAttempt[];
670
727
  artifactPaths?: ArtifactPaths;
671
728
  interrupted?: boolean;
729
+ timedOut?: boolean;
672
730
  sessionFile?: string;
673
731
  intercomTarget?: string;
674
732
  completionGuardTriggered?: boolean;
@@ -678,27 +736,52 @@ async function runSingleStep(
678
736
  acceptance?: import("../../shared/types.ts").AcceptanceLedger;
679
737
  }> {
680
738
  if (step.importAsyncRoot) {
681
- 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
+ });
682
754
  try {
683
- fs.writeFileSync(ctx.outputFile, imported.output, "utf-8");
684
- } catch {
685
- // 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);
686
784
  }
687
- return {
688
- agent: imported.agent,
689
- output: imported.output,
690
- exitCode: imported.exitCode,
691
- error: imported.error,
692
- sessionFile: imported.sessionFile,
693
- intercomTarget: imported.intercomTarget,
694
- model: imported.model,
695
- attemptedModels: imported.attemptedModels,
696
- modelAttempts: imported.modelAttempts,
697
- structuredOutput: imported.structuredOutput,
698
- structuredOutputPath: imported.structuredOutputPath,
699
- structuredOutputSchemaPath: imported.structuredOutputSchemaPath,
700
- acceptance: imported.acceptance,
701
- };
702
785
  }
703
786
 
704
787
  const effectiveStructuredOutput = step.structuredOutput ?? (step.structuredOutputSchema
@@ -739,6 +822,7 @@ async function runSingleStep(
739
822
  let completionGuardTriggeredFinal = false;
740
823
 
741
824
  for (let index = 0; index < candidates.length; index++) {
825
+ if (ctx.timeoutSignal?.aborted || ctx.skipAcceptance?.()) break;
742
826
  const candidate = candidates[index];
743
827
  ctx.onAttemptStart?.({ model: candidate, thinking: resolveEffectiveThinking(candidate, step.thinking) });
744
828
  const outputSnapshot = captureSingleOutputSnapshot(step.outputPath);
@@ -790,13 +874,21 @@ async function runSingleStep(
790
874
  { eventsPath, runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
791
875
  ctx.registerInterrupt,
792
876
  ctx.onChildEvent,
877
+ ctx.registerTimeout,
878
+ ctx.timeoutMessage,
793
879
  );
794
880
  cleanupTempDir(tempDir);
795
881
 
796
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;
797
889
  let structuredOutput: unknown;
798
890
  let structuredError: string | undefined;
799
- if (effectiveStructuredOutput && run.exitCode === 0 && !run.error && !hiddenError?.hasError) {
891
+ if (effectiveStructuredOutput && run.exitCode === 0 && !run.error && !hiddenError?.hasError && !emptyOutputError) {
800
892
  const structured = readStructuredOutput({
801
893
  schema: effectiveStructuredOutput.schema,
802
894
  schemaPath: effectiveStructuredOutput.schemaPath,
@@ -805,7 +897,7 @@ async function runSingleStep(
805
897
  if (structured.error) structuredError = structured.error;
806
898
  else structuredOutput = structured.value;
807
899
  }
808
- 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
809
901
  ? evaluateCompletionMutationGuard({
810
902
  agent: step.agent,
811
903
  task: taskForCompletionGuard,
@@ -824,16 +916,18 @@ async function runSingleStep(
824
916
  ? 1
825
917
  : hiddenError?.hasError
826
918
  ? (hiddenError.exitCode ?? 1)
827
- : run.error && run.exitCode === 0
919
+ : emptyOutputError
828
920
  ? 1
829
- : run.exitCode;
921
+ : run.error && run.exitCode === 0
922
+ ? 1
923
+ : run.exitCode;
830
924
  const error = completionGuardError
831
925
  ?? structuredError
832
926
  ?? (hiddenError?.hasError
833
927
  ? hiddenError.details
834
928
  ? `${hiddenError.errorType} failed (exit ${effectiveExitCode}): ${hiddenError.details}`
835
929
  : `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
836
- : 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)));
837
931
  const attempt: ModelAttempt = {
838
932
  model: candidate ?? run.model ?? step.model ?? "default",
839
933
  success: effectiveExitCode === 0 && !error,
@@ -846,6 +940,7 @@ async function runSingleStep(
846
940
  completionGuardTriggeredFinal = completionGuardTriggered;
847
941
  finalOutputSnapshot = outputSnapshot;
848
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;
849
944
  if (attempt.success || completionGuardTriggered) break;
850
945
  if (!isRetryableModelFailure(error) || index === candidates.length - 1) break;
851
946
  attemptNotes.push(formatModelAttemptNote(attempt, candidates[index + 1]));
@@ -873,19 +968,25 @@ async function runSingleStep(
873
968
  saveError: resolvedOutput.saveError,
874
969
  });
875
970
  outputForSummary = finalizedOutput.displayOutput;
876
- const acceptance = step.effectiveAcceptance
971
+ const acceptance = step.effectiveAcceptance && !ctx.timeoutSignal?.aborted && !ctx.skipAcceptance?.()
877
972
  ? await evaluateAcceptance({
878
973
  acceptance: step.effectiveAcceptance,
879
974
  output: outputForAcceptance,
880
975
  cwd: step.cwd ?? ctx.cwd,
976
+ signal: ctx.timeoutSignal,
977
+ abortMessage: ctx.timeoutMessage ?? "Subagent timed out.",
881
978
  })
882
979
  : undefined;
883
- const acceptanceFailure = acceptance ? acceptanceFailureMessage(acceptance) : undefined;
884
- const acceptanceCanFailRun = acceptanceFailure && acceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted;
885
- const effectiveFinalExitCode = acceptanceCanFailRun ? 1 : finalResult?.exitCode ?? 1;
886
- const effectiveFinalError = acceptanceCanFailRun
887
- ? (finalResult?.error ? `${finalResult.error}\n${acceptanceFailure}` : acceptanceFailure)
888
- : 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;
889
990
 
890
991
  if (artifactPaths && ctx.artifactConfig?.enabled !== false) {
891
992
  if (ctx.artifactConfig?.includeOutput !== false) {
@@ -920,13 +1021,15 @@ async function runSingleStep(
920
1021
  model: finalResult?.model,
921
1022
  attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
922
1023
  modelAttempts,
1024
+ totalCost: costSummaryFromAttempts(modelAttempts),
923
1025
  artifactPaths,
924
- interrupted: finalResult?.interrupted,
1026
+ interrupted: timedOutAfterAcceptance ? false : finalResult?.interrupted,
1027
+ timedOut: timedOutAfterAcceptance ? true : finalResult?.timedOut,
925
1028
  completionGuardTriggered: completionGuardTriggeredFinal,
926
- structuredOutput: (finalResult as (RunPiStreamingResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput,
927
- structuredOutputPath: effectiveStructuredOutput?.outputPath,
928
- structuredOutputSchemaPath: effectiveStructuredOutput?.schemaPath,
929
- 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,
930
1033
  };
931
1034
  }
932
1035
 
@@ -1055,9 +1158,12 @@ function ensureParallelProgressFile(cwd: string, group: Extract<RunnerStep, { pa
1055
1158
  writeInitialProgressFile(cwd);
1056
1159
  }
1057
1160
 
1161
+ type SingleStepResult = Awaited<ReturnType<typeof runSingleStep>>;
1162
+
1058
1163
  async function runSubagent(config: SubagentRunConfig): Promise<void> {
1059
1164
  const { id, steps, resultPath, cwd, placeholder, taskIndex, totalTasks, maxOutput, artifactsDir, artifactConfig } =
1060
1165
  config;
1166
+ const globalSemaphore = new Semaphore(config.globalConcurrencyLimit ?? DEFAULT_GLOBAL_CONCURRENCY_LIMIT);
1061
1167
  let previousOutput = "";
1062
1168
  const outputs: ChainOutputMap = {};
1063
1169
  const results: StepResult[] = [];
@@ -1068,10 +1174,15 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1068
1174
  const eventsPath = path.join(asyncDir, "events.jsonl");
1069
1175
  const logPath = path.join(asyncDir, `subagent-log-${id}.md`);
1070
1176
  const controlConfig = config.controlConfig ?? DEFAULT_CONTROL_CONFIG;
1071
- let activeChildInterrupt: (() => void) | undefined;
1177
+ const activeChildInterrupts = new Map<number, () => void>();
1178
+ const activeChildTimeouts = new Map<number, () => void>();
1072
1179
  let interrupted = false;
1073
1180
  let currentActivityState: ActivityState | undefined;
1074
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();
1075
1186
  let previousCumulativeTokens: TokenUsage = { input: 0, output: 0, total: 0 };
1076
1187
  let latestSessionFile: string | undefined;
1077
1188
 
@@ -1137,6 +1248,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1137
1248
  || shareEnabled
1138
1249
  || flatSteps.some((step) => Boolean(step.sessionFile));
1139
1250
  const statusPayload: RunnerStatusPayload = {
1251
+ lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
1140
1252
  runId: id,
1141
1253
  ...(config.sessionId ? { sessionId: config.sessionId } : {}),
1142
1254
  mode: config.resultMode ?? (flatSteps.length > 1 ? "chain" : "single"),
@@ -1144,6 +1256,8 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1144
1256
  lastActivityAt: overallStartTime,
1145
1257
  startedAt: overallStartTime,
1146
1258
  lastUpdate: overallStartTime,
1259
+ ...(config.timeoutMs !== undefined ? { timeoutMs: config.timeoutMs } : {}),
1260
+ ...(config.deadlineAt !== undefined ? { deadlineAt: config.deadlineAt } : {}),
1147
1261
  pid: process.pid,
1148
1262
  cwd,
1149
1263
  currentStep: 0,
@@ -1215,6 +1329,110 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1215
1329
  writeAtomicJson(statusPath, statusPayload);
1216
1330
  emitNestedSelfEvent(statusPayload.state === "running" || statusPayload.state === "queued" ? "subagent.nested.updated" : "subagent.nested.completed");
1217
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
+ });
1218
1436
  const consumePendingAppendRequests = (): void => {
1219
1437
  if (statusPayload.mode !== "chain" || statusPayload.state !== "running") return;
1220
1438
  const requests = consumeChainAppendRequests(asyncDir);
@@ -1507,6 +1725,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1507
1725
  }
1508
1726
 
1509
1727
  const interruptRunner = () => {
1728
+ consumeInterruptRequest(asyncDir);
1510
1729
  if (interrupted || statusPayload.state !== "running") return;
1511
1730
  interrupted = true;
1512
1731
  const now = Date.now();
@@ -1529,13 +1748,59 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1529
1748
  ts: now,
1530
1749
  runId: id,
1531
1750
  }));
1532
- 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();
1533
1788
  };
1534
1789
  process.on(ASYNC_INTERRUPT_SIGNAL, interruptRunner);
1790
+ // Portable control inbox: the parent drops an interrupt request file here when
1791
+ // it cannot deliver the OS signal (e.g. ENOSYS on Windows). Routes into the
1792
+ // same graceful interruptRunner() so stop/steer work on every platform.
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
+ }
1535
1799
  appendJsonl(
1536
1800
  eventsPath,
1537
1801
  JSON.stringify({
1538
1802
  type: "subagent.run.started",
1803
+ lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
1539
1804
  ts: overallStartTime,
1540
1805
  runId: id,
1541
1806
  mode: statusPayload.mode,
@@ -1548,7 +1813,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1548
1813
  let stepCursor = 0;
1549
1814
 
1550
1815
  while (true) {
1551
- if (interrupted) break;
1816
+ if (interrupted || timedOut) break;
1552
1817
  consumePendingAppendRequests();
1553
1818
  if (stepCursor >= steps.length) break;
1554
1819
  const stepIndex = stepCursor++;
@@ -1600,7 +1865,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1600
1865
  placeholder.durationMs = 0;
1601
1866
  }
1602
1867
  previousOutput = "Dynamic fanout produced 0 results.";
1603
- const groupAcceptance = step.effectiveAcceptance?.explicit
1868
+ const groupAcceptance = step.effectiveAcceptance?.explicit && !timedOut
1604
1869
  ? await evaluateAcceptance({
1605
1870
  acceptance: step.effectiveAcceptance,
1606
1871
  output: "",
@@ -1609,38 +1874,55 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1609
1874
  notes: "Dynamic fanout produced 0 results.",
1610
1875
  }),
1611
1876
  cwd,
1877
+ signal: timeoutAbortController.signal,
1878
+ abortMessage: timeoutMessage ?? "Subagent timed out.",
1612
1879
  })
1613
1880
  : undefined;
1614
- if (placeholder && groupAcceptance) placeholder.acceptance = groupAcceptance;
1615
- const groupAcceptanceFailure = groupAcceptance ? acceptanceFailureMessage(groupAcceptance) : undefined;
1616
- 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!;
1617
1887
  statusPayload.state = "failed";
1618
- statusPayload.error = groupAcceptanceFailure;
1888
+ statusPayload.error = errorMessage;
1619
1889
  if (placeholder) {
1620
1890
  placeholder.status = "failed";
1621
- placeholder.error = groupAcceptanceFailure;
1891
+ placeholder.error = errorMessage;
1622
1892
  placeholder.exitCode = 1;
1893
+ placeholder.timedOut = groupTimedOut ? true : undefined;
1623
1894
  }
1624
- markDynamicGraphGroup(stepIndex, "failed", groupAcceptanceFailure, groupAcceptance);
1625
- statusPayload.lastUpdate = now;
1895
+ markDynamicGraphGroup(stepIndex, "failed", errorMessage, effectiveGroupAcceptance);
1896
+ statusPayload.lastUpdate = Date.now();
1626
1897
  writeStatusPayload();
1627
- 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 });
1628
1899
  break;
1629
1900
  }
1630
1901
  flatIndex++;
1631
1902
  statusPayload.lastUpdate = now;
1632
- markDynamicGraphGroup(stepIndex, "completed", undefined, groupAcceptance);
1903
+ markDynamicGraphGroup(stepIndex, "completed", undefined, effectiveGroupAcceptance);
1633
1904
  writeStatusPayload();
1634
1905
  continue;
1635
1906
  }
1636
1907
 
1637
- const dynamicSteps = materialized.parallel.map((task, itemIndex) => ({
1638
- ...step.parallel,
1639
- task: task.task ?? step.parallel.task,
1640
- label: task.label ?? step.parallel.label,
1641
- structuredOutput: undefined,
1642
- structuredOutputSchema: step.parallel.structuredOutputSchema ?? step.parallel.structuredOutput?.schema,
1643
- }));
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
+ });
1644
1926
  const dynamicStatusSteps: RunnerStatusStep[] = dynamicSteps.map((task) => ({
1645
1927
  agent: task.agent,
1646
1928
  phase: task.phase ?? step.phase,
@@ -1704,6 +1986,8 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1704
1986
  let aborted = false;
1705
1987
  const parallelResults = await mapConcurrent(dynamicSteps, concurrency, async (task, taskIdx) => {
1706
1988
  const fi = groupStartFlatIndex + taskIdx;
1989
+ if (timedOut) return timedOutStepResult(task.agent);
1990
+ if (interrupted) return pausedStepResult(task.agent);
1707
1991
  if (aborted && failFast) {
1708
1992
  const skippedAt = Date.now();
1709
1993
  statusPayload.steps[fi].status = "failed";
@@ -1741,22 +2025,27 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1741
2025
  childIntercomTarget: config.childIntercomTargets?.[fi],
1742
2026
  orchestratorIntercomTarget: config.controlIntercomTarget,
1743
2027
  nestedRoute: config.nestedRoute,
1744
- registerInterrupt: (interrupt) => {
1745
- activeChildInterrupt = interrupt;
1746
- },
2028
+ registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
2029
+ registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
2030
+ timeoutSignal: timeoutAbortController.signal,
2031
+ timeoutMessage,
1747
2032
  onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking),
1748
2033
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
2034
+ skipAcceptance: () => timedOut,
1749
2035
  });
1750
2036
  const taskEndTime = Date.now();
1751
- 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";
1752
2039
  statusPayload.steps[fi].endedAt = taskEndTime;
1753
2040
  statusPayload.steps[fi].durationMs = taskEndTime - taskStartTime;
1754
- 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;
1755
2043
  statusPayload.steps[fi].model = singleResult.model;
1756
2044
  statusPayload.steps[fi].thinking = resolveEffectiveThinking(singleResult.model, statusPayload.steps[fi].thinking);
1757
2045
  statusPayload.steps[fi].attemptedModels = singleResult.attemptedModels;
1758
2046
  statusPayload.steps[fi].modelAttempts = singleResult.modelAttempts;
1759
- 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;
1760
2049
  statusPayload.steps[fi].structuredOutput = singleResult.structuredOutput;
1761
2050
  statusPayload.steps[fi].structuredOutputPath = singleResult.structuredOutputPath;
1762
2051
  statusPayload.steps[fi].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
@@ -1764,13 +2053,13 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1764
2053
  statusPayload.lastUpdate = taskEndTime;
1765
2054
  writeStatusPayload();
1766
2055
  appendJsonl(eventsPath, JSON.stringify({
1767
- 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",
1768
2057
  ts: taskEndTime, runId: id, stepIndex: fi, agent: task.agent,
1769
- exitCode: singleResult.exitCode, durationMs: taskEndTime - taskStartTime,
2058
+ exitCode: timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode, durationMs: taskEndTime - taskStartTime,
1770
2059
  }));
1771
2060
  if (singleResult.exitCode !== 0 && failFast) aborted = true;
1772
- return { ...singleResult, skipped: false };
1773
- });
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);
1774
2063
 
1775
2064
  flatIndex += dynamicSteps.length;
1776
2065
  for (const pr of parallelResults) {
@@ -1778,14 +2067,17 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1778
2067
  agent: pr.agent,
1779
2068
  output: pr.output,
1780
2069
  error: pr.error,
1781
- success: pr.exitCode === 0,
1782
- exitCode: pr.exitCode,
2070
+ success: pr.interrupted !== true && pr.exitCode === 0,
2071
+ exitCode: pr.interrupted === true ? 0 : pr.exitCode,
1783
2072
  skipped: pr.skipped,
2073
+ interrupted: pr.interrupted,
2074
+ timedOut: pr.timedOut,
1784
2075
  sessionFile: pr.sessionFile,
1785
2076
  intercomTarget: pr.intercomTarget,
1786
2077
  model: pr.model,
1787
2078
  attemptedModels: pr.attemptedModels,
1788
2079
  modelAttempts: pr.modelAttempts,
2080
+ totalCost: pr.totalCost,
1789
2081
  artifactPaths: pr.artifactPaths,
1790
2082
  structuredOutput: pr.structuredOutput,
1791
2083
  structuredOutputPath: pr.structuredOutputPath,
@@ -1805,7 +2097,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1805
2097
  stepIndex,
1806
2098
  };
1807
2099
  statusPayload.outputs = outputs;
1808
- const groupAcceptance = step.effectiveAcceptance
2100
+ const groupAcceptance = step.effectiveAcceptance && !timedOut
1809
2101
  ? await evaluateAcceptance({
1810
2102
  acceptance: step.effectiveAcceptance,
1811
2103
  output: "",
@@ -1814,21 +2106,27 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1814
2106
  notes: `Dynamic fanout collected ${collection.length} result(s) into ${step.collect.as}.`,
1815
2107
  }),
1816
2108
  cwd,
2109
+ signal: timeoutAbortController.signal,
2110
+ abortMessage: timeoutMessage ?? "Subagent timed out.",
1817
2111
  })
1818
2112
  : undefined;
1819
- const groupAcceptanceFailure = groupAcceptance ? acceptanceFailureMessage(groupAcceptance) : undefined;
1820
- markDynamicGraphGroup(stepIndex, groupAcceptanceFailure ? "failed" : "completed", groupAcceptanceFailure, groupAcceptance);
1821
- 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) {
1822
2119
  results.push({
1823
2120
  agent: step.parallel.agent,
1824
- output: groupAcceptanceFailure,
1825
- error: groupAcceptanceFailure,
2121
+ output: groupError,
2122
+ error: groupError,
1826
2123
  success: false,
1827
2124
  exitCode: 1,
2125
+ timedOut: groupTimedOut ? true : undefined,
1828
2126
  structuredOutput: collection,
1829
- acceptance: groupAcceptance,
2127
+ acceptance: effectiveGroupAcceptance,
1830
2128
  });
1831
- statusPayload.error = groupAcceptanceFailure;
2129
+ statusPayload.error = groupError;
1832
2130
  }
1833
2131
  } catch (error) {
1834
2132
  const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
@@ -1894,6 +2192,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1894
2192
  setupHook: config.worktreeSetupHook
1895
2193
  ? { hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs }
1896
2194
  : undefined,
2195
+ baseDir: config.worktreeBaseDir,
1897
2196
  });
1898
2197
  } catch (error) {
1899
2198
  const setupError = error instanceof Error ? error.message : String(error);
@@ -1935,6 +2234,8 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1935
2234
  concurrency,
1936
2235
  async (task, taskIdx) => {
1937
2236
  const fi = groupStartFlatIndex + taskIdx;
2237
+ if (timedOut) return timedOutStepResult(task.agent);
2238
+ if (interrupted) return pausedStepResult(task.agent);
1938
2239
  if (aborted && failFast) {
1939
2240
  const skippedAt = Date.now();
1940
2241
  statusPayload.steps[fi].status = "failed";
@@ -1988,11 +2289,13 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1988
2289
  childIntercomTarget: config.childIntercomTargets?.[fi],
1989
2290
  orchestratorIntercomTarget: config.controlIntercomTarget,
1990
2291
  nestedRoute: config.nestedRoute,
1991
- registerInterrupt: (interrupt) => {
1992
- activeChildInterrupt = interrupt;
1993
- },
2292
+ registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
2293
+ registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
2294
+ timeoutSignal: timeoutAbortController.signal,
2295
+ timeoutMessage,
1994
2296
  onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking),
1995
2297
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
2298
+ skipAcceptance: () => timedOut,
1996
2299
  });
1997
2300
  if (task.sessionFile) {
1998
2301
  latestSessionFile = task.sessionFile;
@@ -2000,16 +2303,19 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2000
2303
 
2001
2304
  const taskEndTime = Date.now();
2002
2305
  const taskDuration = taskEndTime - taskStartTime;
2306
+ const childInterrupted = singleResult.interrupted === true;
2003
2307
 
2004
- statusPayload.steps[fi].status = singleResult.exitCode === 0 ? "complete" : "failed";
2308
+ statusPayload.steps[fi].status = timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
2005
2309
  statusPayload.steps[fi].endedAt = taskEndTime;
2006
2310
  statusPayload.steps[fi].durationMs = taskDuration;
2007
- 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;
2008
2313
  statusPayload.steps[fi].model = singleResult.model;
2009
2314
  statusPayload.steps[fi].thinking = resolveEffectiveThinking(singleResult.model, statusPayload.steps[fi].thinking);
2010
2315
  statusPayload.steps[fi].attemptedModels = singleResult.attemptedModels;
2011
2316
  statusPayload.steps[fi].modelAttempts = singleResult.modelAttempts;
2012
- 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;
2013
2319
  statusPayload.steps[fi].structuredOutput = singleResult.structuredOutput;
2014
2320
  statusPayload.steps[fi].structuredOutputPath = singleResult.structuredOutputPath;
2015
2321
  statusPayload.steps[fi].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
@@ -2018,9 +2324,9 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2018
2324
  writeStatusPayload();
2019
2325
 
2020
2326
  appendJsonl(eventsPath, JSON.stringify({
2021
- 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",
2022
2328
  ts: taskEndTime, runId: id, stepIndex: fi, agent: task.agent,
2023
- exitCode: singleResult.exitCode, durationMs: taskDuration,
2329
+ exitCode: timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode, durationMs: taskDuration,
2024
2330
  }));
2025
2331
  if (singleResult.completionGuardTriggered) {
2026
2332
  const event = buildControlEvent({
@@ -2037,8 +2343,9 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2037
2343
  }
2038
2344
 
2039
2345
  if (singleResult.exitCode !== 0 && failFast) aborted = true;
2040
- 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 };
2041
2347
  },
2348
+ globalSemaphore,
2042
2349
  );
2043
2350
 
2044
2351
  flatIndex += group.parallel.length;
@@ -2066,14 +2373,17 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2066
2373
  agent: pr.agent,
2067
2374
  output: pr.output,
2068
2375
  error: pr.error,
2069
- success: pr.exitCode === 0,
2070
- exitCode: pr.exitCode,
2376
+ success: pr.interrupted !== true && pr.exitCode === 0,
2377
+ exitCode: pr.interrupted === true ? 0 : pr.exitCode,
2071
2378
  skipped: pr.skipped,
2379
+ interrupted: pr.interrupted,
2380
+ timedOut: pr.timedOut,
2072
2381
  sessionFile: pr.sessionFile,
2073
2382
  intercomTarget: pr.intercomTarget,
2074
2383
  model: pr.model,
2075
2384
  attemptedModels: pr.attemptedModels,
2076
2385
  modelAttempts: pr.modelAttempts,
2386
+ totalCost: pr.totalCost,
2077
2387
  artifactPaths: pr.artifactPaths,
2078
2388
  structuredOutput: pr.structuredOutput,
2079
2389
  structuredOutputPath: pr.structuredOutputPath,
@@ -2153,11 +2463,13 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2153
2463
  childIntercomTarget: config.childIntercomTargets?.[flatIndex],
2154
2464
  orchestratorIntercomTarget: config.controlIntercomTarget,
2155
2465
  nestedRoute: config.nestedRoute,
2156
- registerInterrupt: (interrupt) => {
2157
- activeChildInterrupt = interrupt;
2158
- },
2466
+ registerInterrupt: (interrupt) => registerStepInterrupt(flatIndex, interrupt),
2467
+ registerTimeout: (interrupt) => registerStepTimeout(flatIndex, interrupt),
2468
+ timeoutSignal: timeoutAbortController.signal,
2469
+ timeoutMessage,
2159
2470
  onAttemptStart: (attempt) => updateStepModel(flatIndex, attempt.model, attempt.thinking),
2160
2471
  onChildEvent: (event) => updateStepFromChildEvent(flatIndex, event),
2472
+ skipAcceptance: () => timedOut,
2161
2473
  });
2162
2474
  if (seqStep.sessionFile) {
2163
2475
  latestSessionFile = seqStep.sessionFile;
@@ -2166,20 +2478,23 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2166
2478
  previousOutput = singleResult.output;
2167
2479
  results.push({
2168
2480
  agent: singleResult.agent,
2169
- output: singleResult.output,
2170
- error: singleResult.error,
2171
- success: singleResult.exitCode === 0,
2172
- 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,
2173
2485
  sessionFile: singleResult.sessionFile,
2174
2486
  intercomTarget: singleResult.intercomTarget,
2175
2487
  model: singleResult.model,
2176
2488
  attemptedModels: singleResult.attemptedModels,
2177
2489
  modelAttempts: singleResult.modelAttempts,
2490
+ totalCost: singleResult.totalCost,
2178
2491
  artifactPaths: singleResult.artifactPaths,
2179
2492
  structuredOutput: singleResult.structuredOutput,
2180
2493
  structuredOutputPath: singleResult.structuredOutputPath,
2181
2494
  structuredOutputSchemaPath: singleResult.structuredOutputSchemaPath,
2182
2495
  acceptance: singleResult.acceptance,
2496
+ interrupted: singleResult.interrupted,
2497
+ timedOut: timedOut || singleResult.timedOut ? true : undefined,
2183
2498
  });
2184
2499
  if (seqStep.outputName) {
2185
2500
  outputs[seqStep.outputName] = outputEntryFromAsyncResult({
@@ -2212,15 +2527,18 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2212
2527
  }
2213
2528
 
2214
2529
  const stepEndTime = Date.now();
2215
- 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";
2216
2532
  statusPayload.steps[flatIndex].endedAt = stepEndTime;
2217
2533
  statusPayload.steps[flatIndex].durationMs = stepEndTime - stepStartTime;
2218
- 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;
2219
2536
  statusPayload.steps[flatIndex].model = singleResult.model;
2220
2537
  statusPayload.steps[flatIndex].thinking = resolveEffectiveThinking(singleResult.model, statusPayload.steps[flatIndex].thinking);
2221
2538
  statusPayload.steps[flatIndex].attemptedModels = singleResult.attemptedModels;
2222
2539
  statusPayload.steps[flatIndex].modelAttempts = singleResult.modelAttempts;
2223
- 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;
2224
2542
  statusPayload.steps[flatIndex].structuredOutput = singleResult.structuredOutput;
2225
2543
  statusPayload.steps[flatIndex].structuredOutputPath = singleResult.structuredOutputPath;
2226
2544
  statusPayload.steps[flatIndex].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
@@ -2233,12 +2551,12 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2233
2551
  writeStatusPayload();
2234
2552
 
2235
2553
  appendJsonl(eventsPath, JSON.stringify({
2236
- 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",
2237
2555
  ts: stepEndTime,
2238
2556
  runId: id,
2239
2557
  stepIndex: flatIndex,
2240
2558
  agent: seqStep.agent,
2241
- exitCode: singleResult.exitCode,
2559
+ exitCode: timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode,
2242
2560
  durationMs: stepEndTime - stepStartTime,
2243
2561
  tokens: stepTokens,
2244
2562
  }));
@@ -2277,6 +2595,12 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2277
2595
  }
2278
2596
 
2279
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;
2280
2604
  const finalFlatAgents = statusPayload.steps.map((step) => step.agent);
2281
2605
  const agentName = finalFlatAgents.length === 1
2282
2606
  ? finalFlatAgents[0]!
@@ -2317,13 +2641,23 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2317
2641
  clearInterval(activityTimer);
2318
2642
  activityTimer = undefined;
2319
2643
  }
2644
+ if (timeoutTimer) {
2645
+ clearTimeout(timeoutTimer);
2646
+ timeoutTimer = undefined;
2647
+ }
2648
+ disposeControlInbox();
2320
2649
  const effectiveSessionFile = sessionFile ?? latestSessionFile;
2321
2650
  const runEndedAt = Date.now();
2322
- 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";
2323
2652
  statusPayload.activityState = undefined;
2653
+ if (timedOut) {
2654
+ statusPayload.timedOut = true;
2655
+ statusPayload.error = timeoutMessage ?? "Subagent timed out.";
2656
+ }
2324
2657
  statusPayload.endedAt = runEndedAt;
2325
2658
  statusPayload.lastUpdate = runEndedAt;
2326
2659
  statusPayload.sessionFile = effectiveSessionFile;
2660
+ statusPayload.totalCost = finalTotalCost;
2327
2661
  statusPayload.shareUrl = shareUrl;
2328
2662
  statusPayload.gistUrl = gistUrl;
2329
2663
  statusPayload.shareError = shareError;
@@ -2338,10 +2672,13 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2338
2672
  eventsPath,
2339
2673
  JSON.stringify({
2340
2674
  type: "subagent.run.completed",
2675
+ lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
2341
2676
  ts: runEndedAt,
2342
2677
  runId: id,
2343
2678
  status: statusPayload.state,
2344
2679
  durationMs: runEndedAt - overallStartTime,
2680
+ totalTokens: statusPayload.totalTokens,
2681
+ totalCost: finalTotalCost,
2345
2682
  }),
2346
2683
  );
2347
2684
  writeRunLog(logPath, {
@@ -2365,23 +2702,30 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2365
2702
 
2366
2703
  try {
2367
2704
  writeAtomicJson(resultPath, {
2705
+ lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
2368
2706
  id,
2369
2707
  agent: agentName,
2370
2708
  mode: resultMode,
2371
- success: !interrupted && results.every((r) => r.success),
2372
- state: interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed",
2373
- 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." } : {}),
2374
2715
  results: results.map((r) => ({
2375
2716
  agent: r.agent,
2376
2717
  output: r.output,
2377
2718
  error: r.error,
2378
2719
  success: r.success,
2379
2720
  skipped: r.skipped || undefined,
2721
+ interrupted: r.interrupted || undefined,
2722
+ timedOut: r.timedOut || undefined,
2380
2723
  sessionFile: r.sessionFile,
2381
2724
  intercomTarget: r.intercomTarget,
2382
2725
  model: r.model,
2383
2726
  attemptedModels: r.attemptedModels,
2384
2727
  modelAttempts: r.modelAttempts,
2728
+ totalCost: r.totalCost,
2385
2729
  artifactPaths: r.artifactPaths,
2386
2730
  truncated: r.truncated,
2387
2731
  structuredOutput: r.structuredOutput,
@@ -2391,9 +2735,11 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2391
2735
  })),
2392
2736
  outputs,
2393
2737
  workflowGraph: statusPayload.workflowGraph,
2394
- exitCode: interrupted || results.every((r) => r.success) ? 0 : 1,
2738
+ exitCode: timedOut ? 1 : interrupted || results.every((r) => r.success) ? 0 : 1,
2395
2739
  timestamp: runEndedAt,
2396
2740
  durationMs: runEndedAt - overallStartTime,
2741
+ totalTokens: statusPayload.totalTokens,
2742
+ totalCost: finalTotalCost,
2397
2743
  truncated,
2398
2744
  artifactsDir,
2399
2745
  cwd,