pi-subagents 0.48.0 → 0.50.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 (88) hide show
  1. package/CHANGELOG.md +82 -19
  2. package/agents/oracle.md +7 -5
  3. package/agents/researcher.md +1 -1
  4. package/agents/reviewer.md +2 -2
  5. package/agents/scout.md +1 -1
  6. package/agents/worker.md +1 -1
  7. package/docs/agents.md +3 -0
  8. package/docs/configuration.md +75 -8
  9. package/docs/extension-api.md +36 -0
  10. package/docs/missions.md +5 -3
  11. package/docs/observability.md +42 -2
  12. package/docs/tool-reference.md +19 -2
  13. package/docs/workflows.md +2 -2
  14. package/package.json +1 -1
  15. package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
  16. package/skills/pi-subagents/references/execution-controls.md +5 -4
  17. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  18. package/skills/pi-subagents/references/prompting-and-roles.md +31 -15
  19. package/src/agents/agent-serializer.ts +2 -0
  20. package/src/agents/agents.ts +37 -12
  21. package/src/api/external-runs.ts +174 -84
  22. package/src/api/preflight.ts +13 -7
  23. package/src/extension/config.ts +53 -0
  24. package/src/extension/index.ts +62 -9
  25. package/src/extension/public-execution.ts +32 -4
  26. package/src/extension/rpc.ts +5 -1
  27. package/src/extension/schemas.ts +10 -8
  28. package/src/extension/tool-description.ts +13 -9
  29. package/src/inspectors/herdr/actions.ts +13 -8
  30. package/src/inspectors/herdr/inspector-runner.ts +16 -3
  31. package/src/inspectors/herdr/project-panes.ts +2 -6
  32. package/src/inspectors/herdr/shell-command.ts +16 -0
  33. package/src/intercom/intercom-bridge.ts +5 -4
  34. package/src/intercom/native-supervisor-channel.ts +19 -42
  35. package/src/missions/goal-driver.ts +3 -1
  36. package/src/missions/store.ts +8 -3
  37. package/src/runs/background/active-async-capacity.ts +82 -25
  38. package/src/runs/background/active-run-index.ts +71 -1
  39. package/src/runs/background/async-execution.ts +73 -43
  40. package/src/runs/background/async-job-tracker.ts +5 -0
  41. package/src/runs/background/async-resume.ts +14 -6
  42. package/src/runs/background/async-status-snapshot.ts +277 -0
  43. package/src/runs/background/async-status.ts +8 -3
  44. package/src/runs/background/chain-root-attachment.ts +2 -2
  45. package/src/runs/background/completion-replay.ts +11 -1
  46. package/src/runs/background/fleet-view.ts +21 -6
  47. package/src/runs/background/result-files.ts +437 -0
  48. package/src/runs/background/result-watcher.ts +188 -41
  49. package/src/runs/background/resume-guidance.ts +27 -7
  50. package/src/runs/background/retained-children.ts +75 -18
  51. package/src/runs/background/run-id-resolver.ts +30 -24
  52. package/src/runs/background/run-status.ts +101 -4
  53. package/src/runs/background/scheduled-runs.ts +54 -28
  54. package/src/runs/background/stale-run-reconciler.ts +27 -13
  55. package/src/runs/background/subagent-runner.ts +298 -33
  56. package/src/runs/background/subagent-wait.ts +2 -0
  57. package/src/runs/background/wait-completions.ts +5 -2
  58. package/src/runs/foreground/async-dismiss-action.ts +2 -1
  59. package/src/runs/foreground/chain-execution.ts +16 -0
  60. package/src/runs/foreground/execution.ts +219 -15
  61. package/src/runs/foreground/prompt-audit.ts +4 -3
  62. package/src/runs/foreground/subagent-executor.ts +328 -43
  63. package/src/runs/shared/completion-guard.ts +107 -1
  64. package/src/runs/shared/external-cli-runner.ts +4 -0
  65. package/src/runs/shared/llm-intent-arbiter.ts +39 -23
  66. package/src/runs/shared/model-fallback.ts +16 -2
  67. package/src/runs/shared/nested-events.ts +66 -62
  68. package/src/runs/shared/orca-progress-tabs.ts +375 -0
  69. package/src/runs/shared/parallel-utils.ts +2 -0
  70. package/src/runs/shared/subagent-control.ts +15 -0
  71. package/src/runs/shared/subagent-prompt-runtime.ts +1 -9
  72. package/src/runs/shared/subagent-startup-retry.ts +12 -0
  73. package/src/runs/shared/tool-timeout.ts +93 -0
  74. package/src/shared/agent-stream-options.ts +5 -0
  75. package/src/shared/artifacts.ts +2 -6
  76. package/src/shared/display-text.ts +50 -0
  77. package/src/shared/node-executable.ts +21 -0
  78. package/src/shared/types.ts +51 -5
  79. package/src/slash/slash-commands.ts +34 -25
  80. package/src/slash/slash-live-state.ts +3 -0
  81. package/src/tui/fleet-status.ts +160 -45
  82. package/src/tui/fleet-transcript.ts +1 -48
  83. package/src/tui/fleet.ts +128 -16
  84. package/src/tui/render.ts +122 -44
  85. package/src/watchdog/permission-arbiter.ts +2 -1
  86. package/src/watchdog/review.ts +4 -3
  87. package/src/workflows/chat-progress.ts +10 -2
  88. package/src/workflows/scripted-workflow.ts +272 -76
@@ -5,6 +5,7 @@ import * as path from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
6
  import type { Message } from "@earendil-works/pi-ai";
7
7
  import { writeAtomicJson } from "../../shared/atomic-json.ts";
8
+ import { writeAsyncResultFile, writePendingAsyncResultFile } from "./result-files.ts";
8
9
  import { createFileCoalescer } from "../../shared/file-coalescer.ts";
9
10
  import { isActiveAsyncState, updateActiveRunIndex } from "./active-run-index.ts";
10
11
  import { createChildTranscriptWriter, type ChildTranscriptWriter } from "../../shared/child-transcript.ts";
@@ -57,6 +58,7 @@ import {
57
58
  claimControlNotification,
58
59
  formatControlIntercomMessage,
59
60
  formatControlNoticeMessage,
61
+ shouldEmitOpenToolAttention,
60
62
  } from "../shared/subagent-control.ts";
61
63
  import {
62
64
  type RunnerSubagentStep as SubagentStep,
@@ -83,6 +85,7 @@ import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDi
83
85
  import { formatModelAttemptNote, isRetryableModelFailure } from "../shared/model-fallback.ts";
84
86
  import {
85
87
  SUBAGENT_STARTUP_RETRY_DELAYS_MS,
88
+ formatSubagentExtensionConflictError,
86
89
  formatSubagentStartupRetryExhaustedError,
87
90
  formatSubagentStartupRetryNote,
88
91
  isRetryableSubagentStartupFailure,
@@ -126,12 +129,14 @@ import { waitForImportedAsyncRoot } from "./chain-root-attachment.ts";
126
129
  import { appendRunnerStepsToStatus, consumeChainAppendRequests, countPendingChainAppendRequests, statusStepDescription } from "./chain-append.ts";
127
130
  import { appendTurnBudgetSystemPrompt, formatTurnBudgetOutput, initialTurnBudgetState, turnBudgetDecision, turnBudgetDeferredNote, turnBudgetDeferredState, turnBudgetExceededMessage, turnBudgetSoftNote, turnBudgetState } from "../shared/turn-budget.ts";
128
131
  import { initialToolBudgetState, toolBudgetState } from "../shared/tool-budget.ts";
132
+ import { effectiveToolTimeoutMs, formatToolTimeoutMessage, toolTimeoutCallKey } from "../shared/tool-timeout.ts";
129
133
  import { usageBudgetExceededMessage, usageBudgetState } from "../shared/usage-budget.ts";
130
134
  import { formatParallelHandoffError, formatParallelHandoffReference, parallelHandoffPath, writeParallelHandoffGroup, writePendingParallelHandoff } from "../shared/parallel-handoff.ts";
131
135
  import { resolveWatchdogConfig } from "../../watchdog/settings.ts";
132
136
  import { createBoundedByteTail, createBoundedLineReader, formatProtocolOutputLimit, MAX_CHILD_STDERR_BYTES, PI_AGGREGATE_EVENT_PROJECTOR, projectChildLifecycle, type ChildLifecycleAction, type ProtocolOutputLimit } from "../shared/child-protocol.ts";
133
137
  import { acquireSessionLease, type SessionLeaseRequest } from "../shared/session-lease.ts";
134
138
  import { buildExternalCliPrompt, runExternalCli } from "../shared/external-cli-runner.ts";
139
+ import { createOrcaProgressTab, type OrcaProgressTab } from "../shared/orca-progress-tabs.ts";
135
140
  import { decodeSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
136
141
  import {
137
142
  CHILD_WATCHDOG_CONFIG_ENV,
@@ -143,6 +148,8 @@ import {
143
148
  type ChildWatchdogStateSnapshot,
144
149
  } from "../../watchdog/child-status.ts";
145
150
 
151
+ const INTERCOM_DETACH_RECEIPT = "Detached for intercom coordination before task completion.";
152
+
146
153
  interface SubagentRunConfig {
147
154
  id: string;
148
155
  steps: RunnerStep[];
@@ -174,6 +181,8 @@ interface SubagentRunConfig {
174
181
  nestedSelf?: { parentRunId: string; parentStepIndex?: number; depth: number; path?: Array<{ runId: string; stepIndex?: number; agent?: string }> };
175
182
  timeoutMs?: number;
176
183
  deadlineAt?: number;
184
+ /** Resolved configured hard per-tool-call timeout (ms); fast tools still have a default when undefined. */
185
+ toolTimeoutMs?: number;
177
186
  turnBudget?: ResolvedTurnBudget;
178
187
  toolBudget?: ResolvedToolBudget;
179
188
  usageBudget?: UsageBudgetConfig;
@@ -539,6 +548,9 @@ function runPiStreaming(
539
548
  stopMessage?: string,
540
549
  registerTurnBudgetAbort?: (abort: ((message: string, state?: TurnBudgetState) => void) | undefined) => void,
541
550
  onWriterProcess?: (writer: { state: "none" | "spawning" } | { state: "running"; pid: number }) => void,
551
+ toolTimeoutMs?: number,
552
+ runDeadlineAt?: number,
553
+ orcaProgressTab?: OrcaProgressTab,
542
554
  ): Promise<RunPiStreamingResult> {
543
555
  return new Promise((resolve) => {
544
556
  const startedAt = Date.now();
@@ -595,6 +607,7 @@ function runPiStreaming(
595
607
  const writeOutputLine = (line: string) => {
596
608
  if (!line.trim()) return;
597
609
  outputStream.write(`${line}\n`);
610
+ orcaProgressTab?.append(`${line}\n`);
598
611
  };
599
612
 
600
613
  const writeOutputText = (text: string) => {
@@ -672,8 +685,14 @@ function runPiStreaming(
672
685
 
673
686
  onChildEvent?.(event);
674
687
 
688
+ if (event.type === "tool_execution_end") {
689
+ clearActiveToolTimeout(event);
690
+ return;
691
+ }
692
+
675
693
  if (event.type === "tool_execution_start" && event.toolName) {
676
694
  toolCount += 1;
695
+ armToolTimeout({ toolCallId: (event as { toolCallId?: unknown }).toolCallId, toolName: event.toolName });
677
696
  if (event.toolName === "structured_output") {
678
697
  structuredOutputToolInvoked = true;
679
698
  structuredOutputMessageStartIndex = messages.length;
@@ -769,6 +788,7 @@ function runPiStreaming(
769
788
  stderrTail.push(chunk);
770
789
  stderrReader.push(chunk);
771
790
  outputStream.write(chunk);
791
+ orcaProgressTab?.append(chunk.toString("utf-8"));
772
792
  });
773
793
  registerInterrupt?.(() => {
774
794
  if (settled || timedOut || stopped) return;
@@ -779,14 +799,60 @@ function runPiStreaming(
779
799
  if (!settled && !timedOut && !stopped) trySignalChild(child, "SIGTERM");
780
800
  }, 1000).unref?.();
781
801
  });
782
- registerTimeout?.(() => {
802
+ const terminateForTimeout = (message: string): void => {
783
803
  if (settled || timedOut || stopped) return;
784
804
  timedOut = true;
805
+ // runPiStreaming's terminal result derives the timeout error from this
806
+ // message, so retain the tool-specific reason through finalization.
807
+ timeoutMessage = message;
785
808
  interrupted = false;
786
- error = timeoutMessage ?? "Subagent timed out.";
809
+ error = message;
787
810
  if (processTreeController) void processTreeController.terminate();
788
811
  else trySignalChild(child, "SIGTERM");
789
- });
812
+ };
813
+ let toolTimeoutSequence = 0;
814
+ const activeToolTimeouts = new Map<string, { toolName: string; timer: ReturnType<typeof setTimeout> }>();
815
+ const activeToolTimeoutKeysByName = new Map<string, string[]>();
816
+ const removeToolTimeoutKey = (key: string): void => {
817
+ const active = activeToolTimeouts.get(key);
818
+ if (!active) return;
819
+ clearTimeout(active.timer);
820
+ activeToolTimeouts.delete(key);
821
+ const keys = activeToolTimeoutKeysByName.get(active.toolName)?.filter((candidate) => candidate !== key) ?? [];
822
+ if (keys.length > 0) activeToolTimeoutKeysByName.set(active.toolName, keys);
823
+ else activeToolTimeoutKeysByName.delete(active.toolName);
824
+ };
825
+ const clearActiveToolTimeout = (event: { toolCallId?: unknown; toolName?: unknown }): void => {
826
+ const key = typeof event.toolCallId === "string" && event.toolCallId.length > 0
827
+ ? `id:${event.toolCallId}`
828
+ : typeof event.toolName === "string"
829
+ ? activeToolTimeoutKeysByName.get(event.toolName)?.[0]
830
+ : activeToolTimeouts.size === 1
831
+ ? [...activeToolTimeouts.keys()][0]
832
+ : undefined;
833
+ if (key) removeToolTimeoutKey(key);
834
+ };
835
+ const clearAllToolTimeouts = (): void => {
836
+ for (const key of [...activeToolTimeouts.keys()]) removeToolTimeoutKey(key);
837
+ };
838
+ const armToolTimeout = (event: { toolCallId?: unknown; toolName: string }): void => {
839
+ const timeoutForTool = effectiveToolTimeoutMs(event.toolName, toolTimeoutMs);
840
+ if (timeoutForTool === undefined) return;
841
+ const runRemaining = runDeadlineAt === undefined ? undefined : Math.max(0, runDeadlineAt - Date.now());
842
+ if (runRemaining !== undefined && timeoutForTool >= runRemaining) return;
843
+ const key = toolTimeoutCallKey(event, ++toolTimeoutSequence);
844
+ const toolName = event.toolName;
845
+ const timer = setTimeout(() => {
846
+ removeToolTimeoutKey(key);
847
+ terminateForTimeout(formatToolTimeoutMessage(toolName, timeoutForTool));
848
+ }, timeoutForTool);
849
+ timer.unref?.();
850
+ activeToolTimeouts.set(key, { toolName, timer });
851
+ const keys = activeToolTimeoutKeysByName.get(toolName) ?? [];
852
+ keys.push(key);
853
+ activeToolTimeoutKeysByName.set(toolName, keys);
854
+ };
855
+ registerTimeout?.(() => terminateForTimeout(timeoutMessage ?? "Subagent timed out."));
790
856
  registerStop?.(() => {
791
857
  if (settled || timedOut || stopped) return;
792
858
  stopped = true;
@@ -813,6 +879,7 @@ function runPiStreaming(
813
879
  turnBudgetHardKillTimer.unref?.();
814
880
  });
815
881
  const clearDrainTimers = () => {
882
+ clearAllToolTimeouts();
816
883
  if (finalDrainTimer) {
817
884
  clearTimeout(finalDrainTimer);
818
885
  finalDrainTimer = undefined;
@@ -1102,6 +1169,10 @@ interface SingleStepContext {
1102
1169
  stopSignal?: AbortSignal;
1103
1170
  timeoutMessage?: string;
1104
1171
  stopMessage?: string;
1172
+ /** Resolved configured hard per-tool-call timeout (ms); fast tools still have a default when undefined. */
1173
+ toolTimeoutMs?: number;
1174
+ /** Effective step deadline (Date.now() + effective timeout) when a run budget exists. */
1175
+ deadlineAt?: number;
1105
1176
  turnBudget?: ResolvedTurnBudget;
1106
1177
  childIntercomTarget?: string;
1107
1178
  orchestratorIntercomTarget?: string;
@@ -1113,10 +1184,11 @@ interface SingleStepContext {
1113
1184
  onWriterProcess?: (writer: { state: "none" | "spawning" } | { state: "running"; pid: number }) => void;
1114
1185
  onExternalProcess?: (process: ExternalProcessStatus) => void;
1115
1186
  skipAcceptance?: () => boolean;
1187
+ orcaProgressTab?: OrcaProgressTab;
1116
1188
  }
1117
1189
 
1118
1190
  /** Run a single pi agent step, returning output and metadata */
1119
- async function runSingleStep(
1191
+ async function runSingleStepInner(
1120
1192
  step: SubagentStep,
1121
1193
  ctx: SingleStepContext,
1122
1194
  ): Promise<StepResult & { completionGuardTriggered?: boolean }> {
@@ -1245,6 +1317,8 @@ async function runSingleStep(
1245
1317
  timeoutMessage: ctx.timeoutMessage,
1246
1318
  stopMessage: ctx.stopMessage,
1247
1319
  onProcess: ctx.onExternalProcess,
1320
+ onStdout: (chunk) => ctx.orcaProgressTab?.append(chunk.toString("utf-8")),
1321
+ onStderr: (chunk) => ctx.orcaProgressTab?.append(chunk.toString("utf-8")),
1248
1322
  }));
1249
1323
  try { fs.writeFileSync(ctx.outputFile, external.output, "utf-8"); } catch { /* Observability output is best-effort. */ }
1250
1324
  const resolvedOutput = step.outputPath && external.exitCode === 0
@@ -1431,6 +1505,9 @@ async function runSingleStep(
1431
1505
  ctx.stopMessage,
1432
1506
  ctx.registerTurnBudgetAbort,
1433
1507
  ctx.onWriterProcess,
1508
+ ctx.toolTimeoutMs,
1509
+ ctx.deadlineAt,
1510
+ ctx.orcaProgressTab,
1434
1511
  );
1435
1512
  if (run.processCloseObservedAt !== undefined) {
1436
1513
  writerProcesses.push({
@@ -1538,15 +1615,21 @@ async function runSingleStep(
1538
1615
  stopped: run.stopped,
1539
1616
  turnBudgetExceeded: run.turnBudgetExceeded,
1540
1617
  })) ? formatProcessSignalError(run.processSignal!) : undefined;
1541
- const error = toolAvailabilityError
1542
- ?? completionGuardError
1543
- ?? structuredError
1544
- ?? emptyOutputError
1545
- ?? (hiddenError?.hasError
1546
- ? hiddenError.details
1547
- ? `${hiddenError.errorType} failed (exit ${effectiveExitCode}): ${hiddenError.details}`
1548
- : `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
1549
- : run.error || signalError || (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined));
1618
+ const error = formatSubagentExtensionConflictError(
1619
+ toolAvailabilityError
1620
+ ?? completionGuardError
1621
+ ?? structuredError
1622
+ ?? emptyOutputError
1623
+ ?? (hiddenError?.hasError
1624
+ ? hiddenError.details
1625
+ ? `${hiddenError.errorType} failed (exit ${effectiveExitCode}): ${hiddenError.details}`
1626
+ : `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
1627
+ : run.error || signalError || (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined)),
1628
+ {
1629
+ agent: step.agent,
1630
+ ambientExtensionsEnabled: launchResolvedExtensions?.disableAmbientExtensions === false,
1631
+ },
1632
+ );
1550
1633
  const attempt: ModelAttempt = omitUndefinedProperties({
1551
1634
  model: candidate ?? run.model ?? step.model ?? "default",
1552
1635
  success: effectiveExitCode === 0 && !error,
@@ -1693,15 +1776,16 @@ async function runSingleStep(
1693
1776
  const acceptanceFailure = effectiveAcceptance ? acceptanceFailureMessage(effectiveAcceptance) : undefined;
1694
1777
  const acceptanceCanFailRun = acceptanceFailure && effectiveAcceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted && !timedOutAfterAcceptance && !stoppedAfterAcceptance && !turnBudgetExceeded && !isAgentContractV1(step.agentContract);
1695
1778
  const effectiveFinalExitCode = timedOutAfterAcceptance || stoppedAfterAcceptance || turnBudgetExceeded ? 1 : acceptanceCanFailRun ? 1 : finalResult?.exitCode ?? 1;
1779
+ const intercomDetachReceipt = finalResult?.finalOutput === INTERCOM_DETACH_RECEIPT;
1696
1780
  const effectiveFinalError = stoppedAfterAcceptance
1697
1781
  ? ctx.stopMessage ?? "Subagent stopped by user."
1698
1782
  : timedOutAfterAcceptance
1699
- ? ctx.timeoutMessage ?? "Subagent timed out."
1783
+ ? finalResult?.error ?? ctx.timeoutMessage ?? "Subagent timed out."
1700
1784
  : turnBudgetExceeded
1701
1785
  ? finalResult?.error ?? (turnBudget ? turnBudgetExceededMessage(turnBudget, turnBudget.turnCount) : "Subagent exceeded turn budget.")
1702
1786
  : acceptanceCanFailRun
1703
1787
  ? (finalResult?.error ? `${finalResult.error}\n${acceptanceFailure}` : acceptanceFailure)
1704
- : finalResult?.error;
1788
+ : finalResult?.error ?? (intercomDetachReceipt ? INTERCOM_DETACH_RECEIPT : undefined);
1705
1789
 
1706
1790
  const artifactErrors = artifactPaths && ctx.artifactConfig?.enabled !== false
1707
1791
  ? persistStepArtifacts({
@@ -1781,6 +1865,27 @@ async function runSingleStep(
1781
1865
  return isAgentContractV1(step.agentContract) ? attachContractProjections(result as unknown as import("../../shared/types.ts").SingleResult) as unknown as typeof result : result;
1782
1866
  }
1783
1867
 
1868
+ async function runSingleStep(
1869
+ step: SubagentStep,
1870
+ ctx: SingleStepContext,
1871
+ ): Promise<StepResult & { completionGuardTriggered?: boolean }> {
1872
+ if (step.importAsyncRoot) return runSingleStepInner(step, ctx);
1873
+ const orcaProgressTab = createOrcaProgressTab({
1874
+ cwd: step.cwd ?? ctx.cwd,
1875
+ runId: ctx.id,
1876
+ agent: step.agent,
1877
+ index: ctx.flatIndex,
1878
+ });
1879
+ try {
1880
+ const result = await runSingleStepInner(step, { ...ctx, orcaProgressTab });
1881
+ orcaProgressTab?.finish(result.stopped ? "stopped" : result.exitCode === 0 && !result.error ? "completed" : "failed", result.sessionFile);
1882
+ return result;
1883
+ } catch (error) {
1884
+ orcaProgressTab?.finish("failed");
1885
+ throw error;
1886
+ }
1887
+ }
1888
+
1784
1889
  type RunnerStatusStep = NonNullable<AsyncStatus["steps"]>[number] & {
1785
1890
  exitCode?: number | null;
1786
1891
  description?: string;
@@ -2005,6 +2110,7 @@ async function runSingleStepWithTimeout(
2005
2110
  return await runSingleStep(step, {
2006
2111
  ...ctx,
2007
2112
  registerTimeout,
2113
+ deadlineAt: Date.now() + timeoutMs,
2008
2114
  timeoutSignal: combinedAbortSignal([ctx.timeoutSignal, timeoutController.signal]),
2009
2115
  timeoutMessage,
2010
2116
  });
@@ -2199,7 +2305,7 @@ async function runSubagent(
2199
2305
 
2200
2306
  fs.mkdirSync(asyncDir, { recursive: true });
2201
2307
  writeAtomicJson(statusPath, statusPayload);
2202
- updateActiveRunIndex(asyncDir, statusPayload.state);
2308
+ updateActiveRunIndex(asyncDir, statusPayload.state, statusPayload.toolCallId);
2203
2309
  let pendingParallelUsageCost: CostSummary = { inputTokens: 0, outputTokens: 0, costUsd: 0 };
2204
2310
  const currentUsageTotals = (): CostSummary => {
2205
2311
  const cost = results.reduce<CostSummary>((sum, result) => ({
@@ -2272,13 +2378,70 @@ async function runSubagent(
2272
2378
  for (const node of graph.nodes) updateNode(node);
2273
2379
  statusPayload.workflowGraph = graph;
2274
2380
  };
2381
+ let finalResultCommitted = false;
2275
2382
  let lastIndexedActiveState = isActiveAsyncState(statusPayload.state);
2383
+ const statusResultState = (): AsyncStatus["state"] | undefined => {
2384
+ if (statusPayload.state === "running" || statusPayload.state === "queued") return undefined;
2385
+ if (statusPayload.state === "paused" && statusPayload.checkpoint?.status === "pending") return undefined;
2386
+ return statusPayload.state;
2387
+ };
2388
+ const statusResultSummary = (state: AsyncStatus["state"]): string => {
2389
+ if (statusPayload.error) return statusPayload.error;
2390
+ if (state === "paused") return "Paused after interrupt. Waiting for explicit next action.";
2391
+ if (state === "stopped") return stopMessage;
2392
+ if (state === "rejected") return statusPayload.checkpoint?.name ? `Checkpoint '${statusPayload.checkpoint.name}' rejected.` : "Subagent rejected.";
2393
+ return state === "complete" ? "Subagent completed." : "Subagent failed.";
2394
+ };
2395
+ const statusResultSuccess = (state: AsyncStatus["state"], step: RunnerStatusStep): boolean | undefined => {
2396
+ if (step.status === "complete" || step.status === "completed") return true;
2397
+ if (step.status === "failed" || step.status === "stopped" || step.status === "rejected") return false;
2398
+ if (state === "complete") return true;
2399
+ if (state === "failed" || state === "stopped" || state === "rejected") return false;
2400
+ return undefined;
2401
+ };
2402
+ const writeRecoverableStatusResult = (): void => {
2403
+ const state = statusResultState();
2404
+ if (!state || finalResultCommitted || !config.sessionId) return;
2405
+ const now = statusPayload.endedAt ?? statusPayload.lastUpdate ?? Date.now();
2406
+ const summary = statusResultSummary(state);
2407
+ writePendingAsyncResultFile(resultPath, omitUndefinedProperties({
2408
+ lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
2409
+ id,
2410
+ runId: id,
2411
+ agent: statusPayload.steps.length === 1 ? statusPayload.steps[0]!.agent : statusPayload.mode === "parallel" ? `parallel:${statusPayload.steps.map((step) => step.agent).join("+")}` : `chain:${statusPayload.steps.map((step) => step.agent).join("->")}`,
2412
+ mode: statusPayload.mode,
2413
+ success: state === "complete",
2414
+ state,
2415
+ summary,
2416
+ error: state === "failed" || state === "stopped" || state === "rejected" ? summary : undefined,
2417
+ stopped: state === "stopped" ? true : undefined,
2418
+ checkpoint: statusPayload.checkpoint,
2419
+ results: statusPayload.steps.map((step) => omitUndefinedProperties({
2420
+ agent: step.agent,
2421
+ output: step.status === "complete" || step.status === "completed" ? "" : step.error ?? summary,
2422
+ error: step.error,
2423
+ success: statusResultSuccess(state, step),
2424
+ sessionFile: step.sessionFile,
2425
+ model: step.model,
2426
+ attemptedModels: step.attemptedModels,
2427
+ modelAttempts: step.modelAttempts,
2428
+ })),
2429
+ exitCode: state === "complete" || state === "paused" ? 0 : 1,
2430
+ timestamp: now,
2431
+ durationMs: Math.max(0, now - overallStartTime),
2432
+ asyncDir,
2433
+ cwd,
2434
+ sessionId: config.sessionId,
2435
+ sessionFile: statusPayload.sessionFile ?? latestSessionFile,
2436
+ }));
2437
+ };
2276
2438
  const writeStatusPayloadNow = (): void => {
2277
2439
  refreshWorkflowGraph();
2440
+ writeRecoverableStatusResult();
2278
2441
  writeAtomicJson(statusPath, statusPayload);
2279
2442
  const activeState = isActiveAsyncState(statusPayload.state);
2280
- if (activeState && activeState !== lastIndexedActiveState) {
2281
- updateActiveRunIndex(asyncDir, statusPayload.state);
2443
+ if (activeState !== lastIndexedActiveState) {
2444
+ updateActiveRunIndex(asyncDir, statusPayload.state, statusPayload.toolCallId);
2282
2445
  }
2283
2446
  lastIndexedActiveState = activeState;
2284
2447
  emitNestedSelfEvent(statusPayload.state === "running" || statusPayload.state === "queued" ? "subagent.nested.updated" : "subagent.nested.completed");
@@ -2523,6 +2686,72 @@ async function runSubagent(
2523
2686
  const activeLongRunningSteps = new Set<number>();
2524
2687
  const mutatingFailureStates = initialStatusSteps.map(() => createMutatingFailureState());
2525
2688
  const pendingToolResults: Array<{ tool: string; path?: string; mutates: boolean; startedAt?: number } | undefined> = initialStatusSteps.map(() => undefined);
2689
+ type ActiveToolCall = { key: string; tool: string; args: string; startedAt: number; path?: string; blocksSupervisor: boolean };
2690
+ const activeToolCalls = initialStatusSteps.map(() => new Map<string, ActiveToolCall>());
2691
+ const activeToolKeysByName = initialStatusSteps.map(() => new Map<string, string[]>());
2692
+ const activeToolSequences = initialStatusSteps.map(() => 0);
2693
+ const latestActiveToolCall = (flatIndex: number): ActiveToolCall | undefined => [...(activeToolCalls[flatIndex]?.values() ?? [])].sort((left, right) => right.startedAt - left.startedAt)[0];
2694
+ const refreshStepCurrentTool = (flatIndex: number): void => {
2695
+ const step = statusPayload.steps[flatIndex];
2696
+ if (!step) return;
2697
+ const active = latestActiveToolCall(flatIndex);
2698
+ if (!active) {
2699
+ delete step.currentTool;
2700
+ delete step.currentToolArgs;
2701
+ delete step.currentToolStartedAt;
2702
+ delete step.currentPath;
2703
+ return;
2704
+ }
2705
+ step.currentTool = active.tool;
2706
+ step.currentToolArgs = active.args;
2707
+ step.currentToolStartedAt = active.startedAt;
2708
+ setOptionalProperty(step, "currentPath", active.path);
2709
+ };
2710
+ const recordActiveToolCall = (flatIndex: number, event: { toolCallId?: unknown; toolName: string }, input: { argsPreview: string; currentPath?: string; blocksSupervisor: boolean; now: number }): ActiveToolCall => {
2711
+ const sequence = (activeToolSequences[flatIndex] ?? 0) + 1;
2712
+ activeToolSequences[flatIndex] = sequence;
2713
+ const key = toolTimeoutCallKey(event, sequence);
2714
+ const active: ActiveToolCall = {
2715
+ key,
2716
+ tool: event.toolName,
2717
+ args: input.argsPreview,
2718
+ startedAt: input.now,
2719
+ blocksSupervisor: input.blocksSupervisor,
2720
+ ...(input.currentPath !== undefined ? { path: input.currentPath } : {}),
2721
+ };
2722
+ activeToolCalls[flatIndex]?.set(key, active);
2723
+ const keysByName = activeToolKeysByName[flatIndex];
2724
+ const keys = keysByName?.get(active.tool) ?? [];
2725
+ keys.push(key);
2726
+ keysByName?.set(active.tool, keys);
2727
+ refreshStepCurrentTool(flatIndex);
2728
+ return active;
2729
+ };
2730
+ const removeActiveToolCallKey = (flatIndex: number, key: string): ActiveToolCall | undefined => {
2731
+ const calls = activeToolCalls[flatIndex];
2732
+ const active = calls?.get(key);
2733
+ if (!active) return undefined;
2734
+ calls?.delete(key);
2735
+ const keysByName = activeToolKeysByName[flatIndex];
2736
+ const keys = keysByName?.get(active.tool)?.filter((candidate) => candidate !== key) ?? [];
2737
+ if (keys.length > 0) keysByName?.set(active.tool, keys);
2738
+ else keysByName?.delete(active.tool);
2739
+ return active;
2740
+ };
2741
+ const removeActiveToolCall = (flatIndex: number, event: { toolCallId?: unknown; toolName?: unknown }): ActiveToolCall | undefined => {
2742
+ const calls = activeToolCalls[flatIndex];
2743
+ const key = typeof event.toolCallId === "string" && event.toolCallId.length > 0
2744
+ ? `id:${event.toolCallId}`
2745
+ : typeof event.toolName === "string"
2746
+ ? activeToolKeysByName[flatIndex]?.get(event.toolName)?.[0]
2747
+ : calls?.size === 1
2748
+ ? [...calls.keys()][0]
2749
+ : undefined;
2750
+ return key ? removeActiveToolCallKey(flatIndex, key) : undefined;
2751
+ };
2752
+ const openToolAttentionTarget = (flatIndex: number, now: number): ActiveToolCall | undefined => [...(activeToolCalls[flatIndex]?.values() ?? [])]
2753
+ .filter((active) => shouldEmitOpenToolAttention({ config: controlConfig, currentTool: active.tool, currentToolStartedAt: active.startedAt, now }))
2754
+ .sort((left, right) => left.startedAt - right.startedAt)[0];
2526
2755
  const supervisorAttentionSteps = new Map<number, ActivityState | undefined>();
2527
2756
  const mutatingFailureWindowMs = 5 * 60_000;
2528
2757
  const appendControlEvent = (event: ReturnType<typeof buildControlEvent>) => {
@@ -2563,6 +2792,34 @@ async function runSubagent(
2563
2792
  currentActivityState = nextRunState;
2564
2793
  setOptionalProperty(statusPayload, "activityState", nextRunState);
2565
2794
  };
2795
+ const maybeEmitOpenToolAttention = (flatIndex: number, now: number): boolean => {
2796
+ const step = statusPayload.steps[flatIndex];
2797
+ if (!step || step.status !== "running" || step.activityState === "needs_attention") return false;
2798
+ const target = openToolAttentionTarget(flatIndex, now);
2799
+ if (!target) return false;
2800
+ const previous = step.activityState;
2801
+ step.activityState = "needs_attention";
2802
+ statusPayload.activityState = "needs_attention";
2803
+ const toolDurationMs = Math.max(0, now - target.startedAt);
2804
+ appendControlEvent(buildControlEvent(omitUndefinedProperties({
2805
+ type: "needs_attention",
2806
+ from: previous,
2807
+ to: "needs_attention",
2808
+ runId: id,
2809
+ agent: step.agent,
2810
+ index: flatIndex,
2811
+ ts: now,
2812
+ message: `${step.agent} has had tool '${target.tool}' open for ${Math.floor(toolDurationMs / 1000)}s`,
2813
+ reason: "tool_open_threshold",
2814
+ turns: step.turnCount,
2815
+ tokens: step.tokens?.total,
2816
+ toolCount: step.toolCount,
2817
+ currentTool: target.tool,
2818
+ currentToolDurationMs: toolDurationMs,
2819
+ currentPath: target.path,
2820
+ })));
2821
+ return true;
2822
+ };
2566
2823
  const maybeEmitActiveLongRunning = (flatIndex: number, now: number): boolean => {
2567
2824
  if (!controlConfig.enabled || activeLongRunningSteps.has(flatIndex)) return false;
2568
2825
  const step = statusPayload.steps[flatIndex];
@@ -2823,20 +3080,19 @@ async function runSubagent(
2823
3080
  if (event.type === "tool_execution_start" && event.toolName) {
2824
3081
  const mutates = isMutatingTool(event.toolName, event.args);
2825
3082
  const currentPath = resolveCurrentPath(event.toolName, event.args);
3083
+ const argsPreview = extractToolArgsPreview(event.args ?? {});
3084
+ const blocksSupervisor = isBlockingSupervisorTool(event.toolName, event.args);
2826
3085
  step.toolCount = (step.toolCount ?? 0) + 1;
2827
3086
  const configuredToolBudget = flatSteps[flatIndex]?.toolBudget;
2828
3087
  if (configuredToolBudget) {
2829
3088
  step.toolBudget = toolBudgetState(configuredToolBudget, step.toolCount);
2830
3089
  statusPayload.toolBudget = step.toolBudget;
2831
3090
  }
2832
- step.currentTool = event.toolName;
2833
- step.currentToolArgs = extractToolArgsPreview(event.args ?? {});
2834
- step.currentToolStartedAt = now;
2835
- setOptionalProperty(step, "currentPath", currentPath);
3091
+ recordActiveToolCall(flatIndex, { toolCallId: (event as { toolCallId?: unknown }).toolCallId, toolName: event.toolName }, { argsPreview, currentPath, blocksSupervisor, now });
2836
3092
  pendingToolResults[flatIndex] = omitUndefinedProperties({ tool: event.toolName, path: currentPath, mutates, startedAt: now });
2837
3093
  statusPayload.toolCount = (statusPayload.toolCount ?? 0) + 1;
2838
3094
  syncTopLevelCurrentTool();
2839
- if (controlConfig.enabled && isBlockingSupervisorTool(event.toolName, event.args) && step.activityState !== "needs_attention") {
3095
+ if (controlConfig.enabled && blocksSupervisor && step.activityState !== "needs_attention") {
2840
3096
  const previous = step.activityState;
2841
3097
  step.activityState = "needs_attention";
2842
3098
  supervisorAttentionSteps.set(flatIndex, previous);
@@ -2861,16 +3117,15 @@ async function runSubagent(
2861
3117
  })));
2862
3118
  }
2863
3119
  } else if (event.type === "tool_execution_end") {
2864
- if (step.currentTool) {
3120
+ const endedTool = removeActiveToolCall(flatIndex, event);
3121
+ if (endedTool) {
2865
3122
  step.recentTools ??= [];
2866
- step.recentTools.push({ tool: step.currentTool, args: step.currentToolArgs || "", endMs: now });
3123
+ step.recentTools.push({ tool: endedTool.tool, args: endedTool.args, endMs: now });
2867
3124
  }
3125
+ refreshStepCurrentTool(flatIndex);
2868
3126
  const supervisorPreviousActivity = supervisorAttentionSteps.get(flatIndex);
2869
- const clearedSupervisorAttention = supervisorAttentionSteps.delete(flatIndex);
2870
- delete step.currentTool;
2871
- delete step.currentToolArgs;
2872
- delete step.currentToolStartedAt;
2873
- delete step.currentPath;
3127
+ const stillBlockingSupervisor = [...(activeToolCalls[flatIndex]?.values() ?? [])].some((active) => active.blocksSupervisor);
3128
+ const clearedSupervisorAttention = endedTool?.blocksSupervisor && !stillBlockingSupervisor ? supervisorAttentionSteps.delete(flatIndex) : false;
2874
3129
  if (clearedSupervisorAttention && step.activityState === "needs_attention") {
2875
3130
  setOptionalProperty(step, "activityState", supervisorPreviousActivity);
2876
3131
  syncAggregateActivityState();
@@ -2990,6 +3245,8 @@ async function runSubagent(
2990
3245
  })));
2991
3246
  changed = true;
2992
3247
  }
3248
+ } else if (maybeEmitOpenToolAttention(index, now)) {
3249
+ changed = true;
2993
3250
  } else if (maybeEmitActiveLongRunning(index, now)) {
2994
3251
  changed = true;
2995
3252
  }
@@ -3552,6 +3809,7 @@ async function runSubagent(
3552
3809
  timeoutMessage,
3553
3810
  stopMessage,
3554
3811
  turnBudget: config.turnBudget,
3812
+ toolTimeoutMs: task.toolTimeoutMs ?? config.toolTimeoutMs,
3555
3813
  onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking),
3556
3814
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
3557
3815
  onWriterProcess,
@@ -3935,6 +4193,7 @@ async function runSubagent(
3935
4193
  timeoutMessage,
3936
4194
  stopMessage,
3937
4195
  turnBudget: config.turnBudget,
4196
+ toolTimeoutMs: taskForRun.toolTimeoutMs ?? config.toolTimeoutMs,
3938
4197
  onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking),
3939
4198
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
3940
4199
  onWriterProcess,
@@ -4225,6 +4484,7 @@ async function runSubagent(
4225
4484
  timeoutMessage,
4226
4485
  stopMessage,
4227
4486
  turnBudget: config.turnBudget,
4487
+ toolTimeoutMs: seqStep.toolTimeoutMs ?? config.toolTimeoutMs,
4228
4488
  onAttemptStart: (attempt) => updateStepModel(flatIndex, attempt.model, attempt.thinking),
4229
4489
  onChildEvent: (event) => updateStepFromChildEvent(flatIndex, event),
4230
4490
  onWriterProcess,
@@ -4522,9 +4782,8 @@ async function runSubagent(
4522
4782
  statusPayload.error = `Step failed: ${failedStep.agent}`;
4523
4783
  }
4524
4784
  }
4525
- writeStatusPayload();
4526
4785
  try {
4527
- writeAtomicJson(resultPath, {
4786
+ writeAsyncResultFile(resultPath, {
4528
4787
  lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
4529
4788
  id,
4530
4789
  agent: agentName,
@@ -4619,9 +4878,15 @@ async function runSubagent(
4619
4878
  ...(taskIndex !== undefined && { taskIndex }),
4620
4879
  ...(totalTasks !== undefined && { totalTasks }),
4621
4880
  });
4881
+ finalResultCommitted = true;
4622
4882
  } catch (err) {
4623
- console.error(`Failed to write result file ${resultPath}:`, err);
4883
+ const message = `Failed to write result file ${resultPath}: ${err instanceof Error ? err.message : String(err)}`;
4884
+ console.error(message, err);
4885
+ statusPayload.state = "failed";
4886
+ statusPayload.error = message;
4887
+ statusPayload.lastUpdate = Date.now();
4624
4888
  }
4889
+ writeStatusPayload();
4625
4890
  appendJsonl(
4626
4891
  eventsPath,
4627
4892
  JSON.stringify({
@@ -267,6 +267,7 @@ function activeRunsForSession(params: SubagentWaitParams, deps: SubagentWaitDeps
267
267
  resultsDir,
268
268
  kill: deps.kill,
269
269
  now: deps.now,
270
+ includeNested: false,
270
271
  ...(params.id ? { runId: params.id } : {}),
271
272
  });
272
273
  return params.id ? runs.filter((run) => matchesId(run, params.id!)) : runs;
@@ -286,6 +287,7 @@ function allRunsForSession(params: SubagentWaitParams, deps: SubagentWaitDeps):
286
287
  resultsDir,
287
288
  kill: deps.kill,
288
289
  now: deps.now,
290
+ includeNested: false,
289
291
  ...(params.id ? { runId: params.id } : {}),
290
292
  });
291
293
  return params.id ? runs.filter((run) => matchesId(run, params.id!)) : runs;
@@ -1,8 +1,8 @@
1
1
  import * as fs from "node:fs";
2
- import * as path from "node:path";
3
2
  import type { ArtifactPaths, SubagentState, WaitCompletion, WaitCompletionChild } from "../../shared/types.ts";
4
3
  import type { AsyncRunSummary } from "./async-status.ts";
5
4
  import { readCompletionReplay, writeCompletionReplay } from "./completion-replay.ts";
5
+ import { resultFilePath, resultPayloadPathForSessionRun } from "./result-files.ts";
6
6
 
7
7
  function asNonEmptyString(value: unknown): string | undefined {
8
8
  return typeof value === "string" && value ? value : undefined;
@@ -114,7 +114,10 @@ export function collectWaitCompletions(terminal: AsyncRunSummary[], state: Subag
114
114
  completions.push(recorded.completion);
115
115
  continue;
116
116
  }
117
- const resultPath = path.join(resultsDir, `${run.id}.json`);
117
+ const publicResultPath = resultFilePath(resultsDir, run.id);
118
+ const resultPath = run.sessionId
119
+ ? resultPayloadPathForSessionRun(resultsDir, run.sessionId, run.id) ?? publicResultPath
120
+ : publicResultPath;
118
121
  try {
119
122
  const raw = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as Record<string, unknown>;
120
123
  completions.push(toWaitCompletion(raw, run.id));
@@ -6,6 +6,7 @@ import { DIRS, type Details, type SubagentState } from "../../shared/types.ts";
6
6
  import { readStatus } from "../../shared/utils.ts";
7
7
  import { updateActiveRunIndex } from "../background/active-run-index.ts";
8
8
  import { reconcileAsyncRun } from "../background/stale-run-reconciler.ts";
9
+ import { resultFilePath } from "../background/result-files.ts";
9
10
 
10
11
  export function dismissRecoveredWorkflow(
11
12
  state: SubagentState,
@@ -50,7 +51,7 @@ export function dismissRecoveredWorkflow(
50
51
  };
51
52
  }
52
53
  let latestStatus = status;
53
- const resultPath = path.join(DIRS.results, `${status.runId}.json`);
54
+ const resultPath = resultFilePath(DIRS.results, status.runId);
54
55
  if (fs.existsSync(resultPath)) {
55
56
  const reconciled = reconcileAsyncRun(asyncDir).status;
56
57
  if (reconciled && reconciled.state !== "running") {