@selesai/code 0.8.6 → 0.8.7

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 (192) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/core/agent-session.js +13 -1
  3. package/dist/core/extensions/types.d.ts +2 -0
  4. package/dist/core/provider-composer.js +17 -4
  5. package/dist/core/settings-manager.d.ts +2 -0
  6. package/dist/core/settings-manager.js +3 -0
  7. package/dist/core/system-prompt.js +2 -8
  8. package/dist/core/system-prompt.test.js +6 -0
  9. package/dist/core/tools/index.d.ts +1 -0
  10. package/dist/core/tools/index.js +1 -0
  11. package/dist/core/tools/schema-prune.d.ts +16 -0
  12. package/dist/core/tools/schema-prune.js +39 -0
  13. package/dist/core/tools/schema-prune.test.d.ts +1 -0
  14. package/dist/core/tools/schema-prune.test.js +111 -0
  15. package/dist/defaults/settings.json +1 -0
  16. package/dist/extensions/model-prompt-injector/config.json +10 -0
  17. package/dist/extensions/model-prompt-injector/index.test.ts +112 -0
  18. package/dist/extensions/model-prompt-injector/index.ts +151 -0
  19. package/dist/extensions/package.json +1 -0
  20. package/dist/extensions/pi-subagents/CHANGELOG.md +158 -90
  21. package/dist/extensions/pi-subagents/UPSTREAM-V0.50-MAPPING.md +172 -0
  22. package/dist/extensions/pi-subagents/agents/oracle.md +7 -5
  23. package/dist/extensions/pi-subagents/agents/reviewer.md +2 -2
  24. package/dist/extensions/pi-subagents/agents/scout.md +1 -1
  25. package/dist/extensions/pi-subagents/agents/worker.md +1 -1
  26. package/dist/extensions/pi-subagents/docs/configuration.md +78 -19
  27. package/dist/extensions/pi-subagents/docs/extension-api.md +36 -0
  28. package/dist/extensions/pi-subagents/docs/missions.md +5 -3
  29. package/dist/extensions/pi-subagents/docs/models.md +1 -1
  30. package/dist/extensions/pi-subagents/docs/observability.md +42 -2
  31. package/dist/extensions/pi-subagents/docs/tool-reference.md +20 -3
  32. package/dist/extensions/pi-subagents/docs/workflows.md +5 -5
  33. package/dist/extensions/pi-subagents/install.mjs +2 -2
  34. package/dist/extensions/pi-subagents/package-lock.json +1470 -1472
  35. package/dist/extensions/pi-subagents/package.json +1 -1
  36. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +1 -1
  37. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
  38. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +15 -14
  39. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  40. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +46 -26
  41. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +2 -0
  42. package/dist/extensions/pi-subagents/src/agents/agents.ts +37 -12
  43. package/dist/extensions/pi-subagents/src/api/external-runs.ts +174 -84
  44. package/dist/extensions/pi-subagents/src/api/preflight.ts +13 -7
  45. package/dist/extensions/pi-subagents/src/extension/config.ts +53 -0
  46. package/dist/extensions/pi-subagents/src/extension/index.ts +67 -13
  47. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +34 -6
  48. package/dist/extensions/pi-subagents/src/extension/rpc.ts +5 -1
  49. package/dist/extensions/pi-subagents/src/extension/schemas.ts +31 -30
  50. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +10 -9
  51. package/dist/extensions/pi-subagents/src/inspectors/herdr/actions.ts +13 -8
  52. package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +16 -3
  53. package/dist/extensions/pi-subagents/src/inspectors/herdr/project-panes.ts +2 -6
  54. package/dist/extensions/pi-subagents/src/inspectors/herdr/shell-command.ts +16 -0
  55. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +5 -4
  56. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +19 -42
  57. package/dist/extensions/pi-subagents/src/missions/goal-driver.ts +3 -1
  58. package/dist/extensions/pi-subagents/src/missions/store.ts +8 -3
  59. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +82 -25
  60. package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +71 -1
  61. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +73 -43
  62. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +5 -0
  63. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +14 -6
  64. package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +277 -0
  65. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +8 -3
  66. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +2 -2
  67. package/dist/extensions/pi-subagents/src/runs/background/completion-replay.ts +11 -1
  68. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +21 -6
  69. package/dist/extensions/pi-subagents/src/runs/background/result-files.ts +437 -0
  70. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +205 -41
  71. package/dist/extensions/pi-subagents/src/runs/background/resume-guidance.ts +27 -7
  72. package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +76 -19
  73. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +30 -24
  74. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +101 -4
  75. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +54 -28
  76. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +27 -13
  77. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +298 -33
  78. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +2 -0
  79. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +5 -2
  80. package/dist/extensions/pi-subagents/src/runs/foreground/async-dismiss-action.ts +2 -1
  81. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +16 -0
  82. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +219 -15
  83. package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +4 -3
  84. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +333 -45
  85. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +107 -1
  86. package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +4 -0
  87. package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +39 -23
  88. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +16 -2
  89. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +66 -62
  90. package/dist/extensions/pi-subagents/src/runs/shared/orca-progress-tabs.ts +376 -0
  91. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
  92. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +15 -0
  93. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +1 -9
  94. package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +12 -0
  95. package/dist/extensions/pi-subagents/src/runs/shared/tool-timeout.ts +95 -0
  96. package/dist/extensions/pi-subagents/src/shared/agent-stream-options.ts +5 -0
  97. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +0 -4
  98. package/dist/extensions/pi-subagents/src/shared/display-text.ts +50 -0
  99. package/dist/extensions/pi-subagents/src/shared/node-executable.ts +21 -0
  100. package/dist/extensions/pi-subagents/src/shared/types.ts +50 -4
  101. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +34 -25
  102. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +4 -0
  103. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +160 -45
  104. package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +1 -48
  105. package/dist/extensions/pi-subagents/src/tui/fleet.ts +128 -16
  106. package/dist/extensions/pi-subagents/src/tui/render.ts +122 -44
  107. package/dist/extensions/pi-subagents/src/watchdog/permission-arbiter.ts +2 -1
  108. package/dist/extensions/pi-subagents/src/watchdog/review.ts +4 -3
  109. package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +10 -2
  110. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +272 -76
  111. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/types.d.ts +2 -0
  112. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2 -2
  113. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +68 -9
  114. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +27 -1
  115. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1 -1
  116. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +49 -0
  117. package/dist/extensions/pi-subagents/test/integration/external-cli-runner.test.ts +1 -0
  118. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +26 -1
  119. package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +144 -0
  120. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +26 -0
  121. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +369 -98
  122. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +111 -78
  123. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +49 -1
  124. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +12 -0
  125. package/dist/extensions/pi-subagents/test/support/node-command.ts +15 -0
  126. package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +50 -0
  127. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +28 -0
  128. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +61 -0
  129. package/dist/extensions/pi-subagents/test/unit/agent-stream-options.test.ts +14 -0
  130. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +76 -0
  131. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +2 -0
  132. package/dist/extensions/pi-subagents/test/unit/async-status-snapshot.test.ts +162 -0
  133. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +209 -10
  134. package/dist/extensions/pi-subagents/test/unit/completion-replay.test.ts +41 -1
  135. package/dist/extensions/pi-subagents/test/unit/external-runs.test.ts +135 -42
  136. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +252 -1
  137. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +192 -6
  138. package/dist/extensions/pi-subagents/test/unit/herdr-inspector-bootstrap.test.ts +32 -0
  139. package/dist/extensions/pi-subagents/test/unit/herdr-shell-command.test.ts +59 -0
  140. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +69 -0
  141. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +23 -4
  142. package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +56 -1
  143. package/dist/extensions/pi-subagents/test/unit/mission-goal-driver.test.ts +67 -3
  144. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +1 -1
  145. package/dist/extensions/pi-subagents/test/unit/mission-store.test.ts +14 -0
  146. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +18 -0
  147. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +2 -2
  148. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +25 -2
  149. package/dist/extensions/pi-subagents/test/unit/node-executable.test.ts +26 -0
  150. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +8 -0
  151. package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +348 -0
  152. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +5 -2
  153. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +1 -1
  154. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +34 -0
  155. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +6 -1
  156. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +92 -0
  157. package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +175 -0
  158. package/dist/extensions/pi-subagents/test/unit/retained-children.test.ts +127 -17
  159. package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +44 -0
  160. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +247 -0
  161. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +107 -2
  162. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +5 -8
  163. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +456 -4
  164. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +128 -0
  165. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +28 -0
  166. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +16 -7
  167. package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +15 -0
  168. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +79 -15
  169. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +12 -12
  170. package/dist/extensions/pi-subagents/test/unit/tool-timeout.test.ts +109 -0
  171. package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +34 -0
  172. package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +94 -1
  173. package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +42 -2
  174. package/dist/extensions/question/index.ts +21 -6
  175. package/dist/extensions/question/question-list.ts +7 -1
  176. package/dist/extensions/question/row-layout.ts +3 -0
  177. package/dist/extensions/question/tests/exp.test.ts +70 -0
  178. package/dist/extensions/question/tests/exp2.test.ts +59 -0
  179. package/dist/extensions/question/tests/exp3.test.ts +47 -0
  180. package/dist/extensions/question/tests/helpers.test.ts +7 -0
  181. package/dist/extensions/question/tests/question-list.test.ts +140 -0
  182. package/dist/extensions/question/tests/row-layout.test.ts +13 -0
  183. package/dist/extensions/question/tests/selection-mode.test.ts +54 -0
  184. package/dist/extensions/question/tests/zz-ig3.test.ts +20 -0
  185. package/dist/extensions/question/tests/zz-probe.test.ts +29 -0
  186. package/dist/skills/pi-subagents/SKILL.md +1 -1
  187. package/dist/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
  188. package/dist/skills/pi-subagents/references/execution-controls.md +15 -14
  189. package/dist/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  190. package/dist/skills/pi-subagents/references/prompting-and-roles.md +46 -26
  191. package/docs/settings.md +6 -0
  192. package/package.json +3 -3
@@ -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, formatModelRetryNote, isRetryableModelFailure, MODEL_RETRY_DELAYS_MS } 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
@@ -1432,6 +1506,9 @@ async function runSingleStep(
1432
1506
  ctx.stopMessage,
1433
1507
  ctx.registerTurnBudgetAbort,
1434
1508
  ctx.onWriterProcess,
1509
+ ctx.toolTimeoutMs,
1510
+ ctx.deadlineAt,
1511
+ ctx.orcaProgressTab,
1435
1512
  );
1436
1513
  if (run.processCloseObservedAt !== undefined) {
1437
1514
  writerProcesses.push({
@@ -1539,15 +1616,21 @@ async function runSingleStep(
1539
1616
  stopped: run.stopped,
1540
1617
  turnBudgetExceeded: run.turnBudgetExceeded,
1541
1618
  })) ? formatProcessSignalError(run.processSignal!) : undefined;
1542
- const error = toolAvailabilityError
1543
- ?? completionGuardError
1544
- ?? structuredError
1545
- ?? emptyOutputError
1546
- ?? (hiddenError?.hasError
1547
- ? hiddenError.details
1548
- ? `${hiddenError.errorType} failed (exit ${effectiveExitCode}): ${hiddenError.details}`
1549
- : `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
1550
- : run.error || signalError || (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined));
1619
+ const error = formatSubagentExtensionConflictError(
1620
+ toolAvailabilityError
1621
+ ?? completionGuardError
1622
+ ?? structuredError
1623
+ ?? emptyOutputError
1624
+ ?? (hiddenError?.hasError
1625
+ ? hiddenError.details
1626
+ ? `${hiddenError.errorType} failed (exit ${effectiveExitCode}): ${hiddenError.details}`
1627
+ : `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
1628
+ : run.error || signalError || (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined)),
1629
+ {
1630
+ agent: step.agent,
1631
+ ambientExtensionsEnabled: launchResolvedExtensions?.disableAmbientExtensions === false,
1632
+ },
1633
+ );
1551
1634
  const attempt: ModelAttempt = omitUndefinedProperties({
1552
1635
  model: candidate ?? run.model ?? step.model ?? "default",
1553
1636
  success: effectiveExitCode === 0 && !error,
@@ -1716,15 +1799,16 @@ async function runSingleStep(
1716
1799
  const acceptanceFailure = effectiveAcceptance ? acceptanceFailureMessage(effectiveAcceptance) : undefined;
1717
1800
  const acceptanceCanFailRun = acceptanceFailure && effectiveAcceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted && !timedOutAfterAcceptance && !stoppedAfterAcceptance && !turnBudgetExceeded && !isAgentContractV1(step.agentContract);
1718
1801
  const effectiveFinalExitCode = timedOutAfterAcceptance || stoppedAfterAcceptance || turnBudgetExceeded ? 1 : acceptanceCanFailRun ? 1 : finalResult?.exitCode ?? 1;
1802
+ const intercomDetachReceipt = finalResult?.finalOutput === INTERCOM_DETACH_RECEIPT;
1719
1803
  const effectiveFinalError = stoppedAfterAcceptance
1720
1804
  ? ctx.stopMessage ?? "Subagent stopped by user."
1721
1805
  : timedOutAfterAcceptance
1722
- ? ctx.timeoutMessage ?? "Subagent timed out."
1806
+ ? finalResult?.error ?? ctx.timeoutMessage ?? "Subagent timed out."
1723
1807
  : turnBudgetExceeded
1724
1808
  ? finalResult?.error ?? (turnBudget ? turnBudgetExceededMessage(turnBudget, turnBudget.turnCount) : "Subagent exceeded turn budget.")
1725
1809
  : acceptanceCanFailRun
1726
1810
  ? (finalResult?.error ? `${finalResult.error}\n${acceptanceFailure}` : acceptanceFailure)
1727
- : finalResult?.error;
1811
+ : finalResult?.error ?? (intercomDetachReceipt ? INTERCOM_DETACH_RECEIPT : undefined);
1728
1812
 
1729
1813
  const artifactErrors = artifactPaths && ctx.artifactConfig?.enabled !== false
1730
1814
  ? persistStepArtifacts({
@@ -1804,6 +1888,27 @@ async function runSingleStep(
1804
1888
  return isAgentContractV1(step.agentContract) ? attachContractProjections(result as unknown as import("../../shared/types.ts").SingleResult) as unknown as typeof result : result;
1805
1889
  }
1806
1890
 
1891
+ async function runSingleStep(
1892
+ step: SubagentStep,
1893
+ ctx: SingleStepContext,
1894
+ ): Promise<StepResult & { completionGuardTriggered?: boolean }> {
1895
+ if (step.importAsyncRoot) return runSingleStepInner(step, ctx);
1896
+ const orcaProgressTab = createOrcaProgressTab({
1897
+ cwd: step.cwd ?? ctx.cwd,
1898
+ runId: ctx.id,
1899
+ agent: step.agent,
1900
+ index: ctx.flatIndex,
1901
+ });
1902
+ try {
1903
+ const result = await runSingleStepInner(step, { ...ctx, orcaProgressTab });
1904
+ orcaProgressTab?.finish(result.stopped ? "stopped" : result.exitCode === 0 && !result.error ? "completed" : "failed", result.sessionFile);
1905
+ return result;
1906
+ } catch (error) {
1907
+ orcaProgressTab?.finish("failed");
1908
+ throw error;
1909
+ }
1910
+ }
1911
+
1807
1912
  type RunnerStatusStep = NonNullable<AsyncStatus["steps"]>[number] & {
1808
1913
  exitCode?: number | null;
1809
1914
  description?: string;
@@ -2028,6 +2133,7 @@ async function runSingleStepWithTimeout(
2028
2133
  return await runSingleStep(step, {
2029
2134
  ...ctx,
2030
2135
  registerTimeout,
2136
+ deadlineAt: Date.now() + timeoutMs,
2031
2137
  timeoutSignal: combinedAbortSignal([ctx.timeoutSignal, timeoutController.signal]),
2032
2138
  timeoutMessage,
2033
2139
  });
@@ -2222,7 +2328,7 @@ async function runSubagent(
2222
2328
 
2223
2329
  fs.mkdirSync(asyncDir, { recursive: true });
2224
2330
  writeAtomicJson(statusPath, statusPayload);
2225
- updateActiveRunIndex(asyncDir, statusPayload.state);
2331
+ updateActiveRunIndex(asyncDir, statusPayload.state, statusPayload.toolCallId);
2226
2332
  let pendingParallelUsageCost: CostSummary = { inputTokens: 0, outputTokens: 0, costUsd: 0 };
2227
2333
  const currentUsageTotals = (): CostSummary => {
2228
2334
  const cost = results.reduce<CostSummary>((sum, result) => ({
@@ -2295,13 +2401,70 @@ async function runSubagent(
2295
2401
  for (const node of graph.nodes) updateNode(node);
2296
2402
  statusPayload.workflowGraph = graph;
2297
2403
  };
2404
+ let finalResultCommitted = false;
2298
2405
  let lastIndexedActiveState = isActiveAsyncState(statusPayload.state);
2406
+ const statusResultState = (): AsyncStatus["state"] | undefined => {
2407
+ if (statusPayload.state === "running" || statusPayload.state === "queued") return undefined;
2408
+ if (statusPayload.state === "paused" && statusPayload.checkpoint?.status === "pending") return undefined;
2409
+ return statusPayload.state;
2410
+ };
2411
+ const statusResultSummary = (state: AsyncStatus["state"]): string => {
2412
+ if (statusPayload.error) return statusPayload.error;
2413
+ if (state === "paused") return "Paused after interrupt. Waiting for explicit next action.";
2414
+ if (state === "stopped") return stopMessage;
2415
+ if (state === "rejected") return statusPayload.checkpoint?.name ? `Checkpoint '${statusPayload.checkpoint.name}' rejected.` : "Subagent rejected.";
2416
+ return state === "complete" ? "Subagent completed." : "Subagent failed.";
2417
+ };
2418
+ const statusResultSuccess = (state: AsyncStatus["state"], step: RunnerStatusStep): boolean | undefined => {
2419
+ if (step.status === "complete" || step.status === "completed") return true;
2420
+ if (step.status === "failed" || step.status === "stopped" || step.status === "rejected") return false;
2421
+ if (state === "complete") return true;
2422
+ if (state === "failed" || state === "stopped" || state === "rejected") return false;
2423
+ return undefined;
2424
+ };
2425
+ const writeRecoverableStatusResult = (): void => {
2426
+ const state = statusResultState();
2427
+ if (!state || finalResultCommitted || !config.sessionId) return;
2428
+ const now = statusPayload.endedAt ?? statusPayload.lastUpdate ?? Date.now();
2429
+ const summary = statusResultSummary(state);
2430
+ writePendingAsyncResultFile(resultPath, omitUndefinedProperties({
2431
+ lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
2432
+ id,
2433
+ runId: id,
2434
+ 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("->")}`,
2435
+ mode: statusPayload.mode,
2436
+ success: state === "complete",
2437
+ state,
2438
+ summary,
2439
+ error: state === "failed" || state === "stopped" || state === "rejected" ? summary : undefined,
2440
+ stopped: state === "stopped" ? true : undefined,
2441
+ checkpoint: statusPayload.checkpoint,
2442
+ results: statusPayload.steps.map((step) => omitUndefinedProperties({
2443
+ agent: step.agent,
2444
+ output: step.status === "complete" || step.status === "completed" ? "" : step.error ?? summary,
2445
+ error: step.error,
2446
+ success: statusResultSuccess(state, step),
2447
+ sessionFile: step.sessionFile,
2448
+ model: step.model,
2449
+ attemptedModels: step.attemptedModels,
2450
+ modelAttempts: step.modelAttempts,
2451
+ })),
2452
+ exitCode: state === "complete" || state === "paused" ? 0 : 1,
2453
+ timestamp: now,
2454
+ durationMs: Math.max(0, now - overallStartTime),
2455
+ asyncDir,
2456
+ cwd,
2457
+ sessionId: config.sessionId,
2458
+ sessionFile: statusPayload.sessionFile ?? latestSessionFile,
2459
+ }));
2460
+ };
2299
2461
  const writeStatusPayloadNow = (): void => {
2300
2462
  refreshWorkflowGraph();
2463
+ writeRecoverableStatusResult();
2301
2464
  writeAtomicJson(statusPath, statusPayload);
2302
2465
  const activeState = isActiveAsyncState(statusPayload.state);
2303
- if (activeState && activeState !== lastIndexedActiveState) {
2304
- updateActiveRunIndex(asyncDir, statusPayload.state);
2466
+ if (activeState !== lastIndexedActiveState) {
2467
+ updateActiveRunIndex(asyncDir, statusPayload.state, statusPayload.toolCallId);
2305
2468
  }
2306
2469
  lastIndexedActiveState = activeState;
2307
2470
  emitNestedSelfEvent(statusPayload.state === "running" || statusPayload.state === "queued" ? "subagent.nested.updated" : "subagent.nested.completed");
@@ -2546,6 +2709,72 @@ async function runSubagent(
2546
2709
  const activeLongRunningSteps = new Set<number>();
2547
2710
  const mutatingFailureStates = initialStatusSteps.map(() => createMutatingFailureState());
2548
2711
  const pendingToolResults: Array<{ tool: string; path?: string; mutates: boolean; startedAt?: number } | undefined> = initialStatusSteps.map(() => undefined);
2712
+ type ActiveToolCall = { key: string; tool: string; args: string; startedAt: number; path?: string; blocksSupervisor: boolean };
2713
+ const activeToolCalls = initialStatusSteps.map(() => new Map<string, ActiveToolCall>());
2714
+ const activeToolKeysByName = initialStatusSteps.map(() => new Map<string, string[]>());
2715
+ const activeToolSequences = initialStatusSteps.map(() => 0);
2716
+ const latestActiveToolCall = (flatIndex: number): ActiveToolCall | undefined => [...(activeToolCalls[flatIndex]?.values() ?? [])].sort((left, right) => right.startedAt - left.startedAt)[0];
2717
+ const refreshStepCurrentTool = (flatIndex: number): void => {
2718
+ const step = statusPayload.steps[flatIndex];
2719
+ if (!step) return;
2720
+ const active = latestActiveToolCall(flatIndex);
2721
+ if (!active) {
2722
+ delete step.currentTool;
2723
+ delete step.currentToolArgs;
2724
+ delete step.currentToolStartedAt;
2725
+ delete step.currentPath;
2726
+ return;
2727
+ }
2728
+ step.currentTool = active.tool;
2729
+ step.currentToolArgs = active.args;
2730
+ step.currentToolStartedAt = active.startedAt;
2731
+ setOptionalProperty(step, "currentPath", active.path);
2732
+ };
2733
+ const recordActiveToolCall = (flatIndex: number, event: { toolCallId?: unknown; toolName: string }, input: { argsPreview: string; currentPath?: string; blocksSupervisor: boolean; now: number }): ActiveToolCall => {
2734
+ const sequence = (activeToolSequences[flatIndex] ?? 0) + 1;
2735
+ activeToolSequences[flatIndex] = sequence;
2736
+ const key = toolTimeoutCallKey(event, sequence);
2737
+ const active: ActiveToolCall = {
2738
+ key,
2739
+ tool: event.toolName,
2740
+ args: input.argsPreview,
2741
+ startedAt: input.now,
2742
+ blocksSupervisor: input.blocksSupervisor,
2743
+ ...(input.currentPath !== undefined ? { path: input.currentPath } : {}),
2744
+ };
2745
+ activeToolCalls[flatIndex]?.set(key, active);
2746
+ const keysByName = activeToolKeysByName[flatIndex];
2747
+ const keys = keysByName?.get(active.tool) ?? [];
2748
+ keys.push(key);
2749
+ keysByName?.set(active.tool, keys);
2750
+ refreshStepCurrentTool(flatIndex);
2751
+ return active;
2752
+ };
2753
+ const removeActiveToolCallKey = (flatIndex: number, key: string): ActiveToolCall | undefined => {
2754
+ const calls = activeToolCalls[flatIndex];
2755
+ const active = calls?.get(key);
2756
+ if (!active) return undefined;
2757
+ calls?.delete(key);
2758
+ const keysByName = activeToolKeysByName[flatIndex];
2759
+ const keys = keysByName?.get(active.tool)?.filter((candidate) => candidate !== key) ?? [];
2760
+ if (keys.length > 0) keysByName?.set(active.tool, keys);
2761
+ else keysByName?.delete(active.tool);
2762
+ return active;
2763
+ };
2764
+ const removeActiveToolCall = (flatIndex: number, event: { toolCallId?: unknown; toolName?: unknown }): ActiveToolCall | undefined => {
2765
+ const calls = activeToolCalls[flatIndex];
2766
+ const key = typeof event.toolCallId === "string" && event.toolCallId.length > 0
2767
+ ? `id:${event.toolCallId}`
2768
+ : typeof event.toolName === "string"
2769
+ ? activeToolKeysByName[flatIndex]?.get(event.toolName)?.[0]
2770
+ : calls?.size === 1
2771
+ ? [...calls.keys()][0]
2772
+ : undefined;
2773
+ return key ? removeActiveToolCallKey(flatIndex, key) : undefined;
2774
+ };
2775
+ const openToolAttentionTarget = (flatIndex: number, now: number): ActiveToolCall | undefined => [...(activeToolCalls[flatIndex]?.values() ?? [])]
2776
+ .filter((active) => shouldEmitOpenToolAttention({ config: controlConfig, currentTool: active.tool, currentToolStartedAt: active.startedAt, now }))
2777
+ .sort((left, right) => left.startedAt - right.startedAt)[0];
2549
2778
  const supervisorAttentionSteps = new Map<number, ActivityState | undefined>();
2550
2779
  const mutatingFailureWindowMs = 5 * 60_000;
2551
2780
  const appendControlEvent = (event: ReturnType<typeof buildControlEvent>) => {
@@ -2586,6 +2815,34 @@ async function runSubagent(
2586
2815
  currentActivityState = nextRunState;
2587
2816
  setOptionalProperty(statusPayload, "activityState", nextRunState);
2588
2817
  };
2818
+ const maybeEmitOpenToolAttention = (flatIndex: number, now: number): boolean => {
2819
+ const step = statusPayload.steps[flatIndex];
2820
+ if (!step || step.status !== "running" || step.activityState === "needs_attention") return false;
2821
+ const target = openToolAttentionTarget(flatIndex, now);
2822
+ if (!target) return false;
2823
+ const previous = step.activityState;
2824
+ step.activityState = "needs_attention";
2825
+ statusPayload.activityState = "needs_attention";
2826
+ const toolDurationMs = Math.max(0, now - target.startedAt);
2827
+ appendControlEvent(buildControlEvent(omitUndefinedProperties({
2828
+ type: "needs_attention",
2829
+ from: previous,
2830
+ to: "needs_attention",
2831
+ runId: id,
2832
+ agent: step.agent,
2833
+ index: flatIndex,
2834
+ ts: now,
2835
+ message: `${step.agent} has had tool '${target.tool}' open for ${Math.floor(toolDurationMs / 1000)}s`,
2836
+ reason: "tool_open_threshold",
2837
+ turns: step.turnCount,
2838
+ tokens: step.tokens?.total,
2839
+ toolCount: step.toolCount,
2840
+ currentTool: target.tool,
2841
+ currentToolDurationMs: toolDurationMs,
2842
+ currentPath: target.path,
2843
+ })));
2844
+ return true;
2845
+ };
2589
2846
  const maybeEmitActiveLongRunning = (flatIndex: number, now: number): boolean => {
2590
2847
  if (!controlConfig.enabled || activeLongRunningSteps.has(flatIndex)) return false;
2591
2848
  const step = statusPayload.steps[flatIndex];
@@ -2846,20 +3103,19 @@ async function runSubagent(
2846
3103
  if (event.type === "tool_execution_start" && event.toolName) {
2847
3104
  const mutates = isMutatingTool(event.toolName, event.args);
2848
3105
  const currentPath = resolveCurrentPath(event.toolName, event.args);
3106
+ const argsPreview = extractToolArgsPreview(event.args ?? {});
3107
+ const blocksSupervisor = isBlockingSupervisorTool(event.toolName, event.args);
2849
3108
  step.toolCount = (step.toolCount ?? 0) + 1;
2850
3109
  const configuredToolBudget = flatSteps[flatIndex]?.toolBudget;
2851
3110
  if (configuredToolBudget) {
2852
3111
  step.toolBudget = toolBudgetState(configuredToolBudget, step.toolCount);
2853
3112
  statusPayload.toolBudget = step.toolBudget;
2854
3113
  }
2855
- step.currentTool = event.toolName;
2856
- step.currentToolArgs = extractToolArgsPreview(event.args ?? {});
2857
- step.currentToolStartedAt = now;
2858
- setOptionalProperty(step, "currentPath", currentPath);
3114
+ recordActiveToolCall(flatIndex, { toolCallId: (event as { toolCallId?: unknown }).toolCallId, toolName: event.toolName }, { argsPreview, currentPath, blocksSupervisor, now });
2859
3115
  pendingToolResults[flatIndex] = omitUndefinedProperties({ tool: event.toolName, path: currentPath, mutates, startedAt: now });
2860
3116
  statusPayload.toolCount = (statusPayload.toolCount ?? 0) + 1;
2861
3117
  syncTopLevelCurrentTool();
2862
- if (controlConfig.enabled && isBlockingSupervisorTool(event.toolName, event.args) && step.activityState !== "needs_attention") {
3118
+ if (controlConfig.enabled && blocksSupervisor && step.activityState !== "needs_attention") {
2863
3119
  const previous = step.activityState;
2864
3120
  step.activityState = "needs_attention";
2865
3121
  supervisorAttentionSteps.set(flatIndex, previous);
@@ -2884,16 +3140,15 @@ async function runSubagent(
2884
3140
  })));
2885
3141
  }
2886
3142
  } else if (event.type === "tool_execution_end") {
2887
- if (step.currentTool) {
3143
+ const endedTool = removeActiveToolCall(flatIndex, event);
3144
+ if (endedTool) {
2888
3145
  step.recentTools ??= [];
2889
- step.recentTools.push({ tool: step.currentTool, args: step.currentToolArgs || "", endMs: now });
3146
+ step.recentTools.push({ tool: endedTool.tool, args: endedTool.args, endMs: now });
2890
3147
  }
3148
+ refreshStepCurrentTool(flatIndex);
2891
3149
  const supervisorPreviousActivity = supervisorAttentionSteps.get(flatIndex);
2892
- const clearedSupervisorAttention = supervisorAttentionSteps.delete(flatIndex);
2893
- delete step.currentTool;
2894
- delete step.currentToolArgs;
2895
- delete step.currentToolStartedAt;
2896
- delete step.currentPath;
3150
+ const stillBlockingSupervisor = [...(activeToolCalls[flatIndex]?.values() ?? [])].some((active) => active.blocksSupervisor);
3151
+ const clearedSupervisorAttention = endedTool?.blocksSupervisor && !stillBlockingSupervisor ? supervisorAttentionSteps.delete(flatIndex) : false;
2897
3152
  if (clearedSupervisorAttention && step.activityState === "needs_attention") {
2898
3153
  setOptionalProperty(step, "activityState", supervisorPreviousActivity);
2899
3154
  syncAggregateActivityState();
@@ -3013,6 +3268,8 @@ async function runSubagent(
3013
3268
  })));
3014
3269
  changed = true;
3015
3270
  }
3271
+ } else if (maybeEmitOpenToolAttention(index, now)) {
3272
+ changed = true;
3016
3273
  } else if (maybeEmitActiveLongRunning(index, now)) {
3017
3274
  changed = true;
3018
3275
  }
@@ -3575,6 +3832,7 @@ async function runSubagent(
3575
3832
  timeoutMessage,
3576
3833
  stopMessage,
3577
3834
  turnBudget: config.turnBudget,
3835
+ toolTimeoutMs: task.toolTimeoutMs ?? config.toolTimeoutMs,
3578
3836
  onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking),
3579
3837
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
3580
3838
  onWriterProcess,
@@ -3958,6 +4216,7 @@ async function runSubagent(
3958
4216
  timeoutMessage,
3959
4217
  stopMessage,
3960
4218
  turnBudget: config.turnBudget,
4219
+ toolTimeoutMs: taskForRun.toolTimeoutMs ?? config.toolTimeoutMs,
3961
4220
  onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking),
3962
4221
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
3963
4222
  onWriterProcess,
@@ -4248,6 +4507,7 @@ async function runSubagent(
4248
4507
  timeoutMessage,
4249
4508
  stopMessage,
4250
4509
  turnBudget: config.turnBudget,
4510
+ toolTimeoutMs: seqStep.toolTimeoutMs ?? config.toolTimeoutMs,
4251
4511
  onAttemptStart: (attempt) => updateStepModel(flatIndex, attempt.model, attempt.thinking),
4252
4512
  onChildEvent: (event) => updateStepFromChildEvent(flatIndex, event),
4253
4513
  onWriterProcess,
@@ -4545,9 +4805,8 @@ async function runSubagent(
4545
4805
  statusPayload.error = `Step failed: ${failedStep.agent}`;
4546
4806
  }
4547
4807
  }
4548
- writeStatusPayload();
4549
4808
  try {
4550
- writeAtomicJson(resultPath, {
4809
+ writeAsyncResultFile(resultPath, {
4551
4810
  lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
4552
4811
  id,
4553
4812
  agent: agentName,
@@ -4642,9 +4901,15 @@ async function runSubagent(
4642
4901
  ...(taskIndex !== undefined && { taskIndex }),
4643
4902
  ...(totalTasks !== undefined && { totalTasks }),
4644
4903
  });
4904
+ finalResultCommitted = true;
4645
4905
  } catch (err) {
4646
- console.error(`Failed to write result file ${resultPath}:`, err);
4906
+ const message = `Failed to write result file ${resultPath}: ${err instanceof Error ? err.message : String(err)}`;
4907
+ console.error(message, err);
4908
+ statusPayload.state = "failed";
4909
+ statusPayload.error = message;
4910
+ statusPayload.lastUpdate = Date.now();
4647
4911
  }
4912
+ writeStatusPayload();
4648
4913
  appendJsonl(
4649
4914
  eventsPath,
4650
4915
  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") {