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
@@ -162,6 +162,8 @@ interface ParallelChainRunInput {
162
162
  nestedRoute?: NestedRouteInfo;
163
163
  timeoutMs?: number;
164
164
  deadlineAt?: number;
165
+ callToolTimeoutMs?: number;
166
+ configToolTimeoutMs?: number;
165
167
  turnBudget?: ResolvedTurnBudget;
166
168
  usageBudget?: UsageBudgetConfig;
167
169
  onDetachedExit?: (index: number, result: SingleResult) => void;
@@ -436,6 +438,8 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
436
438
  onEffectivePrompt: input.foregroundControl ? (prompt) => updateLiveEffectivePrompt(input.foregroundControl!, childIndex, prompt) : undefined,
437
439
  timeoutMs: input.timeoutMs,
438
440
  deadlineAt: input.deadlineAt,
441
+ toolTimeoutMs: input.callToolTimeoutMs,
442
+ configToolTimeoutMs: input.configToolTimeoutMs,
439
443
  turnBudget: input.turnBudget,
440
444
  onDetachedExit: (result) => {
441
445
  try {
@@ -548,6 +552,12 @@ interface ChainExecutionParams {
548
552
  toolBudget?: ResolvedToolBudget;
549
553
  usageBudget?: UsageBudgetConfig;
550
554
  configToolBudget?: ToolBudgetConfig;
555
+ /** Optional per-call hard toolTimeoutMs override (highest precedence). */
556
+ callToolTimeoutMs?: number;
557
+ /** Global config.toolTimeoutMs (third precedence, after agent frontmatter). */
558
+ configToolTimeoutMs?: number;
559
+ /** PI_SUBAGENT_TOOL_TIMEOUT_MS override (lowest precedence). */
560
+ toolTimeoutMsEnv?: string | undefined;
551
561
  permissions?: PermissionConfig;
552
562
  /** Global cap on simultaneously-running tasks within this chain. Defaults to DEFAULT_GLOBAL_CONCURRENCY_LIMIT. */
553
563
  globalConcurrencyLimit?: number;
@@ -881,6 +891,8 @@ ${step.message}` : ""}` }],
881
891
  maxSubagentDepth: params.maxSubagentDepth,
882
892
  timeoutMs: params.timeoutMs,
883
893
  deadlineAt,
894
+ callToolTimeoutMs: params.callToolTimeoutMs,
895
+ configToolTimeoutMs: params.configToolTimeoutMs,
884
896
  turnBudget: params.turnBudget,
885
897
  usageBudget: params.usageBudget,
886
898
  onDetachedExit,
@@ -1156,6 +1168,8 @@ ${step.message}` : ""}` }],
1156
1168
  maxSubagentDepth: params.maxSubagentDepth,
1157
1169
  timeoutMs: params.timeoutMs,
1158
1170
  deadlineAt,
1171
+ callToolTimeoutMs: params.callToolTimeoutMs,
1172
+ configToolTimeoutMs: params.configToolTimeoutMs,
1159
1173
  turnBudget: params.turnBudget,
1160
1174
  usageBudget: params.usageBudget,
1161
1175
  onDetachedExit,
@@ -1440,6 +1454,8 @@ ${step.message}` : ""}` }],
1440
1454
  onEffectivePrompt: foregroundControl ? (prompt) => updateLiveEffectivePrompt(foregroundControl, childIndex, prompt) : undefined,
1441
1455
  timeoutMs: params.timeoutMs,
1442
1456
  deadlineAt,
1457
+ toolTimeoutMs: params.callToolTimeoutMs,
1458
+ configToolTimeoutMs: params.configToolTimeoutMs,
1443
1459
  turnBudget: params.turnBudget,
1444
1460
  onDetachedExit: (result) => {
1445
1461
  try {
@@ -38,6 +38,7 @@ import {
38
38
  buildControlEvent,
39
39
  claimControlNotification,
40
40
  deriveActivityState,
41
+ shouldEmitOpenToolAttention,
41
42
  shouldNotifyControlEvent,
42
43
  } from "../shared/subagent-control.ts";
43
44
  import {
@@ -53,10 +54,12 @@ import {
53
54
  } from "../../shared/utils.ts";
54
55
  import { buildSkillInjection, resolveSkillsWithFallback } from "../../agents/skills.ts";
55
56
  import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
57
+ import { effectiveToolTimeoutMs, formatToolTimeoutMessage, resolveToolTimeoutMs, toolTimeoutCallKey, toolTimeoutFromEnv } from "../shared/tool-timeout.ts";
56
58
  import { evaluateCompletionMutationGuard } from "../shared/completion-guard.ts";
57
59
  import { arbitrateCompletionGuardRescue } from "../shared/llm-intent-arbiter.ts";
58
60
  import { getPiSpawnCommand } from "../shared/pi-spawn.ts";
59
61
  import { createJsonlWriter } from "../../shared/jsonl-writer.ts";
62
+ import { createOrcaProgressTab, type OrcaProgressTab } from "../shared/orca-progress-tabs.ts";
60
63
  import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
61
64
  import { resolvePermissionRules } from "../shared/permissions.ts";
62
65
  import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan, type SubagentTaskDelivery } from "../shared/pi-args.ts";
@@ -74,6 +77,7 @@ import {
74
77
  } from "../shared/model-fallback.ts";
75
78
  import {
76
79
  SUBAGENT_STARTUP_RETRY_DELAYS_MS,
80
+ formatSubagentExtensionConflictError,
77
81
  formatSubagentStartupRetryExhaustedError,
78
82
  formatSubagentStartupRetryNote,
79
83
  isRetryableSubagentStartupFailure,
@@ -302,6 +306,7 @@ async function runSingleAttempt(
302
306
  outputSnapshot?: SingleOutputSnapshot;
303
307
  originalTask?: string;
304
308
  taskDelivery?: SubagentTaskDelivery;
309
+ orcaProgressTab?: OrcaProgressTab;
305
310
  },
306
311
  ): Promise<SingleResult> {
307
312
  const effectiveThinking = options.thinkingOverride ?? agent.thinking;
@@ -670,6 +675,7 @@ async function runSingleAttempt(
670
675
  clearWatchdogTailTimer();
671
676
  clearStdioGuard();
672
677
  clearTimeoutTimers();
678
+ clearAllToolTimeouts();
673
679
  clearTurnBudgetTimers();
674
680
  if (protocolHardKillTimer) {
675
681
  clearTimeout(protocolHardKillTimer);
@@ -694,6 +700,64 @@ async function runSingleAttempt(
694
700
 
695
701
  let activeLongRunningNotified = false;
696
702
  let pendingToolResult: { tool: string; path?: string; mutates: boolean; startedAt?: number } | undefined;
703
+ type ActiveToolCall = { key: string; tool: string; args: string; startedAt: number; path?: string };
704
+ let activeToolSequence = 0;
705
+ const activeToolCalls = new Map<string, ActiveToolCall>();
706
+ const activeToolKeysByName = new Map<string, string[]>();
707
+ const latestActiveToolCall = (): ActiveToolCall | undefined => [...activeToolCalls.values()].sort((left, right) => right.startedAt - left.startedAt)[0];
708
+ const refreshCurrentTool = (): void => {
709
+ const active = latestActiveToolCall();
710
+ if (!active) {
711
+ progress.currentTool = undefined;
712
+ progress.currentToolArgs = undefined;
713
+ progress.currentToolStartedAt = undefined;
714
+ progress.currentPath = undefined;
715
+ return;
716
+ }
717
+ progress.currentTool = active.tool;
718
+ progress.currentToolArgs = active.args;
719
+ progress.currentToolStartedAt = active.startedAt;
720
+ progress.currentPath = active.path;
721
+ };
722
+ const recordActiveToolCall = (event: { toolCallId?: unknown; toolName: string }, args: Record<string, unknown>, now: number): ActiveToolCall => {
723
+ const key = toolTimeoutCallKey(event, ++activeToolSequence);
724
+ const path = resolveCurrentPath(event.toolName, args);
725
+ const active: ActiveToolCall = {
726
+ key,
727
+ tool: event.toolName,
728
+ args: extractToolArgsPreview(args),
729
+ startedAt: now,
730
+ ...(path !== undefined ? { path } : {}),
731
+ };
732
+ activeToolCalls.set(key, active);
733
+ const keys = activeToolKeysByName.get(active.tool) ?? [];
734
+ keys.push(key);
735
+ activeToolKeysByName.set(active.tool, keys);
736
+ refreshCurrentTool();
737
+ return active;
738
+ };
739
+ const removeActiveToolCallKey = (key: string): ActiveToolCall | undefined => {
740
+ const active = activeToolCalls.get(key);
741
+ if (!active) return undefined;
742
+ activeToolCalls.delete(key);
743
+ const keys = activeToolKeysByName.get(active.tool)?.filter((candidate) => candidate !== key) ?? [];
744
+ if (keys.length > 0) activeToolKeysByName.set(active.tool, keys);
745
+ else activeToolKeysByName.delete(active.tool);
746
+ return active;
747
+ };
748
+ const removeActiveToolCall = (event: { toolCallId?: unknown; toolName?: unknown }): ActiveToolCall | undefined => {
749
+ const key = typeof event.toolCallId === "string" && event.toolCallId.length > 0
750
+ ? `id:${event.toolCallId}`
751
+ : typeof event.toolName === "string"
752
+ ? activeToolKeysByName.get(event.toolName)?.[0]
753
+ : activeToolCalls.size === 1
754
+ ? [...activeToolCalls.keys()][0]
755
+ : undefined;
756
+ return key ? removeActiveToolCallKey(key) : undefined;
757
+ };
758
+ const openToolAttentionTarget = (now: number): ActiveToolCall | undefined => [...activeToolCalls.values()]
759
+ .filter((active) => shouldEmitOpenToolAttention({ config: controlConfig, currentTool: active.tool, currentToolStartedAt: active.startedAt, now }))
760
+ .sort((left, right) => left.startedAt - right.startedAt)[0];
697
761
  const mutatingFailures = createMutatingFailureState();
698
762
  const mutatingFailureWindowMs = 5 * 60_000;
699
763
  const currentToolDurationMs = (now: number) => progress.currentToolStartedAt ? Math.max(0, now - progress.currentToolStartedAt) : undefined;
@@ -811,6 +875,17 @@ async function runSingleAttempt(
811
875
  if (idleState === "needs_attention") {
812
876
  return progress.activityState === "needs_attention" ? false : emitNeedsAttention(now);
813
877
  }
878
+ const toolAttentionTarget = progress.activityState !== "needs_attention" ? openToolAttentionTarget(now) : undefined;
879
+ if (toolAttentionTarget) {
880
+ const durationMs = Math.max(0, now - toolAttentionTarget.startedAt);
881
+ return emitNeedsAttention(now, {
882
+ message: `${agent.name} has had tool '${toolAttentionTarget.tool}' open for ${Math.floor(durationMs / 1000)}s`,
883
+ reason: "tool_open_threshold",
884
+ currentTool: toolAttentionTarget.tool,
885
+ currentPath: toolAttentionTarget.path,
886
+ currentToolDurationMs: durationMs,
887
+ });
888
+ }
814
889
  const activeReason = nextLongRunningTrigger(controlConfig, {
815
890
  startedAt: startTime,
816
891
  now,
@@ -848,16 +923,18 @@ async function runSingleAttempt(
848
923
  const processLine = (line: string) => {
849
924
  if (!line.trim()) return;
850
925
  jsonlWriter.writeLine(line);
851
- let evt: { type?: string; message?: Message; toolName?: string; args?: unknown; willRetry?: unknown };
926
+ let evt: { type?: string; message?: Message; toolName?: string; toolCallId?: string; args?: unknown; willRetry?: unknown };
852
927
  try {
853
- evt = JSON.parse(line) as { type?: string; message?: Message; toolName?: string; args?: unknown; willRetry?: unknown };
928
+ evt = JSON.parse(line) as { type?: string; message?: Message; toolName?: string; toolCallId?: string; args?: unknown; willRetry?: unknown };
854
929
  } catch {
855
930
  rawStdoutTail.push(`${line}\n`);
856
931
  shared.transcriptWriter?.writeStdoutLine(line);
932
+ shared.orcaProgressTab?.append(`${line}\n`);
857
933
  // Non-JSON stdout lines are expected; only structured events are parsed.
858
934
  return;
859
935
  }
860
936
  shared.transcriptWriter?.writeChildEvent(evt);
937
+ shared.orcaProgressTab?.event(evt);
861
938
  if (evt.type === "agent_settled") agentSettledReceived = true;
862
939
  applyChildLifecycle(projectChildLifecycle(evt));
863
940
 
@@ -892,6 +969,8 @@ async function runSingleAttempt(
892
969
  const toolArgs = evt.args && typeof evt.args === "object" && !Array.isArray(evt.args)
893
970
  ? evt.args as Record<string, unknown>
894
971
  : {};
972
+ const activeTool = evt.toolName !== undefined ? recordActiveToolCall(evt as { toolCallId?: unknown; toolName: string }, toolArgs, now) : undefined;
973
+ if (evt.toolName !== undefined) armToolTimeout(evt as { toolCallId?: unknown; toolName: string });
895
974
  if (options.structuredOutput && evt.toolName === "structured_output") {
896
975
  structuredOutputToolInvoked = true;
897
976
  structuredOutputMessageStartIndex = result.messages?.length ?? 0;
@@ -903,28 +982,23 @@ async function runSingleAttempt(
903
982
  if (options.toolBudget) {
904
983
  result.toolBudget = toolBudgetState(options.toolBudget, progress.toolCount);
905
984
  }
906
- progress.currentTool = evt.toolName;
907
- progress.currentToolArgs = extractToolArgsPreview(toolArgs);
908
- progress.currentToolStartedAt = now;
909
- progress.currentPath = resolveCurrentPath(evt.toolName, toolArgs);
910
985
  const mutates = isMutatingTool(evt.toolName, toolArgs);
911
986
  observedMutationAttempt = observedMutationAttempt || mutates;
912
- pendingToolResult = { tool: evt.toolName ?? "tool", path: progress.currentPath, mutates, startedAt: now };
987
+ pendingToolResult = { tool: evt.toolName ?? "tool", path: activeTool?.path, mutates, startedAt: now };
913
988
  fireUpdate();
914
989
  }
915
990
 
916
991
  if (evt.type === "tool_execution_end") {
917
- if (progress.currentTool) {
992
+ clearActiveToolTimeout(evt);
993
+ const endedTool = removeActiveToolCall(evt);
994
+ if (endedTool) {
918
995
  progress.recentTools.push({
919
- tool: progress.currentTool,
920
- args: progress.currentToolArgs || "",
996
+ tool: endedTool.tool,
997
+ args: endedTool.args,
921
998
  endMs: now,
922
999
  });
923
1000
  }
924
- progress.currentTool = undefined;
925
- progress.currentToolArgs = undefined;
926
- progress.currentToolStartedAt = undefined;
927
- progress.currentPath = undefined;
1001
+ refreshCurrentTool();
928
1002
  fireUpdate();
929
1003
  }
930
1004
 
@@ -1022,6 +1096,7 @@ async function runSingleAttempt(
1022
1096
  timeoutTimer = setTimeout(() => {
1023
1097
  if (processClosed || lifecycleFinished || interruptedByControl) return;
1024
1098
  result.timedOut = true;
1099
+ clearAllToolTimeouts();
1025
1100
  result.error = attemptTimeout.message;
1026
1101
  result.finalOutput = attemptTimeout.message;
1027
1102
  progress.status = "failed";
@@ -1043,6 +1118,81 @@ async function runSingleAttempt(
1043
1118
  timeoutTimer.unref?.();
1044
1119
  }
1045
1120
 
1121
+ let toolTimeoutSequence = 0;
1122
+ const activeToolTimeouts = new Map<string, { toolName: string; timer: ReturnType<typeof setTimeout> }>();
1123
+ const activeToolTimeoutKeysByName = new Map<string, string[]>();
1124
+ let toolTimeoutTerminationTimer: ReturnType<typeof setTimeout> | undefined;
1125
+ let toolTimeoutHardKillTimer: ReturnType<typeof setTimeout> | undefined;
1126
+ const removeToolTimeoutKey = (key: string): void => {
1127
+ const active = activeToolTimeouts.get(key);
1128
+ if (!active) return;
1129
+ clearTimeout(active.timer);
1130
+ activeToolTimeouts.delete(key);
1131
+ const keys = activeToolTimeoutKeysByName.get(active.toolName)?.filter((candidate) => candidate !== key) ?? [];
1132
+ if (keys.length > 0) activeToolTimeoutKeysByName.set(active.toolName, keys);
1133
+ else activeToolTimeoutKeysByName.delete(active.toolName);
1134
+ };
1135
+ const clearActiveToolTimeout = (event: { toolCallId?: unknown; toolName?: unknown }): void => {
1136
+ const key = typeof event.toolCallId === "string" && event.toolCallId.length > 0
1137
+ ? `id:${event.toolCallId}`
1138
+ : typeof event.toolName === "string"
1139
+ ? activeToolTimeoutKeysByName.get(event.toolName)?.[0]
1140
+ : activeToolTimeouts.size === 1
1141
+ ? [...activeToolTimeouts.keys()][0]
1142
+ : undefined;
1143
+ if (key) removeToolTimeoutKey(key);
1144
+ };
1145
+ const clearAllToolTimeouts = (): void => {
1146
+ for (const key of [...activeToolTimeouts.keys()]) removeToolTimeoutKey(key);
1147
+ if (toolTimeoutTerminationTimer) {
1148
+ clearTimeout(toolTimeoutTerminationTimer);
1149
+ toolTimeoutTerminationTimer = undefined;
1150
+ }
1151
+ if (toolTimeoutHardKillTimer) {
1152
+ clearTimeout(toolTimeoutHardKillTimer);
1153
+ toolTimeoutHardKillTimer = undefined;
1154
+ }
1155
+ };
1156
+ const terminateForToolTimeout = (message: string): void => {
1157
+ if (processClosed || lifecycleFinished || interruptedByControl) return;
1158
+ result.timedOut = true;
1159
+ result.error = message;
1160
+ result.finalOutput = message;
1161
+ progress.status = "failed";
1162
+ progress.error = message;
1163
+ progress.durationMs = Date.now() - startTime;
1164
+ fireUpdate();
1165
+ trySignalChild(proc, "SIGINT");
1166
+ toolTimeoutTerminationTimer = setTimeout(() => {
1167
+ if (processClosed || lifecycleFinished) return;
1168
+ trySignalChild(proc, "SIGTERM");
1169
+ }, 1000);
1170
+ toolTimeoutTerminationTimer.unref?.();
1171
+ toolTimeoutHardKillTimer = setTimeout(() => {
1172
+ if (processClosed || lifecycleFinished) return;
1173
+ trySignalChild(proc, "SIGKILL");
1174
+ }, 4000);
1175
+ toolTimeoutHardKillTimer.unref?.();
1176
+ };
1177
+ const armToolTimeout = (event: { toolCallId?: unknown; toolName: string }): void => {
1178
+ const timeoutForTool = effectiveToolTimeoutMs(event.toolName, options.toolTimeoutMs);
1179
+ if (timeoutForTool === undefined) return;
1180
+ const elapsed = Date.now() - startTime;
1181
+ const runRemaining = attemptTimeout ? Math.max(0, attemptTimeout.remainingMs - elapsed) : undefined;
1182
+ if (runRemaining !== undefined && timeoutForTool >= runRemaining) return;
1183
+ const key = toolTimeoutCallKey(event, ++toolTimeoutSequence);
1184
+ const toolName = event.toolName;
1185
+ const timer = setTimeout(() => {
1186
+ removeToolTimeoutKey(key);
1187
+ terminateForToolTimeout(formatToolTimeoutMessage(toolName, timeoutForTool));
1188
+ }, timeoutForTool);
1189
+ timer.unref?.();
1190
+ activeToolTimeouts.set(key, { toolName, timer });
1191
+ const keys = activeToolTimeoutKeysByName.get(toolName) ?? [];
1192
+ keys.push(key);
1193
+ activeToolTimeoutKeysByName.set(toolName, keys);
1194
+ };
1195
+
1046
1196
  const stderrTail = createBoundedByteTail();
1047
1197
  const failProtocol = (limit: ProtocolOutputLimit): void => {
1048
1198
  if (result.protocolError) return;
@@ -1077,6 +1227,7 @@ async function runSingleAttempt(
1077
1227
  proc.stderr.on("data", (chunk: Buffer) => {
1078
1228
  stderrTail.push(chunk);
1079
1229
  stderrReader.push(chunk);
1230
+ shared.orcaProgressTab?.append(chunk.toString("utf-8"));
1080
1231
  });
1081
1232
  proc.on("exit", () => {
1082
1233
  childExited = true;
@@ -1156,6 +1307,7 @@ async function runSingleAttempt(
1156
1307
  if (result.timedOut) return;
1157
1308
  interruptedByControl = true;
1158
1309
  clearTimeoutTimers();
1310
+ clearAllToolTimeouts();
1159
1311
  progress.status = "running";
1160
1312
  progress.durationMs = Date.now() - startTime;
1161
1313
  result.interrupted = true;
@@ -1201,6 +1353,10 @@ async function runSingleAttempt(
1201
1353
  };
1202
1354
  return result;
1203
1355
  }
1356
+ result.error = formatSubagentExtensionConflictError(result.error, {
1357
+ agent: agent.name,
1358
+ ambientExtensionsEnabled: !launchResolvedExtensions.disableAmbientExtensions,
1359
+ });
1204
1360
  if (result.error && result.exitCode === 0) {
1205
1361
  result.exitCode = 1;
1206
1362
  }
@@ -1378,7 +1534,7 @@ async function runSingleAttempt(
1378
1534
  /**
1379
1535
  * Run a subagent synchronously (blocking until complete)
1380
1536
  */
1381
- async function runSyncCompletion(
1537
+ async function runSyncCompletionInner(
1382
1538
  runtimeCwd: string,
1383
1539
  agents: AgentConfig[],
1384
1540
  agentName: string,
@@ -1427,6 +1583,24 @@ async function runSyncCompletion(
1427
1583
  error: acceptanceErrors.join(" "),
1428
1584
  }, options.context));
1429
1585
  }
1586
+ const toolTimeout = resolveToolTimeoutMs({
1587
+ callValue: options.toolTimeoutMs,
1588
+ agentValue: agent.defaultToolTimeoutMs,
1589
+ configValue: options.configToolTimeoutMs,
1590
+ envValue: toolTimeoutFromEnv(),
1591
+ });
1592
+ if (toolTimeout.error) {
1593
+ return redactResultPrompt(withRunContext({
1594
+ index: options.index ?? 0,
1595
+ agent: agentName,
1596
+ task,
1597
+ exitCode: 1,
1598
+ messages: [],
1599
+ usage: emptyUsage(),
1600
+ error: toolTimeout.error,
1601
+ }, options.context));
1602
+ }
1603
+ options = { ...options, toolTimeoutMs: toolTimeout.toolTimeoutMs };
1430
1604
  const outputModeValidationError = validateFileOnlyOutputMode(options.outputMode, options.outputPath, `Single run (${agentName})`);
1431
1605
  if (outputModeValidationError) {
1432
1606
  return redactResultPrompt(withRunContext({
@@ -1531,6 +1705,14 @@ async function runSyncCompletion(
1531
1705
  }
1532
1706
  }
1533
1707
 
1708
+ const orcaProgressTab = createOrcaProgressTab({
1709
+ cwd: options.cwd ?? runtimeCwd,
1710
+ runId: options.runId,
1711
+ agent: agentName,
1712
+ index: options.index ?? 0,
1713
+ });
1714
+ if (orcaProgressTab) options.onOrcaProgressTabCreated?.(orcaProgressTab);
1715
+
1534
1716
  const persistResultMetadata = (target: SingleResult): void => {
1535
1717
  persistSingleResultMetadata({
1536
1718
  metadataPath: artifactPathsResult?.metadataPath,
@@ -1585,6 +1767,7 @@ async function runSyncCompletion(
1585
1767
  outputSnapshot,
1586
1768
  originalTask: task,
1587
1769
  taskDelivery: taskDeliveryOverride,
1770
+ orcaProgressTab,
1588
1771
  });
1589
1772
  lastResult = result;
1590
1773
  if (startupAttemptIndex === 0) {
@@ -1813,6 +1996,27 @@ async function runSyncCompletion(
1813
1996
  return result;
1814
1997
  }
1815
1998
 
1999
+ async function runSyncCompletion(
2000
+ runtimeCwd: string,
2001
+ agents: AgentConfig[],
2002
+ agentName: string,
2003
+ task: string,
2004
+ options: RunSyncOptions,
2005
+ ): Promise<SingleResult> {
2006
+ let orcaProgressTab: OrcaProgressTab | undefined;
2007
+ try {
2008
+ const result = await runSyncCompletionInner(runtimeCwd, agents, agentName, task, {
2009
+ ...options,
2010
+ onOrcaProgressTabCreated: (tab) => { orcaProgressTab = tab; },
2011
+ });
2012
+ orcaProgressTab?.finish(result.stopped ? "stopped" : result.exitCode === 0 && !result.error ? "completed" : "failed", result.sessionFile);
2013
+ return result;
2014
+ } catch (error) {
2015
+ orcaProgressTab?.finish("failed");
2016
+ throw error;
2017
+ }
2018
+ }
2019
+
1816
2020
  /**
1817
2021
  * Runs the authoritative completion pipeline independently from the foreground
1818
2022
  * receipt. Detachment is same-runtime only: it does not adopt or daemonize work.