pi-subagents 0.49.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 (75) hide show
  1. package/CHANGELOG.md +35 -0
  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 +2 -0
  8. package/docs/configuration.md +47 -3
  9. package/docs/extension-api.md +36 -0
  10. package/docs/missions.md +1 -1
  11. package/docs/observability.md +16 -0
  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 +10 -0
  21. package/src/api/external-runs.ts +174 -84
  22. package/src/api/preflight.ts +12 -6
  23. package/src/extension/config.ts +33 -0
  24. package/src/extension/index.ts +48 -5
  25. package/src/extension/public-execution.ts +3 -1
  26. package/src/extension/rpc.ts +5 -1
  27. package/src/extension/schemas.ts +2 -1
  28. package/src/extension/tool-description.ts +4 -2
  29. package/src/inspectors/herdr/actions.ts +3 -7
  30. package/src/inspectors/herdr/project-panes.ts +2 -6
  31. package/src/inspectors/herdr/shell-command.ts +16 -0
  32. package/src/intercom/intercom-bridge.ts +2 -3
  33. package/src/intercom/native-supervisor-channel.ts +19 -42
  34. package/src/missions/goal-driver.ts +3 -1
  35. package/src/runs/background/active-run-index.ts +71 -1
  36. package/src/runs/background/async-execution.ts +49 -7
  37. package/src/runs/background/async-job-tracker.ts +1 -0
  38. package/src/runs/background/async-resume.ts +3 -5
  39. package/src/runs/background/async-status-snapshot.ts +277 -0
  40. package/src/runs/background/async-status.ts +8 -3
  41. package/src/runs/background/chain-root-attachment.ts +2 -2
  42. package/src/runs/background/completion-replay.ts +11 -1
  43. package/src/runs/background/fleet-view.ts +3 -1
  44. package/src/runs/background/result-files.ts +437 -0
  45. package/src/runs/background/result-watcher.ts +188 -41
  46. package/src/runs/background/retained-children.ts +69 -20
  47. package/src/runs/background/run-id-resolver.ts +30 -24
  48. package/src/runs/background/run-status.ts +7 -2
  49. package/src/runs/background/scheduled-runs.ts +54 -28
  50. package/src/runs/background/stale-run-reconciler.ts +27 -13
  51. package/src/runs/background/subagent-runner.ts +294 -32
  52. package/src/runs/background/subagent-wait.ts +2 -0
  53. package/src/runs/background/wait-completions.ts +5 -2
  54. package/src/runs/foreground/async-dismiss-action.ts +2 -1
  55. package/src/runs/foreground/chain-execution.ts +16 -0
  56. package/src/runs/foreground/execution.ts +219 -15
  57. package/src/runs/foreground/subagent-executor.ts +114 -28
  58. package/src/runs/shared/completion-guard.ts +96 -6
  59. package/src/runs/shared/external-cli-runner.ts +4 -0
  60. package/src/runs/shared/model-fallback.ts +16 -2
  61. package/src/runs/shared/nested-events.ts +66 -62
  62. package/src/runs/shared/orca-progress-tabs.ts +375 -0
  63. package/src/runs/shared/parallel-utils.ts +2 -0
  64. package/src/runs/shared/subagent-control.ts +15 -0
  65. package/src/runs/shared/subagent-prompt-runtime.ts +1 -9
  66. package/src/runs/shared/subagent-startup-retry.ts +12 -0
  67. package/src/runs/shared/tool-timeout.ts +93 -0
  68. package/src/shared/types.ts +29 -2
  69. package/src/slash/slash-commands.ts +34 -25
  70. package/src/slash/slash-live-state.ts +3 -0
  71. package/src/tui/fleet-status.ts +160 -45
  72. package/src/tui/fleet.ts +125 -15
  73. package/src/tui/render.ts +41 -10
  74. package/src/workflows/chat-progress.ts +6 -4
  75. package/src/workflows/scripted-workflow.ts +227 -70
@@ -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,
@@ -176,6 +181,8 @@ interface SubagentRunConfig {
176
181
  nestedSelf?: { parentRunId: string; parentStepIndex?: number; depth: number; path?: Array<{ runId: string; stepIndex?: number; agent?: string }> };
177
182
  timeoutMs?: number;
178
183
  deadlineAt?: number;
184
+ /** Resolved configured hard per-tool-call timeout (ms); fast tools still have a default when undefined. */
185
+ toolTimeoutMs?: number;
179
186
  turnBudget?: ResolvedTurnBudget;
180
187
  toolBudget?: ResolvedToolBudget;
181
188
  usageBudget?: UsageBudgetConfig;
@@ -541,6 +548,9 @@ function runPiStreaming(
541
548
  stopMessage?: string,
542
549
  registerTurnBudgetAbort?: (abort: ((message: string, state?: TurnBudgetState) => void) | undefined) => void,
543
550
  onWriterProcess?: (writer: { state: "none" | "spawning" } | { state: "running"; pid: number }) => void,
551
+ toolTimeoutMs?: number,
552
+ runDeadlineAt?: number,
553
+ orcaProgressTab?: OrcaProgressTab,
544
554
  ): Promise<RunPiStreamingResult> {
545
555
  return new Promise((resolve) => {
546
556
  const startedAt = Date.now();
@@ -597,6 +607,7 @@ function runPiStreaming(
597
607
  const writeOutputLine = (line: string) => {
598
608
  if (!line.trim()) return;
599
609
  outputStream.write(`${line}\n`);
610
+ orcaProgressTab?.append(`${line}\n`);
600
611
  };
601
612
 
602
613
  const writeOutputText = (text: string) => {
@@ -674,8 +685,14 @@ function runPiStreaming(
674
685
 
675
686
  onChildEvent?.(event);
676
687
 
688
+ if (event.type === "tool_execution_end") {
689
+ clearActiveToolTimeout(event);
690
+ return;
691
+ }
692
+
677
693
  if (event.type === "tool_execution_start" && event.toolName) {
678
694
  toolCount += 1;
695
+ armToolTimeout({ toolCallId: (event as { toolCallId?: unknown }).toolCallId, toolName: event.toolName });
679
696
  if (event.toolName === "structured_output") {
680
697
  structuredOutputToolInvoked = true;
681
698
  structuredOutputMessageStartIndex = messages.length;
@@ -771,6 +788,7 @@ function runPiStreaming(
771
788
  stderrTail.push(chunk);
772
789
  stderrReader.push(chunk);
773
790
  outputStream.write(chunk);
791
+ orcaProgressTab?.append(chunk.toString("utf-8"));
774
792
  });
775
793
  registerInterrupt?.(() => {
776
794
  if (settled || timedOut || stopped) return;
@@ -781,14 +799,60 @@ function runPiStreaming(
781
799
  if (!settled && !timedOut && !stopped) trySignalChild(child, "SIGTERM");
782
800
  }, 1000).unref?.();
783
801
  });
784
- registerTimeout?.(() => {
802
+ const terminateForTimeout = (message: string): void => {
785
803
  if (settled || timedOut || stopped) return;
786
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;
787
808
  interrupted = false;
788
- error = timeoutMessage ?? "Subagent timed out.";
809
+ error = message;
789
810
  if (processTreeController) void processTreeController.terminate();
790
811
  else trySignalChild(child, "SIGTERM");
791
- });
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."));
792
856
  registerStop?.(() => {
793
857
  if (settled || timedOut || stopped) return;
794
858
  stopped = true;
@@ -815,6 +879,7 @@ function runPiStreaming(
815
879
  turnBudgetHardKillTimer.unref?.();
816
880
  });
817
881
  const clearDrainTimers = () => {
882
+ clearAllToolTimeouts();
818
883
  if (finalDrainTimer) {
819
884
  clearTimeout(finalDrainTimer);
820
885
  finalDrainTimer = undefined;
@@ -1104,6 +1169,10 @@ interface SingleStepContext {
1104
1169
  stopSignal?: AbortSignal;
1105
1170
  timeoutMessage?: string;
1106
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;
1107
1176
  turnBudget?: ResolvedTurnBudget;
1108
1177
  childIntercomTarget?: string;
1109
1178
  orchestratorIntercomTarget?: string;
@@ -1115,10 +1184,11 @@ interface SingleStepContext {
1115
1184
  onWriterProcess?: (writer: { state: "none" | "spawning" } | { state: "running"; pid: number }) => void;
1116
1185
  onExternalProcess?: (process: ExternalProcessStatus) => void;
1117
1186
  skipAcceptance?: () => boolean;
1187
+ orcaProgressTab?: OrcaProgressTab;
1118
1188
  }
1119
1189
 
1120
1190
  /** Run a single pi agent step, returning output and metadata */
1121
- async function runSingleStep(
1191
+ async function runSingleStepInner(
1122
1192
  step: SubagentStep,
1123
1193
  ctx: SingleStepContext,
1124
1194
  ): Promise<StepResult & { completionGuardTriggered?: boolean }> {
@@ -1247,6 +1317,8 @@ async function runSingleStep(
1247
1317
  timeoutMessage: ctx.timeoutMessage,
1248
1318
  stopMessage: ctx.stopMessage,
1249
1319
  onProcess: ctx.onExternalProcess,
1320
+ onStdout: (chunk) => ctx.orcaProgressTab?.append(chunk.toString("utf-8")),
1321
+ onStderr: (chunk) => ctx.orcaProgressTab?.append(chunk.toString("utf-8")),
1250
1322
  }));
1251
1323
  try { fs.writeFileSync(ctx.outputFile, external.output, "utf-8"); } catch { /* Observability output is best-effort. */ }
1252
1324
  const resolvedOutput = step.outputPath && external.exitCode === 0
@@ -1433,6 +1505,9 @@ async function runSingleStep(
1433
1505
  ctx.stopMessage,
1434
1506
  ctx.registerTurnBudgetAbort,
1435
1507
  ctx.onWriterProcess,
1508
+ ctx.toolTimeoutMs,
1509
+ ctx.deadlineAt,
1510
+ ctx.orcaProgressTab,
1436
1511
  );
1437
1512
  if (run.processCloseObservedAt !== undefined) {
1438
1513
  writerProcesses.push({
@@ -1540,15 +1615,21 @@ async function runSingleStep(
1540
1615
  stopped: run.stopped,
1541
1616
  turnBudgetExceeded: run.turnBudgetExceeded,
1542
1617
  })) ? formatProcessSignalError(run.processSignal!) : undefined;
1543
- const error = toolAvailabilityError
1544
- ?? completionGuardError
1545
- ?? structuredError
1546
- ?? emptyOutputError
1547
- ?? (hiddenError?.hasError
1548
- ? hiddenError.details
1549
- ? `${hiddenError.errorType} failed (exit ${effectiveExitCode}): ${hiddenError.details}`
1550
- : `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
1551
- : run.error || signalError || (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined));
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
+ );
1552
1633
  const attempt: ModelAttempt = omitUndefinedProperties({
1553
1634
  model: candidate ?? run.model ?? step.model ?? "default",
1554
1635
  success: effectiveExitCode === 0 && !error,
@@ -1699,7 +1780,7 @@ async function runSingleStep(
1699
1780
  const effectiveFinalError = stoppedAfterAcceptance
1700
1781
  ? ctx.stopMessage ?? "Subagent stopped by user."
1701
1782
  : timedOutAfterAcceptance
1702
- ? ctx.timeoutMessage ?? "Subagent timed out."
1783
+ ? finalResult?.error ?? ctx.timeoutMessage ?? "Subagent timed out."
1703
1784
  : turnBudgetExceeded
1704
1785
  ? finalResult?.error ?? (turnBudget ? turnBudgetExceededMessage(turnBudget, turnBudget.turnCount) : "Subagent exceeded turn budget.")
1705
1786
  : acceptanceCanFailRun
@@ -1784,6 +1865,27 @@ async function runSingleStep(
1784
1865
  return isAgentContractV1(step.agentContract) ? attachContractProjections(result as unknown as import("../../shared/types.ts").SingleResult) as unknown as typeof result : result;
1785
1866
  }
1786
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
+
1787
1889
  type RunnerStatusStep = NonNullable<AsyncStatus["steps"]>[number] & {
1788
1890
  exitCode?: number | null;
1789
1891
  description?: string;
@@ -2008,6 +2110,7 @@ async function runSingleStepWithTimeout(
2008
2110
  return await runSingleStep(step, {
2009
2111
  ...ctx,
2010
2112
  registerTimeout,
2113
+ deadlineAt: Date.now() + timeoutMs,
2011
2114
  timeoutSignal: combinedAbortSignal([ctx.timeoutSignal, timeoutController.signal]),
2012
2115
  timeoutMessage,
2013
2116
  });
@@ -2202,7 +2305,7 @@ async function runSubagent(
2202
2305
 
2203
2306
  fs.mkdirSync(asyncDir, { recursive: true });
2204
2307
  writeAtomicJson(statusPath, statusPayload);
2205
- updateActiveRunIndex(asyncDir, statusPayload.state);
2308
+ updateActiveRunIndex(asyncDir, statusPayload.state, statusPayload.toolCallId);
2206
2309
  let pendingParallelUsageCost: CostSummary = { inputTokens: 0, outputTokens: 0, costUsd: 0 };
2207
2310
  const currentUsageTotals = (): CostSummary => {
2208
2311
  const cost = results.reduce<CostSummary>((sum, result) => ({
@@ -2275,13 +2378,70 @@ async function runSubagent(
2275
2378
  for (const node of graph.nodes) updateNode(node);
2276
2379
  statusPayload.workflowGraph = graph;
2277
2380
  };
2381
+ let finalResultCommitted = false;
2278
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
+ };
2279
2438
  const writeStatusPayloadNow = (): void => {
2280
2439
  refreshWorkflowGraph();
2440
+ writeRecoverableStatusResult();
2281
2441
  writeAtomicJson(statusPath, statusPayload);
2282
2442
  const activeState = isActiveAsyncState(statusPayload.state);
2283
- if (activeState && activeState !== lastIndexedActiveState) {
2284
- updateActiveRunIndex(asyncDir, statusPayload.state);
2443
+ if (activeState !== lastIndexedActiveState) {
2444
+ updateActiveRunIndex(asyncDir, statusPayload.state, statusPayload.toolCallId);
2285
2445
  }
2286
2446
  lastIndexedActiveState = activeState;
2287
2447
  emitNestedSelfEvent(statusPayload.state === "running" || statusPayload.state === "queued" ? "subagent.nested.updated" : "subagent.nested.completed");
@@ -2526,6 +2686,72 @@ async function runSubagent(
2526
2686
  const activeLongRunningSteps = new Set<number>();
2527
2687
  const mutatingFailureStates = initialStatusSteps.map(() => createMutatingFailureState());
2528
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];
2529
2755
  const supervisorAttentionSteps = new Map<number, ActivityState | undefined>();
2530
2756
  const mutatingFailureWindowMs = 5 * 60_000;
2531
2757
  const appendControlEvent = (event: ReturnType<typeof buildControlEvent>) => {
@@ -2566,6 +2792,34 @@ async function runSubagent(
2566
2792
  currentActivityState = nextRunState;
2567
2793
  setOptionalProperty(statusPayload, "activityState", nextRunState);
2568
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
+ };
2569
2823
  const maybeEmitActiveLongRunning = (flatIndex: number, now: number): boolean => {
2570
2824
  if (!controlConfig.enabled || activeLongRunningSteps.has(flatIndex)) return false;
2571
2825
  const step = statusPayload.steps[flatIndex];
@@ -2826,20 +3080,19 @@ async function runSubagent(
2826
3080
  if (event.type === "tool_execution_start" && event.toolName) {
2827
3081
  const mutates = isMutatingTool(event.toolName, event.args);
2828
3082
  const currentPath = resolveCurrentPath(event.toolName, event.args);
3083
+ const argsPreview = extractToolArgsPreview(event.args ?? {});
3084
+ const blocksSupervisor = isBlockingSupervisorTool(event.toolName, event.args);
2829
3085
  step.toolCount = (step.toolCount ?? 0) + 1;
2830
3086
  const configuredToolBudget = flatSteps[flatIndex]?.toolBudget;
2831
3087
  if (configuredToolBudget) {
2832
3088
  step.toolBudget = toolBudgetState(configuredToolBudget, step.toolCount);
2833
3089
  statusPayload.toolBudget = step.toolBudget;
2834
3090
  }
2835
- step.currentTool = event.toolName;
2836
- step.currentToolArgs = extractToolArgsPreview(event.args ?? {});
2837
- step.currentToolStartedAt = now;
2838
- setOptionalProperty(step, "currentPath", currentPath);
3091
+ recordActiveToolCall(flatIndex, { toolCallId: (event as { toolCallId?: unknown }).toolCallId, toolName: event.toolName }, { argsPreview, currentPath, blocksSupervisor, now });
2839
3092
  pendingToolResults[flatIndex] = omitUndefinedProperties({ tool: event.toolName, path: currentPath, mutates, startedAt: now });
2840
3093
  statusPayload.toolCount = (statusPayload.toolCount ?? 0) + 1;
2841
3094
  syncTopLevelCurrentTool();
2842
- if (controlConfig.enabled && isBlockingSupervisorTool(event.toolName, event.args) && step.activityState !== "needs_attention") {
3095
+ if (controlConfig.enabled && blocksSupervisor && step.activityState !== "needs_attention") {
2843
3096
  const previous = step.activityState;
2844
3097
  step.activityState = "needs_attention";
2845
3098
  supervisorAttentionSteps.set(flatIndex, previous);
@@ -2864,16 +3117,15 @@ async function runSubagent(
2864
3117
  })));
2865
3118
  }
2866
3119
  } else if (event.type === "tool_execution_end") {
2867
- if (step.currentTool) {
3120
+ const endedTool = removeActiveToolCall(flatIndex, event);
3121
+ if (endedTool) {
2868
3122
  step.recentTools ??= [];
2869
- step.recentTools.push({ tool: step.currentTool, args: step.currentToolArgs || "", endMs: now });
3123
+ step.recentTools.push({ tool: endedTool.tool, args: endedTool.args, endMs: now });
2870
3124
  }
3125
+ refreshStepCurrentTool(flatIndex);
2871
3126
  const supervisorPreviousActivity = supervisorAttentionSteps.get(flatIndex);
2872
- const clearedSupervisorAttention = supervisorAttentionSteps.delete(flatIndex);
2873
- delete step.currentTool;
2874
- delete step.currentToolArgs;
2875
- delete step.currentToolStartedAt;
2876
- delete step.currentPath;
3127
+ const stillBlockingSupervisor = [...(activeToolCalls[flatIndex]?.values() ?? [])].some((active) => active.blocksSupervisor);
3128
+ const clearedSupervisorAttention = endedTool?.blocksSupervisor && !stillBlockingSupervisor ? supervisorAttentionSteps.delete(flatIndex) : false;
2877
3129
  if (clearedSupervisorAttention && step.activityState === "needs_attention") {
2878
3130
  setOptionalProperty(step, "activityState", supervisorPreviousActivity);
2879
3131
  syncAggregateActivityState();
@@ -2993,6 +3245,8 @@ async function runSubagent(
2993
3245
  })));
2994
3246
  changed = true;
2995
3247
  }
3248
+ } else if (maybeEmitOpenToolAttention(index, now)) {
3249
+ changed = true;
2996
3250
  } else if (maybeEmitActiveLongRunning(index, now)) {
2997
3251
  changed = true;
2998
3252
  }
@@ -3555,6 +3809,7 @@ async function runSubagent(
3555
3809
  timeoutMessage,
3556
3810
  stopMessage,
3557
3811
  turnBudget: config.turnBudget,
3812
+ toolTimeoutMs: task.toolTimeoutMs ?? config.toolTimeoutMs,
3558
3813
  onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking),
3559
3814
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
3560
3815
  onWriterProcess,
@@ -3938,6 +4193,7 @@ async function runSubagent(
3938
4193
  timeoutMessage,
3939
4194
  stopMessage,
3940
4195
  turnBudget: config.turnBudget,
4196
+ toolTimeoutMs: taskForRun.toolTimeoutMs ?? config.toolTimeoutMs,
3941
4197
  onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking),
3942
4198
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
3943
4199
  onWriterProcess,
@@ -4228,6 +4484,7 @@ async function runSubagent(
4228
4484
  timeoutMessage,
4229
4485
  stopMessage,
4230
4486
  turnBudget: config.turnBudget,
4487
+ toolTimeoutMs: seqStep.toolTimeoutMs ?? config.toolTimeoutMs,
4231
4488
  onAttemptStart: (attempt) => updateStepModel(flatIndex, attempt.model, attempt.thinking),
4232
4489
  onChildEvent: (event) => updateStepFromChildEvent(flatIndex, event),
4233
4490
  onWriterProcess,
@@ -4525,9 +4782,8 @@ async function runSubagent(
4525
4782
  statusPayload.error = `Step failed: ${failedStep.agent}`;
4526
4783
  }
4527
4784
  }
4528
- writeStatusPayload();
4529
4785
  try {
4530
- writeAtomicJson(resultPath, {
4786
+ writeAsyncResultFile(resultPath, {
4531
4787
  lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
4532
4788
  id,
4533
4789
  agent: agentName,
@@ -4622,9 +4878,15 @@ async function runSubagent(
4622
4878
  ...(taskIndex !== undefined && { taskIndex }),
4623
4879
  ...(totalTasks !== undefined && { totalTasks }),
4624
4880
  });
4881
+ finalResultCommitted = true;
4625
4882
  } catch (err) {
4626
- 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();
4627
4888
  }
4889
+ writeStatusPayload();
4628
4890
  appendJsonl(
4629
4891
  eventsPath,
4630
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") {