pi-subagents 0.48.0 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +82 -19
  2. package/agents/oracle.md +7 -5
  3. package/agents/researcher.md +1 -1
  4. package/agents/reviewer.md +2 -2
  5. package/agents/scout.md +1 -1
  6. package/agents/worker.md +1 -1
  7. package/docs/agents.md +3 -0
  8. package/docs/configuration.md +75 -8
  9. package/docs/extension-api.md +36 -0
  10. package/docs/missions.md +5 -3
  11. package/docs/observability.md +42 -2
  12. package/docs/tool-reference.md +19 -2
  13. package/docs/workflows.md +2 -2
  14. package/package.json +1 -1
  15. package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
  16. package/skills/pi-subagents/references/execution-controls.md +5 -4
  17. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  18. package/skills/pi-subagents/references/prompting-and-roles.md +31 -15
  19. package/src/agents/agent-serializer.ts +2 -0
  20. package/src/agents/agents.ts +37 -12
  21. package/src/api/external-runs.ts +174 -84
  22. package/src/api/preflight.ts +13 -7
  23. package/src/extension/config.ts +53 -0
  24. package/src/extension/index.ts +62 -9
  25. package/src/extension/public-execution.ts +32 -4
  26. package/src/extension/rpc.ts +5 -1
  27. package/src/extension/schemas.ts +10 -8
  28. package/src/extension/tool-description.ts +13 -9
  29. package/src/inspectors/herdr/actions.ts +13 -8
  30. package/src/inspectors/herdr/inspector-runner.ts +16 -3
  31. package/src/inspectors/herdr/project-panes.ts +2 -6
  32. package/src/inspectors/herdr/shell-command.ts +16 -0
  33. package/src/intercom/intercom-bridge.ts +5 -4
  34. package/src/intercom/native-supervisor-channel.ts +19 -42
  35. package/src/missions/goal-driver.ts +3 -1
  36. package/src/missions/store.ts +8 -3
  37. package/src/runs/background/active-async-capacity.ts +82 -25
  38. package/src/runs/background/active-run-index.ts +71 -1
  39. package/src/runs/background/async-execution.ts +73 -43
  40. package/src/runs/background/async-job-tracker.ts +5 -0
  41. package/src/runs/background/async-resume.ts +14 -6
  42. package/src/runs/background/async-status-snapshot.ts +277 -0
  43. package/src/runs/background/async-status.ts +8 -3
  44. package/src/runs/background/chain-root-attachment.ts +2 -2
  45. package/src/runs/background/completion-replay.ts +11 -1
  46. package/src/runs/background/fleet-view.ts +21 -6
  47. package/src/runs/background/result-files.ts +437 -0
  48. package/src/runs/background/result-watcher.ts +188 -41
  49. package/src/runs/background/resume-guidance.ts +27 -7
  50. package/src/runs/background/retained-children.ts +75 -18
  51. package/src/runs/background/run-id-resolver.ts +30 -24
  52. package/src/runs/background/run-status.ts +101 -4
  53. package/src/runs/background/scheduled-runs.ts +54 -28
  54. package/src/runs/background/stale-run-reconciler.ts +27 -13
  55. package/src/runs/background/subagent-runner.ts +298 -33
  56. package/src/runs/background/subagent-wait.ts +2 -0
  57. package/src/runs/background/wait-completions.ts +5 -2
  58. package/src/runs/foreground/async-dismiss-action.ts +2 -1
  59. package/src/runs/foreground/chain-execution.ts +16 -0
  60. package/src/runs/foreground/execution.ts +219 -15
  61. package/src/runs/foreground/prompt-audit.ts +4 -3
  62. package/src/runs/foreground/subagent-executor.ts +328 -43
  63. package/src/runs/shared/completion-guard.ts +107 -1
  64. package/src/runs/shared/external-cli-runner.ts +4 -0
  65. package/src/runs/shared/llm-intent-arbiter.ts +39 -23
  66. package/src/runs/shared/model-fallback.ts +16 -2
  67. package/src/runs/shared/nested-events.ts +66 -62
  68. package/src/runs/shared/orca-progress-tabs.ts +375 -0
  69. package/src/runs/shared/parallel-utils.ts +2 -0
  70. package/src/runs/shared/subagent-control.ts +15 -0
  71. package/src/runs/shared/subagent-prompt-runtime.ts +1 -9
  72. package/src/runs/shared/subagent-startup-retry.ts +12 -0
  73. package/src/runs/shared/tool-timeout.ts +93 -0
  74. package/src/shared/agent-stream-options.ts +5 -0
  75. package/src/shared/artifacts.ts +2 -6
  76. package/src/shared/display-text.ts +50 -0
  77. package/src/shared/node-executable.ts +21 -0
  78. package/src/shared/types.ts +51 -5
  79. package/src/slash/slash-commands.ts +34 -25
  80. package/src/slash/slash-live-state.ts +3 -0
  81. package/src/tui/fleet-status.ts +160 -45
  82. package/src/tui/fleet-transcript.ts +1 -48
  83. package/src/tui/fleet.ts +128 -16
  84. package/src/tui/render.ts +122 -44
  85. package/src/watchdog/permission-arbiter.ts +2 -1
  86. package/src/watchdog/review.ts +4 -3
  87. package/src/workflows/chat-progress.ts +10 -2
  88. package/src/workflows/scripted-workflow.ts +272 -76
@@ -101,6 +101,7 @@ import { stopAsyncRun } from "./async-stop-action.ts";
101
101
  import { dismissRecoveredWorkflow } from "./async-dismiss-action.ts";
102
102
  import { reconcileAsyncRun } from "../background/stale-run-reconciler.ts";
103
103
  import { resolveAsyncRootResultPath, waitForImportedAsyncRoot } from "../background/chain-root-attachment.ts";
104
+ import { resultFilePath, writeAsyncResultFile } from "../background/result-files.ts";
104
105
  import { attachRootChildrenToSteps, createNestedRoute, findNestedControlResult, resolveInheritedNestedRouteFromEnv, resolveNestedAsyncDir, resolveNestedParentAddressFromEnv, snapshotNestedEventFiles, updateForegroundNestedProjection, writeNestedControlRequest, writeNestedEvent, type NestedRunResolutionScope } from "../shared/nested-events.ts";
105
106
  import { resolveSubagentRunId, type ResolvedSubagentRunId } from "../background/run-id-resolver.ts";
106
107
  import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
@@ -108,7 +109,7 @@ import { inspectSubagentStatus } from "../background/run-status.ts";
108
109
  import { applyForceTopLevelAsyncOverride } from "../background/top-level-async.ts";
109
110
  import { handleMissionAction, MISSION_ACTIONS } from "../../missions/actions.ts";
110
111
  import { attachMissionToLaunchResult, prepareMissionLaunch, writeMissionAsyncBinding, type MissionLaunchBinding } from "../../missions/lifecycle.ts";
111
- import { updateMission } from "../../missions/store.ts";
112
+ import { MissionNotFoundError, updateMission } from "../../missions/store.ts";
112
113
  import type { MissionWorkflowChildUpdate } from "../../missions/types.ts";
113
114
  import { createMissionWorkflowState } from "../../missions/workflow-state.ts";
114
115
  import { resolveAuthorityDecision } from "../../policy/authority.ts";
@@ -139,6 +140,7 @@ import {
139
140
  type Details,
140
141
  type ExtensionConfig,
141
142
  type ForegroundRunControl,
143
+ type IntercomBridgeConfig,
142
144
  type IntercomEventBus,
143
145
  type JsonSchemaObject,
144
146
  type MaxOutputConfig,
@@ -300,10 +302,14 @@ export interface SubagentParamsLike {
300
302
  concurrency?: number;
301
303
  worktree?: boolean;
302
304
  context?: "fresh" | "fork";
305
+ /** Per-run intercom bridge config. It replaces the global config for this launch only. */
306
+ intercomBridge?: IntercomBridgeConfig;
303
307
  async?: boolean;
304
308
  foregroundOnly?: boolean;
305
309
  timeoutMs?: number;
306
310
  maxRuntimeMs?: number;
311
+ /** Optional hard per-tool-call timeout (ms). Known-fast tools also have a default. */
312
+ toolTimeoutMs?: number;
307
313
  turnBudget?: TurnBudgetConfig;
308
314
  /** Internal-only strict turn-boundary enforcement for versioned foreground delegation. */
309
315
  enforceHardTurnLimit?: boolean;
@@ -387,6 +393,8 @@ interface ExecutionContextData {
387
393
  signal: AbortSignal;
388
394
  onUpdate?: (r: AgentToolResult<Details>) => void;
389
395
  agents: AgentConfig[];
396
+ /** Discovered agent definitions before per-run bridge injection. */
397
+ recoveryAgents: AgentConfig[];
390
398
  runId: string;
391
399
  shareEnabled: boolean;
392
400
  sessionRoot: string;
@@ -404,6 +412,8 @@ interface ExecutionContextData {
404
412
  nestedRoute?: NestedRouteInfo;
405
413
  timeoutMs?: number;
406
414
  deadlineAt?: number;
415
+ /** Raw global config.toolTimeoutMs, for per-step resolution in async runners. */
416
+ configToolTimeoutMs?: number;
407
417
  turnBudget?: ResolvedTurnBudget;
408
418
  toolBudget?: ResolvedToolBudget;
409
419
  usageBudget?: UsageBudgetConfig;
@@ -1511,17 +1521,18 @@ async function resumeAsyncRun(input: {
1511
1521
  const discoveredAgents = discovered.agents;
1512
1522
  const modelScope = discovered.modelScope;
1513
1523
  const sessionName = resolveIntercomSessionTarget(input.deps.pi.getSessionName(), input.ctx.sessionManager.getSessionId());
1524
+ const recoveryDescriptor = "recoveryDescriptor" in target ? target.recoveryDescriptor : undefined;
1514
1525
  const intercomBridge = resolveIntercomBridge({
1515
1526
  config: input.deps.config.intercomBridge,
1527
+ override: input.params.intercomBridge ?? recoveryDescriptor?.intercomBridge,
1516
1528
  context: input.params.context,
1517
1529
  orchestratorTarget: sessionName,
1518
1530
  });
1519
1531
  const agents = intercomBridge.active
1520
1532
  ? discoveredAgents.map((agent) => applyIntercomBridgeToAgent(agent, intercomBridge))
1521
1533
  : discoveredAgents;
1522
- const recoveryDescriptor = "recoveryDescriptor" in target ? target.recoveryDescriptor : undefined;
1523
- const discoveredAgentConfig = agents.find((agent) => agent.name === target.agent);
1524
- const agentConfig: AgentConfig | undefined = discoveredAgentConfig ?? (recoveryDescriptor ? {
1534
+ const discoveredAgentConfig = discoveredAgents.find((agent) => agent.name === target.agent);
1535
+ const baseAgentConfig: AgentConfig | undefined = discoveredAgentConfig ?? (recoveryDescriptor ? {
1525
1536
  name: recoveryDescriptor.agent,
1526
1537
  description: "Persisted async recovery contract",
1527
1538
  systemPrompt: "",
@@ -1531,7 +1542,7 @@ async function resumeAsyncRun(input: {
1531
1542
  source: "project",
1532
1543
  filePath: recoveryDescriptor.agentFilePath ?? path.join(getProjectSubagentsDir(recoveryDescriptor.cwd), "recovery-agent"),
1533
1544
  } : undefined);
1534
- if (!agentConfig) {
1545
+ if (!baseAgentConfig) {
1535
1546
  return {
1536
1547
  content: [{ type: "text", text: `Unknown agent for resume: ${target.agent}` }],
1537
1548
  isError: true,
@@ -1672,7 +1683,8 @@ async function resumeAsyncRun(input: {
1672
1683
  if (error instanceof ActiveAsyncCapacityError) return { content: [{ type: "text", text: error.message }], isError: true, details: { mode: "single", results: [], activeAsyncCapacity: error.snapshot } };
1673
1684
  return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, details: { mode: "single", results: [] } };
1674
1685
  }
1675
- const recoveryAgentConfig = recoveryDescriptor ? applySteeringRecoveryAgentConfig(agentConfig, recoveryDescriptor) : agentConfig;
1686
+ const recoveryAgentConfig = recoveryDescriptor ? applySteeringRecoveryAgentConfig(baseAgentConfig, recoveryDescriptor) : baseAgentConfig;
1687
+ const agentConfig = intercomBridge.active ? applyIntercomBridgeToAgent(recoveryAgentConfig, intercomBridge) : recoveryAgentConfig;
1676
1688
  const artifactConfig: ArtifactConfig = recoveryDescriptor?.artifactConfig ?? omitUndefinedProperties({ ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false, dir: input.deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir });
1677
1689
  const artifactsDir = recoveryDescriptor?.artifactsDir ?? getArtifactsDir(parentSessionFile, effectiveCwd, artifactConfig.dir);
1678
1690
  const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo);
@@ -1682,7 +1694,8 @@ async function resumeAsyncRun(input: {
1682
1694
  agent: target.agent,
1683
1695
  task: buildRevivedAsyncTask(target as Parameters<typeof buildRevivedAsyncTask>[0], effectiveFollowUp),
1684
1696
  goal: effectiveFollowUp,
1685
- agentConfig: recoveryAgentConfig,
1697
+ agentConfig,
1698
+ recoveryAgentConfig,
1686
1699
  ctx: compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
1687
1700
  pi: input.deps.pi,
1688
1701
  cwd: input.requestCwd,
@@ -1717,6 +1730,7 @@ async function resumeAsyncRun(input: {
1717
1730
  worktreeSetupHookTimeoutMs: input.deps.config.worktreeSetupHookTimeoutMs,
1718
1731
  worktreeBaseDir: input.deps.config.worktreeBaseDir,
1719
1732
  controlConfig: recoveryDescriptor?.controlConfig ?? resolveControlConfig(input.deps.config.control, input.params.control),
1733
+ intercomBridge: input.params.intercomBridge ?? recoveryDescriptor?.intercomBridge,
1720
1734
  controlIntercomTarget: intercomBridge.active ? intercomBridge.orchestratorTarget : undefined,
1721
1735
  childIntercomTarget: intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
1722
1736
  availableModels,
@@ -1731,7 +1745,7 @@ async function resumeAsyncRun(input: {
1731
1745
  ...(input.params.turnBudget !== undefined ? { turnBudget: input.params.turnBudget } : {}),
1732
1746
  ...(input.params.toolBudget !== undefined ? { toolBudget: input.params.toolBudget } : {}),
1733
1747
  capabilityCeiling: intersectSubagentCapabilityCeilings("capabilityCeiling" in target ? target.capabilityCeiling : undefined, recoveryDescriptor?.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(input.deps.state.currentSessionId)),
1734
- runFanoutBudget: recoveryDescriptor?.runFanoutBudget ?? createRunFanoutBudget(runId, resolveMaxSubagentSpawnsPerRun(input.deps.config.maxSubagentSpawnsPerRun)),
1748
+ runFanoutBudget: input.params.runFanoutBudget ?? recoveryDescriptor?.runFanoutBudget ?? createRunFanoutBudget(runId, resolveMaxSubagentSpawnsPerRun(input.deps.config.maxSubagentSpawnsPerRun)),
1735
1749
  parentWorkflowRunId: input.params.workflowParentRunId,
1736
1750
  workflowKey: input.params.workflowKey,
1737
1751
  activeAsyncCapacity,
@@ -2497,16 +2511,19 @@ function resolveStaticLaunchSummary(input: {
2497
2511
  thinkingOverrideForTask: ForkThinkingOverrideForTask;
2498
2512
  }): StaticLaunchSummary {
2499
2513
  const agentConfig = input.agents.find((agent) => agent.name === input.agent);
2500
- const model = resolveEffectiveSubagentModel(
2501
- input.explicitModel,
2502
- agentConfig?.model,
2503
- input.parentModel,
2504
- input.availableModels,
2505
- input.currentProvider,
2506
- input.modelScope === undefined ? {} : { scope: input.modelScope },
2507
- );
2508
- const thinkingOverride = input.thinkingOverrideForTask(input.agent, input.index, model);
2509
- const thinking = resolveEffectiveThinking(model, thinkingOverride ?? agentConfig?.thinking);
2514
+ const externalRunner = agentConfig?.runner?.type === "external-cli";
2515
+ const model = externalRunner
2516
+ ? undefined
2517
+ : resolveEffectiveSubagentModel(
2518
+ input.explicitModel,
2519
+ agentConfig?.model,
2520
+ input.parentModel,
2521
+ input.availableModels,
2522
+ input.currentProvider,
2523
+ input.modelScope === undefined ? {} : { scope: input.modelScope },
2524
+ );
2525
+ const thinkingOverride = externalRunner ? undefined : input.thinkingOverrideForTask(input.agent, input.index, model);
2526
+ const thinking = externalRunner ? undefined : resolveEffectiveThinking(model, thinkingOverride ?? agentConfig?.thinking);
2510
2527
  return {
2511
2528
  agent: input.agent,
2512
2529
  ...(model ? { model } : {}),
@@ -2789,6 +2806,8 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2789
2806
  toolBudget: data.toolBudget,
2790
2807
  usageBudget: data.usageBudget,
2791
2808
  configToolBudget: data.configToolBudget,
2809
+ callToolTimeoutMs: data.params?.toolTimeoutMs,
2810
+ configToolTimeoutMs: data.configToolTimeoutMs,
2792
2811
  capabilityCeiling: data.capabilityCeiling,
2793
2812
  runFanoutBudget: data.runFanoutBudget,
2794
2813
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
@@ -2836,6 +2855,8 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2836
2855
  toolBudget: data.toolBudget,
2837
2856
  usageBudget: data.usageBudget,
2838
2857
  configToolBudget: data.configToolBudget,
2858
+ callToolTimeoutMs: data.params?.toolTimeoutMs,
2859
+ configToolTimeoutMs: data.configToolTimeoutMs,
2839
2860
  capabilityCeiling: data.capabilityCeiling,
2840
2861
  runFanoutBudget: data.runFanoutBudget,
2841
2862
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
@@ -2862,14 +2883,15 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2862
2883
  const externalRunnerWithoutExplicitModel = a.runner?.type === "external-cli"
2863
2884
  && params.model === undefined
2864
2885
  && (a.model === undefined || (a.modelSource?.type === "subagents.defaultModel" && a.model === a.modelSource.model));
2865
- const modelOverride = externalRunnerWithoutExplicitModel
2866
- ? undefined
2886
+ const modelOverride = a.runner?.type === "external-cli"
2887
+ ? params.model ?? (externalRunnerWithoutExplicitModel ? undefined : a.model)
2867
2888
  : resolveEffectiveSubagentModel(params.model as string | undefined, a.model, parentModel, availableModels, currentProvider, data.modelScope === undefined ? {} : { scope: data.modelScope });
2868
2889
  return executeAsyncSingle(id, compactOptional<Parameters<typeof executeAsyncSingle>[1]>({
2869
2890
  agent: params.agent!,
2870
2891
  task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(params.task ?? "") : (params.task ?? ""),
2871
2892
  goal: params.task ?? "",
2872
2893
  agentConfig: a,
2894
+ recoveryAgentConfig: data.recoveryAgents.find((agent) => agent.name === params.agent),
2873
2895
  ctx: asyncCtx,
2874
2896
  availableModels,
2875
2897
  cwd: effectiveCwd,
@@ -2894,6 +2916,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2894
2916
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
2895
2917
  worktreeBaseDir: deps.config.worktreeBaseDir,
2896
2918
  controlConfig,
2919
+ intercomBridge: params.intercomBridge,
2897
2920
  controlIntercomTarget,
2898
2921
  childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(agent, index) : undefined,
2899
2922
  nestedRoute,
@@ -2905,6 +2928,8 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2905
2928
  toolBudget: data.toolBudget,
2906
2929
  usageBudget: data.usageBudget,
2907
2930
  configToolBudget: data.configToolBudget,
2931
+ toolTimeoutMs: data.params?.toolTimeoutMs,
2932
+ configToolTimeoutMs: data.configToolTimeoutMs,
2908
2933
  capabilityCeiling: data.capabilityCeiling,
2909
2934
  runFanoutBudget: data.runFanoutBudget,
2910
2935
  parentWorkflowRunId: params.workflowParentRunId,
@@ -2994,6 +3019,8 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2994
3019
  toolBudget: data.toolBudget,
2995
3020
  usageBudget: data.usageBudget,
2996
3021
  configToolBudget: data.configToolBudget,
3022
+ callToolTimeoutMs: data.params?.toolTimeoutMs,
3023
+ configToolTimeoutMs: data.configToolTimeoutMs,
2997
3024
  permissions: deps.config.permissions,
2998
3025
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2999
3026
  capabilityCeiling: data.capabilityCeiling,
@@ -3061,6 +3088,8 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
3061
3088
  toolBudget: data.toolBudget,
3062
3089
  usageBudget: data.usageBudget,
3063
3090
  configToolBudget: data.configToolBudget,
3091
+ callToolTimeoutMs: data.params?.toolTimeoutMs,
3092
+ configToolTimeoutMs: data.configToolTimeoutMs,
3064
3093
  capabilityCeiling: data.capabilityCeiling,
3065
3094
  runFanoutBudget: data.runFanoutBudget,
3066
3095
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
@@ -3148,6 +3177,10 @@ interface ForegroundParallelRunInput {
3148
3177
  worktreeSetup?: WorktreeSetup;
3149
3178
  timeoutMs?: number;
3150
3179
  deadlineAt?: number;
3180
+ /** Per-call per-tool timeout, resolved separately for each task's agent. */
3181
+ toolTimeoutMs?: number;
3182
+ /** Raw global config.toolTimeoutMs, used by each task's resolver. */
3183
+ configToolTimeoutMs?: number;
3151
3184
  turnBudget?: ResolvedTurnBudget;
3152
3185
  usageBudget?: UsageBudgetConfig;
3153
3186
  toolBudgets: (ResolvedToolBudget | undefined)[];
@@ -3239,10 +3272,128 @@ function buildParallelWorktreeTaskCwdError(
3239
3272
  return formatWorktreeTaskCwdConflict(conflict, sharedCwd);
3240
3273
  }
3241
3274
 
3242
- function resolveSingleRunOutputBaseDir(deps: ExecutorDeps, artifactsDir: string, runId: string): string {
3275
+ function resolveConfiguredSingleRunOutputBaseDir(deps: ExecutorDeps): string | undefined {
3243
3276
  return deps.config.singleRunOutputBaseDir
3244
3277
  ? path.resolve(deps.expandTilde(deps.config.singleRunOutputBaseDir))
3245
- : path.join(artifactsDir, "outputs", runId);
3278
+ : undefined;
3279
+ }
3280
+
3281
+ function resolveSingleRunOutputBaseDir(deps: ExecutorDeps, artifactsDir: string, runId: string): string {
3282
+ return resolveConfiguredSingleRunOutputBaseDir(deps) ?? path.join(artifactsDir, "outputs", runId);
3283
+ }
3284
+
3285
+ function resolveWorkflowAggregateOutputPath(
3286
+ output: string | boolean | undefined,
3287
+ ctxCwd: string,
3288
+ workflowCwd: string,
3289
+ outputBaseDir: string,
3290
+ configuredOutputBaseDir: string | undefined,
3291
+ ): string | undefined {
3292
+ if (typeof output === "string" && output && !path.isAbsolute(output) && !configuredOutputBaseDir) return path.resolve(workflowCwd, output);
3293
+ return resolveSingleOutputPath(output, ctxCwd, workflowCwd, outputBaseDir);
3294
+ }
3295
+
3296
+ function workflowChildDefaultOutput(aggregateOutputPath: string | undefined, artifactsDir: string, workflowRunId: string, workflowKey: string): string {
3297
+ if (aggregateOutputPath) {
3298
+ const parsed = path.parse(aggregateOutputPath);
3299
+ return path.join(parsed.dir, `${parsed.name}.${workflowKey}${parsed.ext || ".md"}`);
3300
+ }
3301
+ return path.join(artifactsDir, "outputs", workflowRunId, `${workflowKey}.md`);
3302
+ }
3303
+
3304
+ function writeWorkflowAggregateOutput(outputPath: string | undefined, text: string): string | undefined {
3305
+ if (!outputPath) return undefined;
3306
+ try {
3307
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
3308
+ fs.writeFileSync(outputPath, text, "utf-8");
3309
+ return undefined;
3310
+ } catch (error) {
3311
+ return `Output file error: ${outputPath}\n${error instanceof Error ? error.message : String(error)}`;
3312
+ }
3313
+ }
3314
+
3315
+ function appendWorkflowOutputWarning(text: string, warning: string | undefined): string {
3316
+ return warning ? `${text}\n\n${warning}` : text;
3317
+ }
3318
+
3319
+ function resolveWorkflowChildOutputPath(input: {
3320
+ ctxCwd: string;
3321
+ workflowCwd: string;
3322
+ artifactsDir: string;
3323
+ workflowRunId: string;
3324
+ aggregateOutputPath?: string;
3325
+ configuredOutputBaseDir?: string;
3326
+ discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[] };
3327
+ workflowAgentScope?: unknown;
3328
+ key: string;
3329
+ params: Record<string, unknown>;
3330
+ }): string | undefined {
3331
+ const rawOutput = input.params.output;
3332
+ const hasExplicitOutput = typeof rawOutput === "string" || typeof rawOutput === "boolean";
3333
+ const childCwd = typeof input.params.cwd === "string" ? resolveChildCwd(input.workflowCwd, input.params.cwd) : input.workflowCwd;
3334
+ const agentScope = resolveExecutionAgentScope(input.params.agentScope ?? input.workflowAgentScope);
3335
+ const agents = input.discoverAgents(childCwd, agentScope).agents;
3336
+ const agent = typeof input.params.agent === "string" ? resolveAgentName(input.params.agent, agents).agent : undefined;
3337
+ const agentOutput = typeof agent?.output === "string" ? agent.output : undefined;
3338
+ const output = rawOutput === true || rawOutput === "true"
3339
+ ? agentOutput
3340
+ : hasExplicitOutput
3341
+ ? rawOutput
3342
+ : workflowChildDefaultOutput(input.aggregateOutputPath, input.artifactsDir, input.workflowRunId, input.key);
3343
+ return resolveSingleOutputPath(output, input.ctxCwd, childCwd, input.configuredOutputBaseDir);
3344
+ }
3345
+
3346
+ function workflowChildOutputClaims(input: {
3347
+ ctxCwd: string;
3348
+ workflowCwd: string;
3349
+ artifactsDir: string;
3350
+ workflowRunId: string;
3351
+ aggregateOutputPath?: string;
3352
+ configuredOutputBaseDir?: string;
3353
+ discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[] };
3354
+ workflowAgentScope?: unknown;
3355
+ claimedOutputPaths: Map<string, string>;
3356
+ entries: Array<{ key: string; params: Record<string, unknown> }>;
3357
+ }): { error?: string; claims?: Map<string, string> } {
3358
+ const claims = new Map(input.claimedOutputPaths);
3359
+ for (const { key, params } of input.entries) {
3360
+ const resolved = resolveWorkflowChildOutputPath({ ...input, key, params });
3361
+ if (!resolved) continue;
3362
+ if (input.aggregateOutputPath && resolved === input.aggregateOutputPath) return { error: `Workflow child '${key}' output resolves to the workflow aggregate output path: ${resolved}. Use a distinct child output path.` };
3363
+ const previous = claims.get(resolved);
3364
+ if (previous) return { error: `Workflow children '${previous}' and '${key}' resolve output to the same path: ${resolved}. Use distinct child output paths.` };
3365
+ claims.set(resolved, key);
3366
+ }
3367
+ return { claims };
3368
+ }
3369
+
3370
+ function applyWorkflowChildOutputClaims(target: Map<string, string>, claims: Map<string, string>): void {
3371
+ target.clear();
3372
+ for (const [resolved, key] of claims) target.set(resolved, key);
3373
+ }
3374
+
3375
+ function prepareWorkflowChildLaunchParams(input: {
3376
+ workflowDefaults: SubagentParamsLike;
3377
+ childParams: Record<string, unknown>;
3378
+ parentWorkflowRunId: string;
3379
+ workflowKey: string;
3380
+ ctxCwd: string;
3381
+ workflowCwd: string;
3382
+ artifactsDir: string;
3383
+ aggregateOutputPath?: string;
3384
+ configuredOutputBaseDir?: string;
3385
+ discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[] };
3386
+ workflowAgentScope?: unknown;
3387
+ options?: { missionDetached?: boolean; suppressRoutineResultIntercom?: boolean; runFanoutBudget?: RunFanoutBudgetDescriptor };
3388
+ }): SubagentParamsLike {
3389
+ let childParams = input.childParams;
3390
+ if (input.childParams.output === undefined && input.childParams.resume === undefined) {
3391
+ childParams = { ...input.childParams, output: workflowChildDefaultOutput(input.aggregateOutputPath, input.artifactsDir, input.parentWorkflowRunId, input.workflowKey) };
3392
+ } else if (input.childParams.resume === undefined) {
3393
+ const resolvedOutput = resolveWorkflowChildOutputPath({ ctxCwd: input.ctxCwd, workflowCwd: input.workflowCwd, artifactsDir: input.artifactsDir, workflowRunId: input.parentWorkflowRunId, aggregateOutputPath: input.aggregateOutputPath, configuredOutputBaseDir: input.configuredOutputBaseDir, discoverAgents: input.discoverAgents, workflowAgentScope: input.workflowAgentScope, key: input.workflowKey, params: input.childParams });
3394
+ if (resolvedOutput) childParams = { ...input.childParams, output: resolvedOutput };
3395
+ }
3396
+ return prepareWorkflowLaunchParams(input.workflowDefaults, childParams, input.parentWorkflowRunId, input.workflowKey, input.options);
3246
3397
  }
3247
3398
 
3248
3399
  function buildChainWorktreeTaskCwdError(chain: ChainStep[], sharedCwd: string): string | undefined {
@@ -3406,6 +3557,8 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
3406
3557
  share: input.shareEnabled || undefined,
3407
3558
  maxOutput: input.maxOutput,
3408
3559
  timeoutMs: input.timeoutMs,
3560
+ toolTimeoutMs: input.toolTimeoutMs,
3561
+ configToolTimeoutMs: input.configToolTimeoutMs,
3409
3562
  turnBudget: input.turnBudget,
3410
3563
  ...(model ? { model } : {}),
3411
3564
  ...(effectiveSkills !== undefined ? { skill: effectiveSkills } : {}),
@@ -3485,6 +3638,8 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
3485
3638
  acceptanceContext: { mode: "parallel" },
3486
3639
  onEffectivePrompt: input.foregroundControl ? (prompt) => updateLiveEffectivePrompt(input.foregroundControl!, index, prompt) : undefined,
3487
3640
  timeoutMs: input.timeoutMs,
3641
+ toolTimeoutMs: input.toolTimeoutMs,
3642
+ configToolTimeoutMs: input.configToolTimeoutMs,
3488
3643
  deadlineAt: input.deadlineAt,
3489
3644
  turnBudget: input.turnBudget,
3490
3645
  toolBudget: input.toolBudgets[index],
@@ -3612,7 +3767,9 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3612
3767
  ...(task.model !== undefined ? { model: task.model } : {}),
3613
3768
  }));
3614
3769
  const modelOverrides: (string | undefined)[] = tasks.map((_, i) =>
3615
- resolveEffectiveSubagentModel(behaviorOverrides[i]?.model, agentConfigs[i]?.model, parentModel, availableModels, currentProvider, data.modelScope === undefined ? {} : { scope: data.modelScope }),
3770
+ agentConfigs[i]?.runner?.type === "external-cli"
3771
+ ? undefined
3772
+ : resolveEffectiveSubagentModel(behaviorOverrides[i]?.model, agentConfigs[i]?.model, parentModel, availableModels, currentProvider, data.modelScope === undefined ? {} : { scope: data.modelScope }),
3616
3773
  );
3617
3774
 
3618
3775
  if (params.clarify === true && ctx.hasUI) {
@@ -3647,7 +3804,9 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3647
3804
  for (let i = 0; i < result.behaviorOverrides.length; i++) {
3648
3805
  const override = result.behaviorOverrides[i];
3649
3806
  if (override?.model !== undefined) {
3650
- modelOverrides[i] = resolveEffectiveSubagentModel(override.model, agentConfigs[i]?.model, parentModel, availableModels, currentProvider, data.modelScope === undefined ? {} : { scope: data.modelScope });
3807
+ modelOverrides[i] = agentConfigs[i]?.runner?.type === "external-cli"
3808
+ ? undefined
3809
+ : resolveEffectiveSubagentModel(override.model, agentConfigs[i]?.model, parentModel, availableModels, currentProvider, data.modelScope === undefined ? {} : { scope: data.modelScope });
3651
3810
  behaviorOverrides[i]!.model = override.model;
3652
3811
  }
3653
3812
  if (override?.output !== undefined) behaviorOverrides[i]!.output = override.output;
@@ -3734,6 +3893,8 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3734
3893
  usageBudget: data.usageBudget,
3735
3894
  toolBudget: data.toolBudget,
3736
3895
  configToolBudget: data.configToolBudget,
3896
+ callToolTimeoutMs: data.params?.toolTimeoutMs,
3897
+ configToolTimeoutMs: data.configToolTimeoutMs,
3737
3898
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
3738
3899
  runFanoutBudget: data.runFanoutBudget,
3739
3900
  }));
@@ -3854,6 +4015,8 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3854
4015
  worktreeSetup,
3855
4016
  timeoutMs: data.timeoutMs,
3856
4017
  deadlineAt,
4018
+ toolTimeoutMs: data.params?.toolTimeoutMs,
4019
+ configToolTimeoutMs: data.configToolTimeoutMs,
3857
4020
  turnBudget: data.turnBudget,
3858
4021
  usageBudget: data.usageBudget,
3859
4022
  toolBudgets,
@@ -4073,6 +4236,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
4073
4236
  task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(task) : task,
4074
4237
  goal: task,
4075
4238
  agentConfig,
4239
+ recoveryAgentConfig: data.recoveryAgents.find((agent) => agent.name === params.agent),
4076
4240
  ctx: asyncCtx,
4077
4241
  availableModels,
4078
4242
  cwd: effectiveCwd,
@@ -4097,6 +4261,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
4097
4261
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
4098
4262
  worktreeBaseDir: deps.config.worktreeBaseDir,
4099
4263
  controlConfig,
4264
+ intercomBridge: params.intercomBridge,
4100
4265
  controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
4101
4266
  childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
4102
4267
  nestedRoute: data.nestedRoute,
@@ -4246,6 +4411,8 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
4246
4411
  },
4247
4412
  timeoutMs: data.timeoutMs,
4248
4413
  deadlineAt,
4414
+ toolTimeoutMs: params.toolTimeoutMs,
4415
+ configToolTimeoutMs: data.configToolTimeoutMs,
4249
4416
  turnBudget: data.turnBudget,
4250
4417
  enforceHardTurnLimit: params.enforceHardTurnLimit,
4251
4418
  toolBudget: effectiveToolBudget.toolBudget,
@@ -4324,8 +4491,15 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
4324
4491
  }
4325
4492
 
4326
4493
  if (r.detached) {
4494
+ const statusRecovery = `subagent({ action: "status", id: "${runId}" }) to recover the result; do not resume or launch a replacement while it remains detached.`;
4495
+ const blockingRecovery = `subagent_wait({ id: "${runId}" }). Use ${statusRecovery}`;
4496
+ const message = r.detachedReason === "intercom coordination"
4497
+ ? `Detached for intercom coordination: ${params.agent}. Reply to the supervisor request first, then wait with ${blockingRecovery}`
4498
+ : r.detachedReason === "user request"
4499
+ ? `Detached at user request: ${params.agent}. The child continues independently. Register a completion wake-up with subagent_wait({ id: "${runId}", nonBlocking: true }), or use ${statusRecovery}`
4500
+ : `Detached before task completion: ${params.agent}. Wait with ${blockingRecovery}`;
4327
4501
  return {
4328
- content: [{ type: "text", text: `Detached for intercom coordination: ${params.agent}. Reply to the supervisor request first, then wait with subagent_wait({ id: "${runId}" }). Use subagent({ action: "status", id: "${runId}" }) to recover the result; do not resume or launch a replacement while it remains detached.` }],
4502
+ content: [{ type: "text", text: message }],
4329
4503
  details,
4330
4504
  };
4331
4505
  }
@@ -4369,17 +4543,28 @@ function duplicateSubagentCallResult(params: SubagentParamsLike): AgentToolResul
4369
4543
 
4370
4544
  const workflowLaunchObservers = new WeakMap<object, (launch: { agent: string; sessionFile?: string; async: boolean; runId?: string }) => void>();
4371
4545
 
4546
+ /**
4547
+ * Terminal-mission retention can remove a mission while its children still
4548
+ * report. Remember it to prevent later heartbeats from warning repeatedly.
4549
+ */
4550
+ const missionsMissingFromStore = new Set<string>();
4551
+
4372
4552
  function recordMissionWorkflowChild(
4373
4553
  binding: MissionLaunchBinding | undefined,
4374
4554
  workflowRunId: string,
4375
4555
  key: string,
4376
4556
  update: Omit<MissionWorkflowChildUpdate, "workflowRunId" | "key">,
4377
4557
  ): void {
4378
- if (!binding) return;
4558
+ if (!binding || missionsMissingFromStore.has(binding.missionId)) return;
4379
4559
  const { task: _task, ...durableUpdate } = update;
4380
4560
  try {
4381
4561
  updateMission(binding.location, binding.missionId, { upsertWorkflowChildren: [{ workflowRunId, key, ...durableUpdate }] });
4382
4562
  } catch (error) {
4563
+ if (error instanceof MissionNotFoundError) {
4564
+ missionsMissingFromStore.add(binding.missionId);
4565
+ console.warn(`[pi-subagents] Mission '${binding.missionId}' is no longer in the mission store; stopped recording its workflow children. Terminal-mission retention can prune a mission while its run is still active.`);
4566
+ return;
4567
+ }
4383
4568
  console.warn(`[pi-subagents] Failed to record mission workflow child '${key}': ${error instanceof Error ? error.message : String(error)}`);
4384
4569
  }
4385
4570
  }
@@ -4449,6 +4634,7 @@ function workflowChildResult(key: string, result: AgentToolResult<Details>): Wor
4449
4634
  ...(result.details.runId || result.details.asyncId ? { runId: result.details.runId ?? result.details.asyncId } : {}),
4450
4635
  output,
4451
4636
  ...(!ok ? { error: receiptOutput || output || "Child run failed." } : {}),
4637
+ ...(detached ? { detached: true } : {}),
4452
4638
  ...(structured.length === 1 ? { structuredOutput: structured[0] } : structured.length > 1 ? { structuredOutput: structured } : {}),
4453
4639
  artifactPaths: [...artifactPaths],
4454
4640
  results: result.details.results,
@@ -4469,16 +4655,19 @@ export function prepareWorkflowLaunchParams(
4469
4655
  const timeoutMs = childParams.timeoutMs ?? childParams.maxRuntimeMs ?? workflowDefaults.timeoutMs ?? workflowDefaults.maxRuntimeMs;
4470
4656
  const turnBudget = childParams.turnBudget ?? workflowDefaults.turnBudget;
4471
4657
  const toolBudget = childParams.toolBudget ?? workflowDefaults.toolBudget;
4658
+ const intercomBridge = childParams.intercomBridge ?? workflowDefaults.intercomBridge;
4472
4659
  return {
4473
4660
  action: "resume",
4474
4661
  id: childParams.resume.trim(),
4475
4662
  message: typeof childParams.task === "string" ? childParams.task.trim() : "",
4476
4663
  workflowParentRunId: parentWorkflowRunId,
4477
4664
  workflowKey,
4665
+ ...(options.runFanoutBudget ? { runFanoutBudget: { ...options.runFanoutBudget, parentPath: `${options.runFanoutBudget.parentPath ? `${options.runFanoutBudget.parentPath}/` : ""}workflow[${workflowKey}]` } } : {}),
4478
4666
  ...(options.missionDetached ? { mission: false } : {}),
4479
4667
  ...(timeoutMs !== undefined ? { timeoutMs: timeoutMs as number } : {}),
4480
4668
  ...(turnBudget !== undefined ? { turnBudget: turnBudget as TurnBudgetConfig } : {}),
4481
4669
  ...(toolBudget !== undefined ? { toolBudget: toolBudget as ToolBudgetConfig } : {}),
4670
+ ...(intercomBridge !== undefined ? { intercomBridge: intercomBridge as IntercomBridgeConfig } : {}),
4482
4671
  };
4483
4672
  }
4484
4673
  const launchParams = {
@@ -4665,6 +4854,17 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4665
4854
  const workflowUsageBudget = validateUsageBudgetConfig(requestParams.usageBudget ?? deps.config.usageBudget, requestParams.usageBudget ? "usageBudget" : "config.usageBudget");
4666
4855
  if (workflowUsageBudget.error) return buildRequestedModeError(requestParams, workflowUsageBudget.error);
4667
4856
  const workflowCwd = resolveRequestedCwd(parentCwd, requestParams.cwd);
4857
+ const workflowArtifactConfig: ArtifactConfig = omitUndefinedProperties({
4858
+ ...DEFAULT_ARTIFACT_CONFIG,
4859
+ enabled: requestParams.artifacts !== false,
4860
+ dir: deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir,
4861
+ });
4862
+ const workflowArtifactsDir = getArtifactsDir(ctx.sessionManager.getSessionFile() ?? null, workflowCwd, workflowArtifactConfig.dir);
4863
+ if (workflowArtifactConfig.dir === "project" && !warnedArtifactPackageDirs.has(workflowCwd)) {
4864
+ warnedArtifactPackageDirs.add(workflowCwd);
4865
+ const warning = getProjectArtifactPackagingWarning(workflowCwd);
4866
+ if (warning) console.warn(`[pi-subagents] ${warning}`);
4867
+ }
4668
4868
  const workflowPrompts = { render: (ref: string, vars?: unknown) => renderWorkflowPrompt(ref, vars, workflowCwd) };
4669
4869
  const chatProgressResult = resolveWorkflowChatProgress({ requested: requestParams.chatProgress, parentCwd, workflowCwd, background: requestParams.async !== false });
4670
4870
  if (chatProgressResult.error) return { content: [{ type: "text", text: chatProgressResult.error }], isError: true, details: { mode: "workflow", results: [] } };
@@ -4743,7 +4943,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4743
4943
  if (workflowRunId) {
4744
4944
  const toolCallId = _id;
4745
4945
  const asyncDir = path.join(DIRS.async, workflowRunId);
4746
- const resultPath = path.join(DIRS.results, `${workflowRunId}.json`);
4946
+ const resultPath = resultFilePath(DIRS.results, workflowRunId);
4747
4947
  const statusPath = path.join(asyncDir, "status.json");
4748
4948
  const eventsPath = path.join(asyncDir, "events.jsonl");
4749
4949
  const startedAt = Date.now();
@@ -4782,7 +4982,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4782
4982
  status.lastUpdate = Date.now();
4783
4983
  writeAtomicJson(statusPath, status);
4784
4984
  if (indexedState !== status.state) {
4785
- updateActiveRunIndex(asyncDir, status.state);
4985
+ updateActiveRunIndex(asyncDir, status.state, status.toolCallId);
4786
4986
  indexedState = status.state;
4787
4987
  }
4788
4988
  const job = deps.state.asyncJobs.get(workflowRunId);
@@ -4807,6 +5007,17 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4807
5007
  job.workflow = status.workflow;
4808
5008
  }
4809
5009
  };
5010
+ const writeWorkflowResult = (payload: Record<string, unknown>): boolean => {
5011
+ try {
5012
+ writeAsyncResultFile(resultPath, payload);
5013
+ return true;
5014
+ } catch (error) {
5015
+ const message = `Failed to write async workflow result ${resultPath}: ${error instanceof Error ? error.message : String(error)}`;
5016
+ console.error(message, error);
5017
+ appendWorkflowEvent({ type: "subagent.workflow.result_write_failed", error: message });
5018
+ return false;
5019
+ }
5020
+ };
4810
5021
  const projectWorkflowActivity = () => {
4811
5022
  const steps = status.steps ?? [];
4812
5023
  const runningSteps = steps.filter((step) => step.status === "running");
@@ -4828,7 +5039,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4828
5039
  status.toolCount = toolCounts.length > 0 ? toolCounts.reduce((total, count) => total + count, 0) : undefined;
4829
5040
  status.currentStep = runningSteps.length === 1 ? steps.indexOf(runningSteps[0]!) : undefined;
4830
5041
  };
4831
- const workflowJob: AsyncJobState = { asyncId: workflowRunId, asyncDir, cwd: workflowCwd, status: "running", sessionId: currentSessionId ?? undefined, mode: "workflow", agents: [], steps: [], startedAt, updatedAt: startedAt, ...(timeout !== undefined ? { timeoutMs: timeout, deadlineAt: startedAt + timeout } : {}), workflow: status.workflow };
5042
+ const workflowJob: AsyncJobState = { asyncId: workflowRunId, asyncDir, toolCallId, cwd: workflowCwd, status: "running", sessionId: currentSessionId ?? undefined, mode: "workflow", agents: [], steps: [], startedAt, updatedAt: startedAt, ...(timeout !== undefined ? { timeoutMs: timeout, deadlineAt: startedAt + timeout } : {}), workflow: status.workflow };
4832
5043
  deps.state.asyncJobs.set(workflowRunId, workflowJob);
4833
5044
  deps.state.fleetJobs ??= new Map();
4834
5045
  deps.state.fleetJobs.set(workflowRunId, workflowJob);
@@ -4838,6 +5049,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4838
5049
  void Promise.resolve().then(async () => {
4839
5050
  const workflowResults: SingleResult[] = [];
4840
5051
  const { action: _action, agent: _agent, task: _task, resume: _resume, tasks: _tasks, chain: _chain, concurrency: _concurrency, foregroundOnly: _foregroundOnly, clarify: _clarify, timeoutMs: _timeoutMs, maxRuntimeMs: _maxRuntimeMs, usageBudget: _usageBudget, missionId: _missionId, mission: _mission, ...workflowChildDefaults } = workflowRequest;
5052
+ const workflowOutput = typeof workflowChildDefaults.output === "string" || typeof workflowChildDefaults.output === "boolean" ? workflowChildDefaults.output : undefined;
5053
+ const configuredOutputBaseDir = resolveConfiguredSingleRunOutputBaseDir(deps);
5054
+ const workflowAggregateOutputPath = resolveWorkflowAggregateOutputPath(workflowOutput, ctx.cwd, workflowCwd, resolveSingleRunOutputBaseDir(deps, workflowArtifactsDir, workflowRunId), configuredOutputBaseDir);
5055
+ const claimedOutputPaths = new Map<string, string>();
4841
5056
  const workflowSteps = new Map<string, NonNullable<AsyncStatus["steps"]>[number]>();
4842
5057
  let projectedTraceLength = 0;
4843
5058
  let projectedTraceTail: NonNullable<Details["workflow"]>["trace"][number] | undefined;
@@ -4857,12 +5072,24 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4857
5072
  if (entry.operation !== "run") continue;
4858
5073
  const existing = workflowSteps.get(entry.key);
4859
5074
  if (entry.state === "reused" && existing) continue;
4860
- const mapped = entry.state === "started" || entry.state === "reused" ? "running" : entry.state === "completed" ? "completed" : "failed";
5075
+ const mapped = entry.state === "started" || entry.state === "reused"
5076
+ ? "running"
5077
+ : entry.state === "completed"
5078
+ ? "completed"
5079
+ : entry.state === "stopped"
5080
+ ? "stopped"
5081
+ : entry.state === "detached"
5082
+ ? "paused"
5083
+ : "failed";
4861
5084
  if (existing) {
4862
5085
  existing.status = mapped;
4863
5086
  if (entry.agent) existing.agent = entry.agent;
4864
5087
  if (entry.runId) existing.runId = entry.runId;
4865
5088
  if (entry.state === "failed" && !entry.runId && existing.async === undefined) existing.async = false;
5089
+ if (entry.state === "detached") existing.activityState = "needs_attention";
5090
+ else if (existing.status !== "running") delete existing.activityState;
5091
+ if (entry.state === "stopped") existing.stopped = true;
5092
+ else delete existing.stopped;
4866
5093
  if (entry.error === undefined) delete existing.error;
4867
5094
  else existing.error = entry.error;
4868
5095
  if (entry.durationMs === undefined) delete existing.durationMs;
@@ -4877,6 +5104,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4877
5104
  startedAt: Date.now(),
4878
5105
  ...(entry.runId ? { runId: entry.runId } : {}),
4879
5106
  ...(entry.state === "failed" && !entry.runId ? { async: false } : {}),
5107
+ ...(entry.state === "detached" ? { activityState: "needs_attention" as const } : {}),
5108
+ ...(entry.state === "stopped" ? { stopped: true } : {}),
4880
5109
  };
4881
5110
  status.steps?.push(step);
4882
5111
  workflowSteps.set(entry.key, step);
@@ -4897,7 +5126,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4897
5126
  ...(workflowState ? { state: workflowState } : {}),
4898
5127
  onTrace: updateTrace,
4899
5128
  admit: (calls) => {
5129
+ const outputClaims = workflowChildOutputClaims({ ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, workflowRunId, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: deps.discoverAgents, workflowAgentScope: workflowChildDefaults.agentScope, claimedOutputPaths, entries: calls });
5130
+ if (outputClaims.error) throw new Error(outputClaims.error);
4900
5131
  status.runFanoutBudget = claimRunFanoutBatch(workflowFanoutBudget, calls.map(({ key }) => `workflow[${key}]`));
5132
+ if (outputClaims.claims) applyWorkflowChildOutputClaims(claimedOutputPaths, outputClaims.claims);
4901
5133
  persist();
4902
5134
  },
4903
5135
  onEmit: (emits) => {
@@ -4921,7 +5153,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4921
5153
  });
4922
5154
  const result = await runMissionWorkflowChild(missionBinding, workflowRunId, key, childPhase, () => {
4923
5155
  const childRequest = bindMissionWorkflowChildAsyncLaunch(
4924
- { ...prepareWorkflowLaunchParams(workflowChildDefaults, childParams, workflowRunId, key, { missionDetached: detachWorkflowChildMissions, runFanoutBudget: workflowFanoutBudget }), runFanoutAdmitted: admission.admitted },
5156
+ { ...prepareWorkflowChildLaunchParams({ workflowDefaults: workflowChildDefaults, childParams, parentWorkflowRunId: workflowRunId, workflowKey: key, ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: deps.discoverAgents, workflowAgentScope: workflowChildDefaults.agentScope, options: { missionDetached: detachWorkflowChildMissions, runFanoutBudget: workflowFanoutBudget } }), runFanoutAdmitted: admission.admitted },
4925
5157
  missionBinding,
4926
5158
  deps.asyncByDefault,
4927
5159
  );
@@ -4939,7 +5171,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4939
5171
  return execute(randomUUID(), childRequest, workflowSignal, (update) => {
4940
5172
  const progress = update.details.progress?.[0];
4941
5173
  const step = status.steps?.find((candidate) => candidate.workflowKey === key);
4942
- if (!progress || !step) return;
5174
+ if (!progress || !step || step.stopped) return;
4943
5175
  step.status = progress.status === "completed" ? "completed" : progress.status === "failed" ? "failed" : "running";
4944
5176
  step.activityState = progress.activityState;
4945
5177
  step.lastActivityAt = progress.lastActivityAt;
@@ -4991,18 +5223,32 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4991
5223
  const returnPreview = formatWorkflowValue(workflow.value).slice(0, 1_000);
4992
5224
  const emitPreview = workflow.emits.length > 0 ? ` Emitted: ${workflow.emits.map(formatWorkflowValue).join(", ").slice(0, 1_000)}` : "";
4993
5225
  const summary = `Workflow completed with ${workflow.children.length} child run(s). Return: ${returnPreview}${emitPreview} Trace: ${workflow.trace.length} event(s).`;
5226
+ const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, summary);
5227
+ const resultSummary = appendWorkflowOutputWarning(summary, outputWarning);
4994
5228
  const workflowUsage = sumResultsUsage(workflowResults);
4995
5229
  status = { ...status, state: "complete", endedAt: Date.now(), workflow: { value: workflow.value, trace: workflow.trace, emits: workflow.emits, console: workflow.console }, totalTokens: { input: workflowUsage.input, output: workflowUsage.output, total: workflowUsage.input + workflowUsage.output }, totalCost: sumResultsCost(workflowResults) };
4996
- writeAtomicJson(resultPath, { id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: true, state: "complete", summary, output: summary, results: workflow.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: workflowCwd, sessionId: currentSessionId, timestamp: Date.now(), durationMs: Date.now() - startedAt });
5230
+ if (!writeWorkflowResult({ id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: true, state: "complete", summary: resultSummary, output: resultSummary, results: workflow.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: workflowCwd, sessionId: currentSessionId, timestamp: Date.now(), durationMs: Date.now() - startedAt })) return;
4997
5231
  persist();
4998
- appendWorkflowEvent({ type: "subagent.workflow.completed", state: "complete" });
5232
+ appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, ...(status.error ? { error: status.error } : {}) });
4999
5233
  } catch (error) {
5000
5234
  const partial = error instanceof WorkflowScriptError ? error.partial : { trace: [], emits: [], console: [], children: [] };
5001
5235
  const stopped = controller.signal.aborted;
5002
- status = compactOptional<AsyncStatus>({ ...status, state: stopped ? "stopped" : "failed", stopped: stopped || undefined, error: error instanceof Error ? error.message : String(error), endedAt: Date.now(), workflow: { trace: partial.trace, emits: partial.emits, console: partial.console } });
5003
- writeAtomicJson(resultPath, { id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: false, state: status.state, summary: status.error, error: status.error, stopped: status.stopped, results: partial.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: workflowCwd, sessionId: currentSessionId, timestamp: Date.now(), durationMs: Date.now() - startedAt });
5236
+ const detachedChildKeys = new Set(partial.children.filter((child) => child.detached).map((child) => child.key));
5237
+ const hasRealFailedChild = partial.children.some((child) => !child.ok && !child.detached);
5238
+ const pauseForDetached = !stopped && error instanceof WorkflowScriptError && error.errorKind === "detached-child" && detachedChildKeys.size > 0 && !hasRealFailedChild;
5239
+ const state = stopped ? "stopped" : pauseForDetached ? "paused" : "failed";
5240
+ for (const step of status.steps ?? []) {
5241
+ if (step.workflowKey && detachedChildKeys.has(step.workflowKey)) {
5242
+ step.status = "paused";
5243
+ step.activityState = "needs_attention";
5244
+ }
5245
+ }
5246
+ status = compactOptional<AsyncStatus>({ ...status, state, stopped: stopped || undefined, activityState: pauseForDetached ? "needs_attention" : undefined, error: error instanceof Error ? error.message : String(error), endedAt: Date.now(), workflow: { trace: partial.trace, emits: partial.emits, console: partial.console } });
5247
+ const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, status.error ?? (pauseForDetached ? "Workflow paused." : "Workflow failed."));
5248
+ const resultSummary = appendWorkflowOutputWarning(status.error ?? (pauseForDetached ? "Workflow paused." : "Workflow failed."), outputWarning);
5249
+ if (!writeWorkflowResult({ id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: false, state: status.state, summary: resultSummary, error: status.error, stopped: status.stopped, activityState: status.activityState, results: partial.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.detached ? { detached: true } : {}), ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: workflowCwd, sessionId: currentSessionId, timestamp: Date.now(), durationMs: Date.now() - startedAt })) return;
5004
5250
  persist();
5005
- appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, error: status.error });
5251
+ appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, error: status.error, ...(status.activityState ? { activityState: status.activityState } : {}) });
5006
5252
  } finally {
5007
5253
  deps.state.workflowControllers?.delete(workflowRunId);
5008
5254
  deps.state.activeAsyncCapacity = workflowCapacity?.reconcile(new Set(deps.state.workflowControllers?.keys() ?? []))
@@ -5015,6 +5261,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5015
5261
  }, workflowFanoutBudget));
5016
5262
  }
5017
5263
  const { workflowScript: _workflowScript, action: _action, agent: _agent, task: _task, resume: _resume, tasks: _tasks, chain: _chain, concurrency: _concurrency, async: _async, foregroundOnly: _foregroundOnly, clarify: _clarify, timeoutMs: _timeoutMs, maxRuntimeMs: _maxRuntimeMs, usageBudget: _usageBudget, chatProgress: _chatProgress, missionId: _missionId, mission: _mission, ...workflowChildDefaults } = requestParams;
5264
+ const workflowOutput = typeof workflowChildDefaults.output === "string" || typeof workflowChildDefaults.output === "boolean" ? workflowChildDefaults.output : undefined;
5265
+ const configuredOutputBaseDir = resolveConfiguredSingleRunOutputBaseDir(deps);
5266
+ const workflowAggregateOutputPath = resolveWorkflowAggregateOutputPath(workflowOutput, ctx.cwd, workflowCwd, resolveSingleRunOutputBaseDir(deps, workflowArtifactsDir, _id), configuredOutputBaseDir);
5267
+ const claimedOutputPaths = new Map<string, string>();
5018
5268
  const workflowResults: SingleResult[] = [];
5019
5269
  let liveWorkflow: NonNullable<Details["workflow"]> = { trace: [], emits: [], console: [] };
5020
5270
  const sendWorkflowProgress = () => {
@@ -5033,7 +5283,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5033
5283
  sendWorkflowProgress();
5034
5284
  },
5035
5285
  admit: (calls) => {
5286
+ const outputClaims = workflowChildOutputClaims({ ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, workflowRunId: _id, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: deps.discoverAgents, workflowAgentScope: workflowChildDefaults.agentScope, claimedOutputPaths, entries: calls });
5287
+ if (outputClaims.error) throw new Error(outputClaims.error);
5036
5288
  claimRunFanoutBatch(workflowFanoutBudget, calls.map(({ key }) => `workflow[${key}]`));
5289
+ if (outputClaims.claims) applyWorkflowChildOutputClaims(claimedOutputPaths, outputClaims.claims);
5037
5290
  },
5038
5291
  onEmit: (emits) => {
5039
5292
  liveWorkflow = { ...liveWorkflow, emits };
@@ -5054,7 +5307,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5054
5307
  });
5055
5308
  const result = await runMissionWorkflowChild(missionBinding, _id, key, childPhase, () => {
5056
5309
  const childRequest = bindMissionWorkflowChildAsyncLaunch(
5057
- { ...prepareWorkflowLaunchParams(workflowChildDefaults, childParams, _id, key, { missionDetached: detachWorkflowChildMissions, suppressRoutineResultIntercom: chatProgress.mode === "live-card", runFanoutBudget: workflowFanoutBudget }), runFanoutAdmitted: admission.admitted },
5310
+ { ...prepareWorkflowChildLaunchParams({ workflowDefaults: workflowChildDefaults, childParams, parentWorkflowRunId: _id, workflowKey: key, ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: deps.discoverAgents, workflowAgentScope: workflowChildDefaults.agentScope, options: { missionDetached: detachWorkflowChildMissions, suppressRoutineResultIntercom: chatProgress.mode === "live-card", runFanoutBudget: workflowFanoutBudget } }), runFanoutAdmitted: admission.admitted },
5058
5311
  missionBinding,
5059
5312
  deps.asyncByDefault,
5060
5313
  );
@@ -5095,8 +5348,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5095
5348
  if (workflow.emits.length > 0) sections.push(`Emitted:\n${workflow.emits.map(formatWorkflowValue).join("\n")}`);
5096
5349
  if (workflow.console.length > 0) sections.push(`Console:\n${workflow.console.map((entry) => `[${entry.level}] ${entry.text}`).join("\n")}`);
5097
5350
  if (traceLines.length > 0) sections.push(`Call trace:\n${traceLines.join("\n")}`);
5351
+ const workflowText = sections.join("\n\n");
5352
+ const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, workflowText);
5353
+ const displayText = appendWorkflowOutputWarning(workflowText, outputWarning);
5098
5354
  return attachWorkflowMission(withRunFanoutBudget({
5099
- content: [{ type: "text", text: sections.join("\n\n") }],
5355
+ content: [{ type: "text", text: displayText }],
5100
5356
  details: compactOptional<Details>({ mode: "workflow", runId: _id, results: workflow.children.flatMap((child) => (child.results ?? []) as SingleResult[]), totalChildUsage: sumResultsUsage(workflowResults), totalCost: sumResultsCost(workflowResults), usageBudget: usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults)), workflow: { value: workflow.value, trace: workflow.trace, emits: workflow.emits, console: workflow.console }, chatProgress }),
5101
5357
  }, workflowFanoutBudget));
5102
5358
  } catch (error) {
@@ -5107,8 +5363,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5107
5363
  if (partial.emits.length > 0) sections.push(`Emitted:\n${partial.emits.map(formatWorkflowValue).join("\n")}`);
5108
5364
  if (partial.console.length > 0) sections.push(`Console:\n${partial.console.map((entry) => `[${entry.level}] ${entry.text}`).join("\n")}`);
5109
5365
  if (traceLines.length > 0) sections.push(`Call trace:\n${traceLines.join("\n")}`);
5366
+ const workflowText = sections.join("\n\n");
5367
+ const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, workflowText);
5368
+ const displayText = appendWorkflowOutputWarning(workflowText, outputWarning);
5110
5369
  return attachWorkflowMission(withRunFanoutBudget({
5111
- content: [{ type: "text", text: sections.join("\n\n") }],
5370
+ content: [{ type: "text", text: displayText }],
5112
5371
  isError: true,
5113
5372
  details: compactOptional<Details>({ mode: "workflow", runId: _id, results: partial.children.flatMap((child) => (child.results ?? []) as SingleResult[]), totalChildUsage: sumResultsUsage(workflowResults), totalCost: sumResultsCost(workflowResults), usageBudget: usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults)), workflow: { trace: partial.trace, emits: partial.emits, console: partial.console }, chatProgress }),
5114
5373
  }, workflowFanoutBudget));
@@ -5372,11 +5631,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5372
5631
  details: { mode: "management", results: [], spawnBudget, activeAsyncCapacity },
5373
5632
  };
5374
5633
  }
5375
- if (action === "status") {
5634
+ if (action === "status" || action === "debug.run") {
5376
5635
  if (!preserveActiveSession) deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
5377
5636
  const targetRunId = paramsWithResolvedCwd.id ?? paramsWithResolvedCwd.runId;
5378
5637
  const hasDirectoryTarget = Boolean(paramsWithResolvedCwd.dir);
5379
- const targetLabel = formatStatusTargetLabel(paramsWithResolvedCwd, targetRunId);
5638
+ const targetLabel = action === "debug.run" ? "Debug run" : formatStatusTargetLabel(paramsWithResolvedCwd, targetRunId);
5380
5639
  const withBudget = (result: AgentToolResult<Details>) => {
5381
5640
  const budgeted = withSpawnBudgetStatus(result, deps.state, deps.config, deps.state.currentSessionId);
5382
5641
  return {
@@ -5388,6 +5647,15 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5388
5647
  };
5389
5648
  const nestedScope = nestedResolutionScopeForExecutor(deps);
5390
5649
  const sessionRoots = trustedSessionRootsForStatus(ctx, deps);
5650
+ if (action === "debug.run") {
5651
+ if (!targetRunId && !hasDirectoryTarget) {
5652
+ return withBudget({ content: [{ type: "text", text: "action='debug.run' requires id, runId, or dir." }], isError: true, details: { mode: "management", results: [] } });
5653
+ }
5654
+ if (paramsWithResolvedCwd.view) {
5655
+ return withBudget({ content: [{ type: "text", text: "action='debug.run' does not support status views." }], isError: true, details: { mode: "management", results: [] } });
5656
+ }
5657
+ return withBudget(inspectSubagentStatus(paramsWithResolvedCwd, omitUndefinedProperties({ state: deps.state, nested: nestedScope, sessionRoots })));
5658
+ }
5391
5659
  if (paramsWithResolvedCwd.view === "fleet") {
5392
5660
  return withBudget(inspectSubagentStatus(paramsWithResolvedCwd, omitUndefinedProperties({ state: deps.state, nested: nestedScope, sessionRoots })));
5393
5661
  }
@@ -5777,6 +6045,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5777
6045
  const sessionName = resolveIntercomSessionTarget(deps.pi.getSessionName(), ctx.sessionManager.getSessionId());
5778
6046
  const intercomBridge = resolveIntercomBridge({
5779
6047
  config: deps.config.intercomBridge,
6048
+ override: effectiveParams.intercomBridge,
5780
6049
  context: effectiveParams.context ?? (contextPolicy.usesFork ? "fork" : undefined),
5781
6050
  orchestratorTarget: sessionName,
5782
6051
  });
@@ -5825,6 +6094,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5825
6094
  const parentModel = requestParentModel;
5826
6095
  prepareForkThinking = (agentName, index, modelOverride) => {
5827
6096
  const agentConfig = agents.find((agent) => agent.name === agentName);
6097
+ if (agentConfig?.runner?.type === "external-cli") {
6098
+ forkThinkingRequirements.set(index, true);
6099
+ return;
6100
+ }
5828
6101
  const primaryModel = resolveEffectiveSubagentModel(
5829
6102
  modelOverride,
5830
6103
  agentConfig?.model,
@@ -6039,6 +6312,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6039
6312
  signal,
6040
6313
  onUpdate: onUpdateWithContext,
6041
6314
  agents,
6315
+ recoveryAgents: discoveredAgents,
6042
6316
  runId,
6043
6317
  shareEnabled,
6044
6318
  sessionRoot,
@@ -6060,6 +6334,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6060
6334
  usageBudget: usageBudget.budget,
6061
6335
  allowZeroToolBudget,
6062
6336
  configToolBudget: configToolBudget.toolBudget,
6337
+ configToolTimeoutMs: deps.config.toolTimeoutMs,
6063
6338
  contextPolicy,
6064
6339
  modelScope,
6065
6340
  parentModel: requestParentModel,
@@ -6229,7 +6504,12 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6229
6504
  };
6230
6505
 
6231
6506
  let nestedForegroundStarted = false;
6507
+ let asyncLaunchFailed = false;
6232
6508
  try {
6509
+ if (effectiveAsync) {
6510
+ deps.state.liveAsyncSessionRoots ??= new Map();
6511
+ deps.state.liveAsyncSessionRoots.set(asyncRunId, sessionRoot);
6512
+ }
6233
6513
  if (workflowLaunchObserver) {
6234
6514
  const singleTask = hasTasks && effectiveParams.tasks?.length === 1 ? effectiveParams.tasks[0] : undefined;
6235
6515
  const launch = hasSingle
@@ -6243,7 +6523,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6243
6523
  }
6244
6524
  }
6245
6525
  const asyncResult = runAsyncPath(execData, deps);
6246
- if (asyncResult) return attachMission(withRunFanoutBudget(withResolvedContext(withForkThinkingNotes(asyncResult, forkThinkingDowngrades), contextPolicy.contextSummary), runFanoutBudget));
6526
+ if (asyncResult) {
6527
+ asyncLaunchFailed = asyncResult.isError === true;
6528
+ return attachMission(withRunFanoutBudget(withResolvedContext(withForkThinkingNotes(asyncResult, forkThinkingDowngrades), contextPolicy.contextSummary), runFanoutBudget));
6529
+ }
6247
6530
  if (foregroundControl) {
6248
6531
  writeNestedForegroundEvent("subagent.nested.started");
6249
6532
  nestedForegroundStarted = true;
@@ -6265,10 +6548,12 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6265
6548
  return attachMission(withRunFanoutBudget(withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary), runFanoutBudget, { annotateContent: runFanoutAnnotateContent }));
6266
6549
  }
6267
6550
  } catch (error) {
6551
+ asyncLaunchFailed = effectiveAsync;
6268
6552
  const errorResult = withForkThinkingNotes(toExecutionErrorResult(effectiveParams, error, contextPolicy.contextSummary), forkThinkingDowngrades);
6269
6553
  if (nestedForegroundStarted) writeNestedForegroundEvent("subagent.nested.completed", errorResult);
6270
6554
  return attachMission(errorResult);
6271
6555
  } finally {
6556
+ if (effectiveAsync && (asyncLaunchFailed || (activeAsyncCapacity && !activeAsyncCapacity.owner.runnerStartedAt))) deps.state.liveAsyncSessionRoots?.delete(asyncRunId);
6272
6557
  if (activeAsyncCapacity && !activeAsyncCapacity.owner.runnerStartedAt) activeAsyncCapacity.rollback();
6273
6558
  if (foregroundControl) {
6274
6559
  settleForegroundSchedulingOwner(foregroundControl);