pi-subagents 0.47.1 → 0.49.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 (66) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +2 -0
  3. package/docs/agents.md +1 -0
  4. package/docs/configuration.md +74 -7
  5. package/docs/missions.md +4 -2
  6. package/docs/observability.md +27 -3
  7. package/docs/tool-reference.md +1 -1
  8. package/package.json +1 -1
  9. package/src/agents/agents.ts +36 -15
  10. package/src/api/preflight.ts +1 -1
  11. package/src/extension/config.ts +26 -0
  12. package/src/extension/doctor.ts +40 -0
  13. package/src/extension/index.ts +39 -8
  14. package/src/extension/public-execution.ts +35 -4
  15. package/src/extension/rpc.ts +3 -9
  16. package/src/extension/schemas.ts +10 -9
  17. package/src/extension/tool-description.ts +10 -8
  18. package/src/inspectors/herdr/actions.ts +11 -2
  19. package/src/inspectors/herdr/inspector-runner.ts +16 -3
  20. package/src/intercom/intercom-bridge.ts +7 -2
  21. package/src/missions/lifecycle.ts +4 -7
  22. package/src/missions/store.ts +12 -7
  23. package/src/missions/workflow-state.ts +6 -2
  24. package/src/runs/background/active-async-capacity.ts +431 -0
  25. package/src/runs/background/active-run-index.ts +9 -5
  26. package/src/runs/background/async-execution.ts +141 -65
  27. package/src/runs/background/async-job-tracker.ts +4 -0
  28. package/src/runs/background/async-resume.ts +18 -2
  29. package/src/runs/background/async-status.ts +11 -5
  30. package/src/runs/background/chain-append.ts +33 -15
  31. package/src/runs/background/fleet-view.ts +18 -5
  32. package/src/runs/background/owned-process-tree.ts +104 -0
  33. package/src/runs/background/process-terminal.ts +17 -3
  34. package/src/runs/background/resume-guidance.ts +27 -7
  35. package/src/runs/background/retained-children.ts +14 -6
  36. package/src/runs/background/run-status.ts +101 -5
  37. package/src/runs/background/stale-run-reconciler.ts +3 -3
  38. package/src/runs/background/subagent-runner.ts +60 -32
  39. package/src/runs/foreground/chain-execution.ts +37 -2
  40. package/src/runs/foreground/execution.ts +90 -19
  41. package/src/runs/foreground/foreground-control.ts +12 -0
  42. package/src/runs/foreground/prompt-audit.ts +172 -0
  43. package/src/runs/foreground/subagent-executor.ts +829 -203
  44. package/src/runs/shared/acceptance.ts +13 -4
  45. package/src/runs/shared/completion-guard.ts +17 -1
  46. package/src/runs/shared/llm-intent-arbiter.ts +302 -0
  47. package/src/runs/shared/parallel-utils.ts +2 -0
  48. package/src/runs/shared/pi-args.ts +44 -1
  49. package/src/runs/shared/run-fanout-budget.ts +280 -0
  50. package/src/runs/shared/single-output.ts +4 -2
  51. package/src/runs/shared/task-intent.ts +19 -3
  52. package/src/runs/shared/worktree.ts +17 -5
  53. package/src/shared/agent-stream-options.ts +5 -0
  54. package/src/shared/artifacts.ts +2 -6
  55. package/src/shared/display-text.ts +50 -0
  56. package/src/shared/node-executable.ts +21 -0
  57. package/src/shared/types.ts +115 -4
  58. package/src/shared/utils.ts +3 -1
  59. package/src/tui/fleet-status.ts +7 -5
  60. package/src/tui/fleet-transcript.ts +1 -48
  61. package/src/tui/fleet.ts +228 -13
  62. package/src/tui/render.ts +86 -39
  63. package/src/watchdog/permission-arbiter.ts +2 -1
  64. package/src/watchdog/review.ts +4 -3
  65. package/src/workflows/chat-progress.ts +8 -2
  66. package/src/workflows/scripted-workflow.ts +66 -10
@@ -17,6 +17,7 @@ import {
17
17
  settleForegroundSchedulingOwner,
18
18
  updateForegroundChild,
19
19
  } from "./foreground-control.ts";
20
+ import { getLivePromptAudit, rewritePromptWithGuidance, updateLiveEffectivePrompt } from "./prompt-audit.ts";
20
21
  import { persistForegroundRunHistory, MAX_REMEMBERED_FOREGROUND_RUNS } from "./foreground-history.ts";
21
22
  import { resolveExecutionAgentScope } from "../../agents/agent-scope.ts";
22
23
  import { handleManagementAction } from "../../agents/agent-management.ts";
@@ -54,6 +55,7 @@ import {
54
55
  import { discoverAvailableSkills, normalizeSkillInput } from "../../agents/skills.ts";
55
56
  import { buildAsyncRunnerSteps, DEFAULT_ASYNC_TIMEOUT_MS, executeAsyncChain, executeAsyncSingle, formatAsyncStartedMessage, isAsyncAvailable, workflowAwaitedAsyncResultPath } from "../background/async-execution.ts";
56
57
  import { updateActiveRunIndex } from "../background/active-run-index.ts";
58
+ import { acquireActiveAsyncCapacity, ActiveAsyncCapacityError, getActiveAsyncCapacitySnapshot, resolveMaxActiveAsyncRunsPerSession, transferActiveAsyncCapacity, type ActiveAsyncCapacityHandle } from "../background/active-async-capacity.ts";
57
59
  import { isScheduledRunAction, type ScheduledRunAction } from "../background/scheduled-runs.ts";
58
60
  import { enqueueChainAppendRequest, readPendingChainAppendRequests, runnerStepOutputNames } from "../background/chain-append.ts";
59
61
  import { ChainOutputValidationError, validateChainOutputBindingsWithContext } from "../shared/chain-outputs.ts";
@@ -64,6 +66,7 @@ import { applyIntercomBridgeToAgent, INTERCOM_BRIDGE_MARKER, resolveIntercomBrid
64
66
  import { formatControlIntercomMessage, formatControlNoticeMessage, resolveControlConfig, shouldNotifyControlEvent } from "../shared/subagent-control.ts";
65
67
  import { resolveTurnBudgetConfig } from "../shared/turn-budget.ts";
66
68
  import { formatSpawnBudget, getSpawnBudgetSnapshot, grantSpawnBudget, preflightSpawnBudget, preflightSpawnBudgetGrant, reserveSpawnBudget } from "../shared/spawn-budget.ts";
69
+ import { claimRunFanoutBatch, claimRunFanoutBatchWithCommit, createRunFanoutBudget, decodeRunFanoutBudgetDescriptor, formatRunFanoutBudget, getRunFanoutBudgetSnapshot, readRunFanoutBudgetDescriptor, RunFanoutLimitError, RUN_FANOUT_BUDGET_ENV, writeRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
67
70
  import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
68
71
  import { usageBudgetExceededMessage, usageBudgetState, validateUsageBudgetConfig } from "../shared/usage-budget.ts";
69
72
  import { intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
@@ -71,6 +74,7 @@ import { isAgentContractV1 } from "../shared/agent-contract.ts";
71
74
  import { finalizeSingleOutput, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
72
75
  import { cleanupStructuredOutputRuntime, createStructuredOutputRuntime } from "../shared/structured-output.ts";
73
76
  import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, readStatus, resolveChildCwd, sumResultsCost, sumResultsUsage } from "../../shared/utils.ts";
77
+ import { createTaskMutationArbiter } from "../shared/llm-intent-arbiter.ts";
74
78
  import { DEFAULT_GLOBAL_CONCURRENCY_LIMIT, Semaphore } from "../shared/parallel-utils.ts";
75
79
  import { discardPreservedWorktrees, formatParallelHandoffError, formatParallelHandoffReference, parallelHandoffPath, writeParallelHandoffGroup, writePendingParallelHandoff } from "../shared/parallel-handoff.ts";
76
80
  import { summarizeContextModes, type ContextMode, type ContextSummary } from "../shared/context-mode.ts";
@@ -135,6 +139,7 @@ import {
135
139
  type Details,
136
140
  type ExtensionConfig,
137
141
  type ForegroundRunControl,
142
+ type IntercomBridgeConfig,
138
143
  type IntercomEventBus,
139
144
  type JsonSchemaObject,
140
145
  type MaxOutputConfig,
@@ -143,6 +148,7 @@ import {
143
148
  type ResolvedControlConfig,
144
149
  type ResolvedTurnBudget,
145
150
  type ResolvedToolBudget,
151
+ type RunFanoutBudgetDescriptor,
146
152
  type SingleResult,
147
153
  type ToolBudgetConfig,
148
154
  type TurnBudgetConfig,
@@ -161,6 +167,7 @@ import {
161
167
  resolveTopLevelParallelMaxTasks,
162
168
  resolveChildMaxSubagentDepth,
163
169
  resolveCurrentMaxSubagentDepth,
170
+ resolveMaxSubagentSpawnsPerRun,
164
171
  wrapForkTask,
165
172
  } from "../../shared/types.ts";
166
173
 
@@ -284,12 +291,18 @@ export interface SubagentParamsLike {
284
291
  workflowKey?: string;
285
292
  workflowChildAsyncId?: string;
286
293
  suppressRoutineResultIntercom?: boolean;
294
+ /** Internal inherited cumulative run-tree budget. */
295
+ runFanoutBudget?: RunFanoutBudgetDescriptor;
296
+ /** Internal workflow host admission proof. */
297
+ runFanoutAdmitted?: boolean;
287
298
  /** Internal durable-run compatibility fields. Public callers must use workflowScript. */
288
299
  chain?: ChainStep[];
289
300
  tasks?: TaskParam[];
290
301
  concurrency?: number;
291
302
  worktree?: boolean;
292
303
  context?: "fresh" | "fork";
304
+ /** Per-run intercom bridge config. It replaces the global config for this launch only. */
305
+ intercomBridge?: IntercomBridgeConfig;
293
306
  async?: boolean;
294
307
  foregroundOnly?: boolean;
295
308
  timeoutMs?: number;
@@ -377,6 +390,8 @@ interface ExecutionContextData {
377
390
  signal: AbortSignal;
378
391
  onUpdate?: (r: AgentToolResult<Details>) => void;
379
392
  agents: AgentConfig[];
393
+ /** Discovered agent definitions before per-run bridge injection. */
394
+ recoveryAgents: AgentConfig[];
380
395
  runId: string;
381
396
  shareEnabled: boolean;
382
397
  sessionRoot: string;
@@ -388,6 +403,7 @@ interface ExecutionContextData {
388
403
  artifactsDir: string;
389
404
  backgroundRequestedWhileClarifying: boolean;
390
405
  effectiveAsync: boolean;
406
+ asyncRunId: string;
391
407
  controlConfig: ResolvedControlConfig;
392
408
  intercomBridge: IntercomBridgeState;
393
409
  nestedRoute?: NestedRouteInfo;
@@ -404,6 +420,9 @@ interface ExecutionContextData {
404
420
  parentSessionId: string | null;
405
421
  parentPiSessionId?: string;
406
422
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
423
+ runFanoutBudget: RunFanoutBudgetDescriptor;
424
+ topLevelAsyncCapacityEligible: boolean;
425
+ activeAsyncCapacity?: ActiveAsyncCapacityHandle;
407
426
  }
408
427
 
409
428
  function resolveRequestedCwd(runtimeCwd: string, requestedCwd: string | undefined): string {
@@ -432,6 +451,17 @@ function getForegroundControl(state: SubagentState, runId: string | undefined) {
432
451
  return newest;
433
452
  }
434
453
 
454
+ function promptAuditRedoParams(value: unknown, rewrittenTask: string): SubagentParamsLike {
455
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Prompt redo is missing a safe live launch contract.");
456
+ const params = { ...(value as SubagentParamsLike), task: rewrittenTask, async: false };
457
+ delete params.workflowParentRunId;
458
+ delete params.workflowKey;
459
+ delete params.workflowChildAsyncId;
460
+ delete params.suppressRoutineResultIntercom;
461
+ if (params.worktree === true && Array.isArray(params.tasks)) delete params.cwd;
462
+ return params;
463
+ }
464
+
435
465
  function formatForegroundActivity(control: SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never): string | undefined {
436
466
  const facts: string[] = [];
437
467
  if (control.currentTool && control.currentToolStartedAt) facts.push(`tool ${control.currentTool} for ${Math.floor(Math.max(0, Date.now() - control.currentToolStartedAt) / 1000)}s`);
@@ -483,12 +513,18 @@ function withSpawnBudgetStatus(
483
513
  sessionId: string | null,
484
514
  ): AgentToolResult<Details> {
485
515
  const spawnBudget = getSpawnBudgetSnapshot(state, config, sessionId);
516
+ const activeAsyncCapacity = sessionId
517
+ ? getActiveAsyncCapacitySnapshot(sessionId, resolveMaxActiveAsyncRunsPerSession(config.maxActiveAsyncRunsPerSession), {
518
+ liveWorkflowRunIds: new Set(state.workflowControllers?.keys() ?? []),
519
+ })
520
+ : { used: 0, limit: resolveMaxActiveAsyncRunsPerSession(config.maxActiveAsyncRunsPerSession) ?? 0 };
521
+ state.activeAsyncCapacity = activeAsyncCapacity;
486
522
  return {
487
523
  ...result,
488
524
  content: result.content.map((item, index) => index === 0 && item.type === "text"
489
- ? { ...item, text: `${formatSpawnBudget(spawnBudget)}\n${item.text}` }
525
+ ? { ...item, text: `${formatSpawnBudget(spawnBudget)}\nActive async capacity: ${activeAsyncCapacity.used}/${activeAsyncCapacity.limit || "unlimited"} used\n${item.text}` }
490
526
  : item),
491
- details: { ...result.details, spawnBudget },
527
+ details: { ...result.details, spawnBudget, activeAsyncCapacity },
492
528
  };
493
529
  }
494
530
 
@@ -509,6 +545,31 @@ function countRequestedSubagentSpawns(params: SubagentParamsLike, config: Extens
509
545
  return params.agent ? 1 : 0;
510
546
  }
511
547
 
548
+ function staticRunFanoutPaths(params: SubagentParamsLike): string[] {
549
+ if (params.tasks) return params.tasks.map((_, index) => `tasks[${index}]`);
550
+ if (params.chain) return params.chain.flatMap((step, stepIndex) => {
551
+ if (isDynamicParallelStep(step) || "checkpoint" in step) return [];
552
+ if (isParallelStep(step)) return step.parallel.map((_, itemIndex) => `chain[${stepIndex}].parallel[${itemIndex}]`);
553
+ return [`chain[${stepIndex}]`];
554
+ });
555
+ return params.agent ? ["single"] : [];
556
+ }
557
+
558
+ function runFanoutErrorResult(error: RunFanoutLimitError, mode: "single" | "parallel" | "chain"): AgentToolResult<Details> {
559
+ return { content: [{ type: "text", text: error.message }], isError: true, details: { mode, results: [], runFanoutBudget: error.snapshot, runFanoutRejection: error.rejection } };
560
+ }
561
+
562
+ function withRunFanoutBudget(result: AgentToolResult<Details>, descriptor: RunFanoutBudgetDescriptor, options: { annotateContent?: boolean } = {}): AgentToolResult<Details> {
563
+ const runFanoutBudget = getRunFanoutBudgetSnapshot(descriptor);
564
+ return {
565
+ ...result,
566
+ content: options.annotateContent === false
567
+ ? result.content
568
+ : result.content.map((item, index) => index === 0 && item.type === "text" ? { ...item, text: `${formatRunFanoutBudget(runFanoutBudget)}\n${item.text}` } : item),
569
+ details: { ...result.details, runFanoutBudget },
570
+ };
571
+ }
572
+
512
573
  function foregroundStatusResult(control: SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never): AgentToolResult<Details> {
513
574
  let nestedWarning: string | undefined;
514
575
  try {
@@ -1141,18 +1202,30 @@ function appendStepToAsyncChain(input: {
1141
1202
  }
1142
1203
 
1143
1204
  try {
1205
+ const asyncDir = resolved.location.asyncDir;
1206
+ if (!asyncDir) throw new Error(`Run '${resolved.id}' is missing its async directory.`);
1207
+ const runFanoutBudget = readRunFanoutBudgetDescriptor(asyncDir);
1208
+ if (!runFanoutBudget) throw new Error(`Run '${resolved.id}' is missing its run fan-out budget identity.`);
1209
+ const startIndex = (status.chainStepCount ?? status.steps?.length ?? 0) + pendingAppendRequests.reduce((total, request) => total + request.steps.length, 0);
1210
+ const appendPaths = chain.flatMap((step, localIndex) => {
1211
+ const absoluteIndex = startIndex + localIndex;
1212
+ if (isDynamicParallelStep(step) || "checkpoint" in step) return [];
1213
+ if (isParallelStep(step)) return step.parallel.map((_, itemIndex) => `chain[${absoluteIndex}].parallel[${itemIndex}]`);
1214
+ return [`chain[${absoluteIndex}]`];
1215
+ });
1144
1216
  const result = enqueueChainAppendRequest({
1145
- asyncDir: resolved.location.asyncDir,
1217
+ asyncDir,
1146
1218
  runId: resolved.id,
1147
1219
  steps: built.steps,
1220
+ admit: (persist) => claimRunFanoutBatchWithCommit(runFanoutBudget, appendPaths, persist),
1148
1221
  });
1149
1222
  const stepText = built.steps.length === 1 ? "step" : "steps";
1150
1223
  return {
1151
1224
  content: [{
1152
1225
  type: "text",
1153
- text: `Append queued for chain run ${resolved.id}: ${built.steps.length} ${stepText}. It becomes eligible after the chain's already-queued steps finish. Pending appends: ${result.pendingCount}.`,
1226
+ text: `Append queued for chain run ${resolved.id}: ${built.steps.length} ${stepText}. It becomes eligible after the chain's already-queued steps finish. Pending appends: ${result.pendingCount}.${result.bookkeepingError ? ` Bookkeeping warning: ${result.bookkeepingError}` : ""}`,
1154
1227
  }],
1155
- details: { mode: "management", results: [], asyncId: resolved.id, asyncDir: resolved.location.asyncDir },
1228
+ details: { mode: "management", results: [], asyncId: resolved.id, asyncDir },
1156
1229
  };
1157
1230
  } catch (error) {
1158
1231
  const message = error instanceof Error ? error.message : String(error);
@@ -1443,17 +1516,18 @@ async function resumeAsyncRun(input: {
1443
1516
  const discoveredAgents = discovered.agents;
1444
1517
  const modelScope = discovered.modelScope;
1445
1518
  const sessionName = resolveIntercomSessionTarget(input.deps.pi.getSessionName(), input.ctx.sessionManager.getSessionId());
1519
+ const recoveryDescriptor = "recoveryDescriptor" in target ? target.recoveryDescriptor : undefined;
1446
1520
  const intercomBridge = resolveIntercomBridge({
1447
1521
  config: input.deps.config.intercomBridge,
1522
+ override: input.params.intercomBridge ?? recoveryDescriptor?.intercomBridge,
1448
1523
  context: input.params.context,
1449
1524
  orchestratorTarget: sessionName,
1450
1525
  });
1451
1526
  const agents = intercomBridge.active
1452
1527
  ? discoveredAgents.map((agent) => applyIntercomBridgeToAgent(agent, intercomBridge))
1453
1528
  : discoveredAgents;
1454
- const recoveryDescriptor = "recoveryDescriptor" in target ? target.recoveryDescriptor : undefined;
1455
- const discoveredAgentConfig = agents.find((agent) => agent.name === target.agent);
1456
- const agentConfig: AgentConfig | undefined = discoveredAgentConfig ?? (recoveryDescriptor ? {
1529
+ const discoveredAgentConfig = discoveredAgents.find((agent) => agent.name === target.agent);
1530
+ const baseAgentConfig: AgentConfig | undefined = discoveredAgentConfig ?? (recoveryDescriptor ? {
1457
1531
  name: recoveryDescriptor.agent,
1458
1532
  description: "Persisted async recovery contract",
1459
1533
  systemPrompt: "",
@@ -1463,7 +1537,7 @@ async function resumeAsyncRun(input: {
1463
1537
  source: "project",
1464
1538
  filePath: recoveryDescriptor.agentFilePath ?? path.join(getProjectSubagentsDir(recoveryDescriptor.cwd), "recovery-agent"),
1465
1539
  } : undefined);
1466
- if (!agentConfig) {
1540
+ if (!baseAgentConfig) {
1467
1541
  return {
1468
1542
  content: [{ type: "text", text: `Unknown agent for resume: ${target.agent}` }],
1469
1543
  isError: true,
@@ -1487,6 +1561,20 @@ async function resumeAsyncRun(input: {
1487
1561
  };
1488
1562
  }
1489
1563
  const runId = randomUUID().slice(0, 8);
1564
+ const topLevelResume = depth === 0 && !resolveInheritedNestedRouteFromEnv() && !input.params.workflowParentRunId;
1565
+ let activeAsyncCapacity: ActiveAsyncCapacityHandle | undefined;
1566
+ try {
1567
+ activeAsyncCapacity = topLevelResume ? acquireActiveAsyncCapacity({
1568
+ sessionId: input.deps.state.currentSessionId!,
1569
+ limit: resolveMaxActiveAsyncRunsPerSession(input.deps.config.maxActiveAsyncRunsPerSession),
1570
+ runId,
1571
+ kind: "runner",
1572
+ asyncDir: path.join(DIRS.async, runId),
1573
+ }, { liveWorkflowRunIds: new Set(input.deps.state.workflowControllers?.keys() ?? []) }) : undefined;
1574
+ } catch (error) {
1575
+ if (error instanceof ActiveAsyncCapacityError) return { content: [{ type: "text", text: error.message }], isError: true, details: { mode: "chain", results: [], activeAsyncCapacity: error.snapshot } };
1576
+ throw error;
1577
+ }
1490
1578
  const artifactConfig: ArtifactConfig = omitUndefinedProperties({ ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false, dir: input.deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir });
1491
1579
  const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo);
1492
1580
  const contextPolicy = resolveExplicitContextPolicy(input.params);
@@ -1538,9 +1626,14 @@ async function resumeAsyncRun(input: {
1538
1626
  controlIntercomTarget: intercomBridge.active ? intercomBridge.orchestratorTarget : undefined,
1539
1627
  childIntercomTarget: intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
1540
1628
  globalConcurrencyLimit: input.deps.config.globalConcurrencyLimit,
1629
+ runFanoutBudget: createRunFanoutBudget(runId, resolveMaxSubagentSpawnsPerRun(input.deps.config.maxSubagentSpawnsPerRun)),
1541
1630
  capabilityCeiling: intersectSubagentCapabilityCeilings("capabilityCeiling" in target ? target.capabilityCeiling : undefined, resolveCurrentSubagentCapabilityCeiling(input.deps.state.currentSessionId)),
1631
+ activeAsyncCapacity,
1542
1632
  }));
1543
- if (result.isError) return result;
1633
+ if (result.isError) {
1634
+ activeAsyncCapacity?.rollback();
1635
+ return result;
1636
+ }
1544
1637
  const attachedId = result.details.asyncId ?? runId;
1545
1638
  const lines = [
1546
1639
  `Attached async subagent ${target.runId} as the first step of a new chain.`,
@@ -1559,17 +1652,45 @@ async function resumeAsyncRun(input: {
1559
1652
  if (!revivalSessionFile) {
1560
1653
  return { content: [{ type: "text", text: `Async child '${target.runId}' has no persisted session file to resume.` }], isError: true, details: { mode: "management", results: [] } };
1561
1654
  }
1655
+ if (target.source === "async" && !recoveryDescriptor) {
1656
+ return { content: [{ type: "text", text: `Async child '${target.runId}' is missing its required run fan-out recovery identity. Start a new run instead.` }], isError: true, details: { mode: "management", results: [] } };
1657
+ }
1562
1658
  const runId = randomUUID().slice(0, 8);
1563
- const recoveryAgentConfig = recoveryDescriptor ? applySteeringRecoveryAgentConfig(agentConfig, recoveryDescriptor) : agentConfig;
1659
+ const topLevelResume = depth === 0 && !resolveInheritedNestedRouteFromEnv() && !input.params.workflowParentRunId;
1660
+ let activeAsyncCapacity: ActiveAsyncCapacityHandle | undefined;
1661
+ try {
1662
+ activeAsyncCapacity = !topLevelResume ? undefined : target.source === "async"
1663
+ ? transferActiveAsyncCapacity({
1664
+ sessionId: input.deps.state.currentSessionId!,
1665
+ limit: resolveMaxActiveAsyncRunsPerSession(input.deps.config.maxActiveAsyncRunsPerSession),
1666
+ sourceRunId: target.runId,
1667
+ runId,
1668
+ asyncDir: path.join(DIRS.async, runId),
1669
+ })
1670
+ : acquireActiveAsyncCapacity({
1671
+ sessionId: input.deps.state.currentSessionId!,
1672
+ limit: resolveMaxActiveAsyncRunsPerSession(input.deps.config.maxActiveAsyncRunsPerSession),
1673
+ runId,
1674
+ kind: "runner",
1675
+ asyncDir: path.join(DIRS.async, runId),
1676
+ });
1677
+ } catch (error) {
1678
+ if (error instanceof ActiveAsyncCapacityError) return { content: [{ type: "text", text: error.message }], isError: true, details: { mode: "single", results: [], activeAsyncCapacity: error.snapshot } };
1679
+ return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, details: { mode: "single", results: [] } };
1680
+ }
1681
+ const recoveryAgentConfig = recoveryDescriptor ? applySteeringRecoveryAgentConfig(baseAgentConfig, recoveryDescriptor) : baseAgentConfig;
1682
+ const agentConfig = intercomBridge.active ? applyIntercomBridgeToAgent(recoveryAgentConfig, intercomBridge) : recoveryAgentConfig;
1564
1683
  const artifactConfig: ArtifactConfig = recoveryDescriptor?.artifactConfig ?? omitUndefinedProperties({ ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false, dir: input.deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir });
1565
1684
  const artifactsDir = recoveryDescriptor?.artifactsDir ?? getArtifactsDir(parentSessionFile, effectiveCwd, artifactConfig.dir);
1566
1685
  const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo);
1567
1686
  const parentModel = input.parentModel;
1687
+ const revivalAsyncDir = path.join(DIRS.async, runId);
1568
1688
  const result = executeAsyncSingle(runId, compactOptional<Parameters<typeof executeAsyncSingle>[1]>({
1569
1689
  agent: target.agent,
1570
1690
  task: buildRevivedAsyncTask(target as Parameters<typeof buildRevivedAsyncTask>[0], effectiveFollowUp),
1571
1691
  goal: effectiveFollowUp,
1572
- agentConfig: recoveryAgentConfig,
1692
+ agentConfig,
1693
+ recoveryAgentConfig,
1573
1694
  ctx: compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
1574
1695
  pi: input.deps.pi,
1575
1696
  cwd: input.requestCwd,
@@ -1604,6 +1725,7 @@ async function resumeAsyncRun(input: {
1604
1725
  worktreeSetupHookTimeoutMs: input.deps.config.worktreeSetupHookTimeoutMs,
1605
1726
  worktreeBaseDir: input.deps.config.worktreeBaseDir,
1606
1727
  controlConfig: recoveryDescriptor?.controlConfig ?? resolveControlConfig(input.deps.config.control, input.params.control),
1728
+ intercomBridge: input.params.intercomBridge ?? recoveryDescriptor?.intercomBridge,
1607
1729
  controlIntercomTarget: intercomBridge.active ? intercomBridge.orchestratorTarget : undefined,
1608
1730
  childIntercomTarget: intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
1609
1731
  availableModels,
@@ -1618,10 +1740,28 @@ async function resumeAsyncRun(input: {
1618
1740
  ...(input.params.turnBudget !== undefined ? { turnBudget: input.params.turnBudget } : {}),
1619
1741
  ...(input.params.toolBudget !== undefined ? { toolBudget: input.params.toolBudget } : {}),
1620
1742
  capabilityCeiling: intersectSubagentCapabilityCeilings("capabilityCeiling" in target ? target.capabilityCeiling : undefined, recoveryDescriptor?.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(input.deps.state.currentSessionId)),
1743
+ runFanoutBudget: input.params.runFanoutBudget ?? recoveryDescriptor?.runFanoutBudget ?? createRunFanoutBudget(runId, resolveMaxSubagentSpawnsPerRun(input.deps.config.maxSubagentSpawnsPerRun)),
1621
1744
  parentWorkflowRunId: input.params.workflowParentRunId,
1622
1745
  workflowKey: input.params.workflowKey,
1746
+ activeAsyncCapacity,
1623
1747
  }));
1624
- if (result.isError) return result;
1748
+ if (result.isError) {
1749
+ const startedStatus = readStatus(revivalAsyncDir);
1750
+ if (input.params.workflowParentRunId !== undefined && startedStatus?.runId === runId && startedStatus.processTerminal?.runnerProcessInstanceId) {
1751
+ return {
1752
+ ...result,
1753
+ details: {
1754
+ ...result.details,
1755
+ runId,
1756
+ asyncId: runId,
1757
+ asyncDir: revivalAsyncDir,
1758
+ ...(target.launchContractDigest ? { sourceLaunchContractDigest: target.launchContractDigest } : {}),
1759
+ },
1760
+ };
1761
+ }
1762
+ activeAsyncCapacity?.rollback();
1763
+ return result;
1764
+ }
1625
1765
  for (const brief of queuedBriefs) fs.rmSync(brief.path, { force: true });
1626
1766
  if (queuedBriefs.length > 0 && sourceAsyncDir) {
1627
1767
  const sourceStatus = readStatus(sourceAsyncDir);
@@ -2117,6 +2257,27 @@ export const DEFAULT_FOREGROUND_TIMEOUT_MS = 30 * 60 * 1000;
2117
2257
  // stay unbounded as a whole, while each runner child has its own deadline.
2118
2258
  export { DEFAULT_ASYNC_TIMEOUT_MS };
2119
2259
 
2260
+ /**
2261
+ * Maximum delay a Node.js timer accepts. Values above the 32-bit signed integer
2262
+ * ceiling overflow `setTimeout`, which silently clamps the delay to ~1ms and
2263
+ * fires almost immediately — so a run configured with a larger deadline would
2264
+ * terminate right away while reporting the long duration. Any timeout destined
2265
+ * for a timer must stay within this bound.
2266
+ */
2267
+ const MAX_TIMER_DELAY_MS = 2_147_483_647;
2268
+
2269
+ /**
2270
+ * Resolve the optional global default runtime deadline from extension config
2271
+ * (`config.timeoutMs`). Returns undefined for unset or invalid values so callers
2272
+ * fall back to the built-in defaults. "Invalid" covers non-positive-integer
2273
+ * values and values above `MAX_TIMER_DELAY_MS`; the latter would overflow the
2274
+ * Node.js timer and expire the run almost immediately instead of running long.
2275
+ */
2276
+ export function resolveConfigDefaultTimeoutMs(raw: unknown): number | undefined {
2277
+ if (typeof raw !== "number" || !Number.isInteger(raw) || raw <= 0 || raw > MAX_TIMER_DELAY_MS) return undefined;
2278
+ return raw;
2279
+ }
2280
+
2120
2281
  export function resolveForegroundTimeout(params: SubagentParamsLike, defaultTimeoutMs?: number): { timeoutMs?: number; error?: string } {
2121
2282
  const rawTimeout = params.timeoutMs;
2122
2283
  const rawMaxRuntime = params.maxRuntimeMs;
@@ -2140,14 +2301,18 @@ export function resolveForegroundTimeout(params: SubagentParamsLike, defaultTime
2140
2301
  * Resolve the effective launch timeout for a single-agent run, applying the
2141
2302
  * async/foreground default when neither the caller nor the agent set one.
2142
2303
  *
2143
- * The async default is deliberately applied only to plain single-agent
2144
- * launches. Composite launches keep their top-level execution unbounded when
2145
- * no timeout is set; their runner children resolve separate deadlines.
2146
- * Exported so the executor wiring is directly testable.
2304
+ * A global config default (`config.timeoutMs`, passed as `configDefaultTimeoutMs`)
2305
+ * replaces the built-in 30-minute backstop wherever a concrete default is applied.
2306
+ * The async default is deliberately applied only to plain single-agent launches.
2307
+ * Composite launches keep their top-level execution unbounded when no timeout is
2308
+ * set — even with a config default — while their runner children resolve separate
2309
+ * deadlines. Exported so the executor wiring is directly testable.
2147
2310
  */
2148
- export function resolveSingleAgentLaunchTimeout(params: SubagentParamsLike, async: boolean): { timeoutMs?: number; error?: string } {
2311
+ export function resolveSingleAgentLaunchTimeout(params: SubagentParamsLike, async: boolean, configDefaultTimeoutMs?: number): { timeoutMs?: number; error?: string } {
2149
2312
  const isComposite = (params.chain?.length ?? 0) > 0 || (params.tasks?.length ?? 0) > 0 || params.workflowScript !== undefined;
2150
- const defaultTimeoutMs = !async ? DEFAULT_FOREGROUND_TIMEOUT_MS : isComposite ? undefined : DEFAULT_ASYNC_TIMEOUT_MS;
2313
+ const foregroundDefault = configDefaultTimeoutMs ?? DEFAULT_FOREGROUND_TIMEOUT_MS;
2314
+ const asyncSingleDefault = configDefaultTimeoutMs ?? DEFAULT_ASYNC_TIMEOUT_MS;
2315
+ const defaultTimeoutMs = !async ? foregroundDefault : isComposite ? undefined : asyncSingleDefault;
2151
2316
  return resolveForegroundTimeout(params, defaultTimeoutMs);
2152
2317
  }
2153
2318
 
@@ -2560,8 +2725,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2560
2725
  details: { mode: "single" as const, results: [] },
2561
2726
  };
2562
2727
  }
2563
- const requestedWorkflowChildAsyncId = typeof params.workflowChildAsyncId === "string" ? params.workflowChildAsyncId.trim() : "";
2564
- const id = requestedWorkflowChildAsyncId && path.basename(requestedWorkflowChildAsyncId) === requestedWorkflowChildAsyncId ? requestedWorkflowChildAsyncId : randomUUID();
2728
+ const id = data.asyncRunId;
2565
2729
  const parentModel = data.parentModel;
2566
2730
  const asyncCtx = compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
2567
2731
  pi: deps.pi,
@@ -2615,6 +2779,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2615
2779
  shareEnabled,
2616
2780
  sessionRoot,
2617
2781
  chainSkills: [],
2782
+ activeAsyncCapacity: data.activeAsyncCapacity,
2618
2783
  sessionFilesByFlatIndex: params.tasks.map((task, index) => sessionFileForTask(task.agent, index, task.model)),
2619
2784
  thinkingOverridesByFlatIndex: params.tasks.map((task, index) => thinkingOverrideForTask(task.agent, index, task.model)),
2620
2785
  contextForAgent: contextPolicy.contextForAgent,
@@ -2634,6 +2799,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2634
2799
  usageBudget: data.usageBudget,
2635
2800
  configToolBudget: data.configToolBudget,
2636
2801
  capabilityCeiling: data.capabilityCeiling,
2802
+ runFanoutBudget: data.runFanoutBudget,
2637
2803
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2638
2804
  parentWorkflowRunId: params.workflowParentRunId,
2639
2805
  workflowKey: params.workflowKey,
@@ -2659,6 +2825,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2659
2825
  shareEnabled,
2660
2826
  sessionRoot,
2661
2827
  chainSkills,
2828
+ activeAsyncCapacity: data.activeAsyncCapacity,
2662
2829
  sessionFilesByFlatIndex: collectChainSessionFiles(chain, sessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems),
2663
2830
  thinkingOverridesByFlatIndex: collectChainThinkingOverrides(chain, thinkingOverrideForTask, deps.config.chain?.dynamicFanout?.maxItems),
2664
2831
  contextForAgent: contextPolicy.contextForAgent,
@@ -2679,6 +2846,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2679
2846
  usageBudget: data.usageBudget,
2680
2847
  configToolBudget: data.configToolBudget,
2681
2848
  capabilityCeiling: data.capabilityCeiling,
2849
+ runFanoutBudget: data.runFanoutBudget,
2682
2850
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2683
2851
  parentWorkflowRunId: params.workflowParentRunId,
2684
2852
  workflowKey: params.workflowKey,
@@ -2700,12 +2868,18 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2700
2868
  const normalizedSkills = normalizeSkillInput(params.skill);
2701
2869
  const skills = normalizedSkills === false ? [] : normalizedSkills;
2702
2870
  const maxSubagentDepth = resolveChildMaxSubagentDepth(currentMaxSubagentDepth, a.maxSubagentDepth);
2703
- const modelOverride = resolveEffectiveSubagentModel(params.model as string | undefined, a.model, parentModel, availableModels, currentProvider, data.modelScope === undefined ? {} : { scope: data.modelScope });
2871
+ const externalRunnerWithoutExplicitModel = a.runner?.type === "external-cli"
2872
+ && params.model === undefined
2873
+ && (a.model === undefined || (a.modelSource?.type === "subagents.defaultModel" && a.model === a.modelSource.model));
2874
+ const modelOverride = externalRunnerWithoutExplicitModel
2875
+ ? undefined
2876
+ : resolveEffectiveSubagentModel(params.model as string | undefined, a.model, parentModel, availableModels, currentProvider, data.modelScope === undefined ? {} : { scope: data.modelScope });
2704
2877
  return executeAsyncSingle(id, compactOptional<Parameters<typeof executeAsyncSingle>[1]>({
2705
2878
  agent: params.agent!,
2706
2879
  task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(params.task ?? "") : (params.task ?? ""),
2707
2880
  goal: params.task ?? "",
2708
2881
  agentConfig: a,
2882
+ recoveryAgentConfig: data.recoveryAgents.find((agent) => agent.name === params.agent),
2709
2883
  ctx: asyncCtx,
2710
2884
  availableModels,
2711
2885
  cwd: effectiveCwd,
@@ -2713,6 +2887,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2713
2887
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
2714
2888
  artifactConfig,
2715
2889
  shareEnabled,
2890
+ activeAsyncCapacity: data.activeAsyncCapacity,
2716
2891
  sessionRoot,
2717
2892
  sessionFile: sessionFileForTask(params.agent!, 0, modelOverride),
2718
2893
  context: contextPolicy.contextForAgent(params.agent!),
@@ -2722,13 +2897,14 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2722
2897
  ...(params.reads !== undefined ? { reads: params.reads } : {}),
2723
2898
  outputBaseDir: resolveSingleRunOutputBaseDir(deps, artifactsDir, id),
2724
2899
  modelOverride,
2725
- thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride),
2900
+ thinkingOverride: externalRunnerWithoutExplicitModel ? undefined : thinkingOverrideForTask(params.agent!, 0, modelOverride),
2726
2901
  maxSubagentDepth,
2727
2902
  waitToolEnabled: deps.waitToolEnabled,
2728
2903
  worktreeSetupHook: deps.config.worktreeSetupHook,
2729
2904
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
2730
2905
  worktreeBaseDir: deps.config.worktreeBaseDir,
2731
2906
  controlConfig,
2907
+ intercomBridge: params.intercomBridge,
2732
2908
  controlIntercomTarget,
2733
2909
  childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(agent, index) : undefined,
2734
2910
  nestedRoute,
@@ -2741,6 +2917,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2741
2917
  usageBudget: data.usageBudget,
2742
2918
  configToolBudget: data.configToolBudget,
2743
2919
  capabilityCeiling: data.capabilityCeiling,
2920
+ runFanoutBudget: data.runFanoutBudget,
2744
2921
  parentWorkflowRunId: params.workflowParentRunId,
2745
2922
  workflowKey: params.workflowKey,
2746
2923
  }));
@@ -2831,6 +3008,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2831
3008
  permissions: deps.config.permissions,
2832
3009
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2833
3010
  capabilityCeiling: data.capabilityCeiling,
3011
+ runFanoutBudget: data.runFanoutBudget,
2834
3012
  }));
2835
3013
 
2836
3014
  if (chainResult.requestedAsync) {
@@ -2841,7 +3019,9 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2841
3019
  details: { mode: "chain" as const, results: [] },
2842
3020
  };
2843
3021
  }
2844
- const id = randomUUID();
3022
+ const id = data.asyncRunId;
3023
+ const capacity = acquireClarifiedBackgroundCapacity(data, deps, "chain", id);
3024
+ if (capacity.errorResult) return capacity.errorResult;
2845
3025
  const parentModel = data.parentModel;
2846
3026
  const asyncCtx = compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
2847
3027
  pi: deps.pi,
@@ -2857,42 +3037,48 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2857
3037
  const rawAsyncChain = chainResult.requestedAsync.chain;
2858
3038
  const asyncChain = wrapChainTasksForFork(rawAsyncChain, contextPolicy);
2859
3039
  const firstAgent = firstChainAgent(rawAsyncChain);
2860
- return executeAsyncChain(id, compactOptional<Parameters<typeof executeAsyncChain>[1]>({
2861
- chain: asyncChain,
2862
- task: params.task,
2863
- goal: resolveAsyncEventGoal(params.task, rawAsyncChain, firstAgent ? shouldForkAgent(contextPolicy, firstAgent) : false),
2864
- agents,
2865
- ctx: asyncCtx,
2866
- availableModels: ctx.modelRegistry.getAvailable().map(toModelInfo),
2867
- cwd: effectiveCwd,
2868
- maxOutput: params.maxOutput,
2869
- artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
2870
- artifactConfig,
2871
- shareEnabled,
2872
- sessionRoot,
2873
- chainSkills: chainResult.requestedAsync.chainSkills,
2874
- sessionFilesByFlatIndex: collectChainSessionFiles(asyncChain, sessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems),
2875
- thinkingOverridesByFlatIndex: collectChainThinkingOverrides(asyncChain, thinkingOverrideForTask, deps.config.chain?.dynamicFanout?.maxItems),
2876
- contextForAgent: contextPolicy.contextForAgent,
2877
- dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
2878
- maxSubagentDepth: currentMaxSubagentDepth,
2879
- waitToolEnabled: deps.waitToolEnabled,
2880
- worktreeSetupHook: deps.config.worktreeSetupHook,
2881
- worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
2882
- worktreeBaseDir: deps.config.worktreeBaseDir,
2883
- controlConfig,
2884
- agentContract: params.agentContract,
2885
- controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
2886
- childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
2887
- nestedRoute: data.nestedRoute,
2888
- timeoutMs: data.timeoutMs,
2889
- turnBudget: data.turnBudget,
2890
- toolBudget: data.toolBudget,
2891
- usageBudget: data.usageBudget,
2892
- configToolBudget: data.configToolBudget,
2893
- capabilityCeiling: data.capabilityCeiling,
2894
- globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2895
- }));
3040
+ try {
3041
+ return executeAsyncChain(id, compactOptional<Parameters<typeof executeAsyncChain>[1]>({
3042
+ chain: asyncChain,
3043
+ task: params.task,
3044
+ goal: resolveAsyncEventGoal(params.task, rawAsyncChain, firstAgent ? shouldForkAgent(contextPolicy, firstAgent) : false),
3045
+ agents,
3046
+ ctx: asyncCtx,
3047
+ availableModels: ctx.modelRegistry.getAvailable().map(toModelInfo),
3048
+ cwd: effectiveCwd,
3049
+ maxOutput: params.maxOutput,
3050
+ artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
3051
+ artifactConfig,
3052
+ shareEnabled,
3053
+ sessionRoot,
3054
+ chainSkills: chainResult.requestedAsync.chainSkills,
3055
+ activeAsyncCapacity: capacity.activeAsyncCapacity,
3056
+ sessionFilesByFlatIndex: collectChainSessionFiles(asyncChain, sessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems),
3057
+ thinkingOverridesByFlatIndex: collectChainThinkingOverrides(asyncChain, thinkingOverrideForTask, deps.config.chain?.dynamicFanout?.maxItems),
3058
+ contextForAgent: contextPolicy.contextForAgent,
3059
+ dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
3060
+ maxSubagentDepth: currentMaxSubagentDepth,
3061
+ waitToolEnabled: deps.waitToolEnabled,
3062
+ worktreeSetupHook: deps.config.worktreeSetupHook,
3063
+ worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
3064
+ worktreeBaseDir: deps.config.worktreeBaseDir,
3065
+ controlConfig,
3066
+ agentContract: params.agentContract,
3067
+ controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
3068
+ childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
3069
+ nestedRoute: data.nestedRoute,
3070
+ timeoutMs: data.timeoutMs,
3071
+ turnBudget: data.turnBudget,
3072
+ toolBudget: data.toolBudget,
3073
+ usageBudget: data.usageBudget,
3074
+ configToolBudget: data.configToolBudget,
3075
+ capabilityCeiling: data.capabilityCeiling,
3076
+ runFanoutBudget: data.runFanoutBudget,
3077
+ globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
3078
+ }));
3079
+ } finally {
3080
+ finalizeClarifiedBackgroundCapacity(data, deps, capacity.activeAsyncCapacity);
3081
+ }
2896
3082
  }
2897
3083
 
2898
3084
  const rawChainDetails = chainResult.details ? compactOptional<Details>({ ...chainResult.details, runId, timeoutMs: data.timeoutMs }) : undefined;
@@ -2937,6 +3123,7 @@ interface ForegroundParallelRunInput {
2937
3123
  intercomEvents: IntercomEventBus;
2938
3124
  parentSessionId: string | null;
2939
3125
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
3126
+ runFanoutBudget: RunFanoutBudgetDescriptor;
2940
3127
  signal: AbortSignal;
2941
3128
  runId: string;
2942
3129
  sessionDirForIndex: (idx?: number) => string | undefined;
@@ -2986,6 +3173,48 @@ function buildParallelModeError(message: string): AgentToolResult<Details> {
2986
3173
  };
2987
3174
  }
2988
3175
 
3176
+ function acquireClarifiedBackgroundCapacity(
3177
+ data: ExecutionContextData,
3178
+ deps: ExecutorDeps,
3179
+ mode: "single" | "parallel" | "chain",
3180
+ asyncRunId: string,
3181
+ ): { activeAsyncCapacity?: ActiveAsyncCapacityHandle; errorResult?: AgentToolResult<Details> } {
3182
+ if (!data.topLevelAsyncCapacityEligible || !data.parentSessionId) return {};
3183
+ const liveWorkflowRunIds = new Set(deps.state.workflowControllers?.keys() ?? []);
3184
+ const activeLimit = resolveMaxActiveAsyncRunsPerSession(deps.config.maxActiveAsyncRunsPerSession);
3185
+ try {
3186
+ const activeAsyncCapacity = acquireActiveAsyncCapacity({
3187
+ sessionId: data.parentSessionId,
3188
+ limit: activeLimit,
3189
+ runId: asyncRunId,
3190
+ kind: "runner",
3191
+ asyncDir: path.join(DIRS.async, asyncRunId),
3192
+ }, { liveWorkflowRunIds });
3193
+ deps.state.activeAsyncCapacity = getActiveAsyncCapacitySnapshot(data.parentSessionId, activeLimit, { liveWorkflowRunIds });
3194
+ return { activeAsyncCapacity };
3195
+ } catch (error) {
3196
+ if (error instanceof ActiveAsyncCapacityError) {
3197
+ deps.state.activeAsyncCapacity = error.snapshot;
3198
+ return { errorResult: { content: [{ type: "text", text: error.message }], isError: true, details: { mode, results: [], activeAsyncCapacity: error.snapshot } } };
3199
+ }
3200
+ throw error;
3201
+ }
3202
+ }
3203
+
3204
+ function finalizeClarifiedBackgroundCapacity(
3205
+ data: ExecutionContextData,
3206
+ deps: ExecutorDeps,
3207
+ activeAsyncCapacity: ActiveAsyncCapacityHandle | undefined,
3208
+ ): void {
3209
+ if (!activeAsyncCapacity || !data.parentSessionId) return;
3210
+ if (!activeAsyncCapacity.owner.runnerStartedAt) activeAsyncCapacity.rollback();
3211
+ deps.state.activeAsyncCapacity = getActiveAsyncCapacitySnapshot(
3212
+ data.parentSessionId,
3213
+ resolveMaxActiveAsyncRunsPerSession(deps.config.maxActiveAsyncRunsPerSession),
3214
+ { liveWorkflowRunIds: new Set(deps.state.workflowControllers?.keys() ?? []) },
3215
+ );
3216
+ }
3217
+
2989
3218
  function createParallelWorktreeSetup(
2990
3219
  enabled: boolean | undefined,
2991
3220
  cwd: string,
@@ -3021,10 +3250,128 @@ function buildParallelWorktreeTaskCwdError(
3021
3250
  return formatWorktreeTaskCwdConflict(conflict, sharedCwd);
3022
3251
  }
3023
3252
 
3024
- function resolveSingleRunOutputBaseDir(deps: ExecutorDeps, artifactsDir: string, runId: string): string {
3253
+ function resolveConfiguredSingleRunOutputBaseDir(deps: ExecutorDeps): string | undefined {
3025
3254
  return deps.config.singleRunOutputBaseDir
3026
3255
  ? path.resolve(deps.expandTilde(deps.config.singleRunOutputBaseDir))
3027
- : path.join(artifactsDir, "outputs", runId);
3256
+ : undefined;
3257
+ }
3258
+
3259
+ function resolveSingleRunOutputBaseDir(deps: ExecutorDeps, artifactsDir: string, runId: string): string {
3260
+ return resolveConfiguredSingleRunOutputBaseDir(deps) ?? path.join(artifactsDir, "outputs", runId);
3261
+ }
3262
+
3263
+ function resolveWorkflowAggregateOutputPath(
3264
+ output: string | boolean | undefined,
3265
+ ctxCwd: string,
3266
+ workflowCwd: string,
3267
+ outputBaseDir: string,
3268
+ configuredOutputBaseDir: string | undefined,
3269
+ ): string | undefined {
3270
+ if (typeof output === "string" && output && !path.isAbsolute(output) && !configuredOutputBaseDir) return path.resolve(workflowCwd, output);
3271
+ return resolveSingleOutputPath(output, ctxCwd, workflowCwd, outputBaseDir);
3272
+ }
3273
+
3274
+ function workflowChildDefaultOutput(aggregateOutputPath: string | undefined, artifactsDir: string, workflowRunId: string, workflowKey: string): string {
3275
+ if (aggregateOutputPath) {
3276
+ const parsed = path.parse(aggregateOutputPath);
3277
+ return path.join(parsed.dir, `${parsed.name}.${workflowKey}${parsed.ext || ".md"}`);
3278
+ }
3279
+ return path.join(artifactsDir, "outputs", workflowRunId, `${workflowKey}.md`);
3280
+ }
3281
+
3282
+ function writeWorkflowAggregateOutput(outputPath: string | undefined, text: string): string | undefined {
3283
+ if (!outputPath) return undefined;
3284
+ try {
3285
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
3286
+ fs.writeFileSync(outputPath, text, "utf-8");
3287
+ return undefined;
3288
+ } catch (error) {
3289
+ return `Output file error: ${outputPath}\n${error instanceof Error ? error.message : String(error)}`;
3290
+ }
3291
+ }
3292
+
3293
+ function appendWorkflowOutputWarning(text: string, warning: string | undefined): string {
3294
+ return warning ? `${text}\n\n${warning}` : text;
3295
+ }
3296
+
3297
+ function resolveWorkflowChildOutputPath(input: {
3298
+ ctxCwd: string;
3299
+ workflowCwd: string;
3300
+ artifactsDir: string;
3301
+ workflowRunId: string;
3302
+ aggregateOutputPath?: string;
3303
+ configuredOutputBaseDir?: string;
3304
+ discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[] };
3305
+ workflowAgentScope?: unknown;
3306
+ key: string;
3307
+ params: Record<string, unknown>;
3308
+ }): string | undefined {
3309
+ const rawOutput = input.params.output;
3310
+ const hasExplicitOutput = typeof rawOutput === "string" || typeof rawOutput === "boolean";
3311
+ const childCwd = typeof input.params.cwd === "string" ? resolveChildCwd(input.workflowCwd, input.params.cwd) : input.workflowCwd;
3312
+ const agentScope = resolveExecutionAgentScope(input.params.agentScope ?? input.workflowAgentScope);
3313
+ const agents = input.discoverAgents(childCwd, agentScope).agents;
3314
+ const agent = typeof input.params.agent === "string" ? resolveAgentName(input.params.agent, agents).agent : undefined;
3315
+ const agentOutput = typeof agent?.output === "string" ? agent.output : undefined;
3316
+ const output = rawOutput === true || rawOutput === "true"
3317
+ ? agentOutput
3318
+ : hasExplicitOutput
3319
+ ? rawOutput
3320
+ : workflowChildDefaultOutput(input.aggregateOutputPath, input.artifactsDir, input.workflowRunId, input.key);
3321
+ return resolveSingleOutputPath(output, input.ctxCwd, childCwd, input.configuredOutputBaseDir);
3322
+ }
3323
+
3324
+ function workflowChildOutputClaims(input: {
3325
+ ctxCwd: string;
3326
+ workflowCwd: string;
3327
+ artifactsDir: string;
3328
+ workflowRunId: string;
3329
+ aggregateOutputPath?: string;
3330
+ configuredOutputBaseDir?: string;
3331
+ discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[] };
3332
+ workflowAgentScope?: unknown;
3333
+ claimedOutputPaths: Map<string, string>;
3334
+ entries: Array<{ key: string; params: Record<string, unknown> }>;
3335
+ }): { error?: string; claims?: Map<string, string> } {
3336
+ const claims = new Map(input.claimedOutputPaths);
3337
+ for (const { key, params } of input.entries) {
3338
+ const resolved = resolveWorkflowChildOutputPath({ ...input, key, params });
3339
+ if (!resolved) continue;
3340
+ 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.` };
3341
+ const previous = claims.get(resolved);
3342
+ if (previous) return { error: `Workflow children '${previous}' and '${key}' resolve output to the same path: ${resolved}. Use distinct child output paths.` };
3343
+ claims.set(resolved, key);
3344
+ }
3345
+ return { claims };
3346
+ }
3347
+
3348
+ function applyWorkflowChildOutputClaims(target: Map<string, string>, claims: Map<string, string>): void {
3349
+ target.clear();
3350
+ for (const [resolved, key] of claims) target.set(resolved, key);
3351
+ }
3352
+
3353
+ function prepareWorkflowChildLaunchParams(input: {
3354
+ workflowDefaults: SubagentParamsLike;
3355
+ childParams: Record<string, unknown>;
3356
+ parentWorkflowRunId: string;
3357
+ workflowKey: string;
3358
+ ctxCwd: string;
3359
+ workflowCwd: string;
3360
+ artifactsDir: string;
3361
+ aggregateOutputPath?: string;
3362
+ configuredOutputBaseDir?: string;
3363
+ discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[] };
3364
+ workflowAgentScope?: unknown;
3365
+ options?: { missionDetached?: boolean; suppressRoutineResultIntercom?: boolean; runFanoutBudget?: RunFanoutBudgetDescriptor };
3366
+ }): SubagentParamsLike {
3367
+ let childParams = input.childParams;
3368
+ if (input.childParams.output === undefined && input.childParams.resume === undefined) {
3369
+ childParams = { ...input.childParams, output: workflowChildDefaultOutput(input.aggregateOutputPath, input.artifactsDir, input.parentWorkflowRunId, input.workflowKey) };
3370
+ } else if (input.childParams.resume === undefined) {
3371
+ 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 });
3372
+ if (resolvedOutput) childParams = { ...input.childParams, output: resolvedOutput };
3373
+ }
3374
+ return prepareWorkflowLaunchParams(input.workflowDefaults, childParams, input.parentWorkflowRunId, input.workflowKey, input.options);
3028
3375
  }
3029
3376
 
3030
3377
  function buildChainWorktreeTaskCwdError(chain: ChainStep[], sharedCwd: string): string | undefined {
@@ -3174,7 +3521,33 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
3174
3521
  beginForegroundChild(input.foregroundControl, {
3175
3522
  index,
3176
3523
  agent: task.agent,
3177
- ...(input.taskDescriptions[index] === undefined ? {} : { description: input.taskDescriptions[index] }),
3524
+ authoredTask: input.taskDescriptions[index] ?? "",
3525
+ effectivePrompt: taskText,
3526
+ cwd: taskCwd,
3527
+ ...(outputPath ? { outputPath } : {}),
3528
+ rerun: { params: compactOptional<Record<string, unknown>>({
3529
+ agent: task.agent,
3530
+ task: input.taskDescriptions[index] ?? "",
3531
+ async: false,
3532
+ cwd: input.worktreeSetup ? undefined : taskCwd,
3533
+ worktree: input.worktreeSetup ? true : false,
3534
+ context: input.contextPolicy.contextForAgent(task.agent),
3535
+ share: input.shareEnabled || undefined,
3536
+ maxOutput: input.maxOutput,
3537
+ timeoutMs: input.timeoutMs,
3538
+ turnBudget: input.turnBudget,
3539
+ ...(model ? { model } : {}),
3540
+ ...(effectiveSkills !== undefined ? { skill: effectiveSkills } : {}),
3541
+ ...(outputPath ? { output: outputPath } : {}),
3542
+ ...(behavior?.outputMode !== undefined ? { outputMode: behavior.outputMode } : {}),
3543
+ ...(behavior?.reads !== undefined ? { reads: behavior.reads } : {}),
3544
+ ...(behavior?.progress !== undefined ? { progress: behavior.progress } : {}),
3545
+ ...(task.toolBudget !== undefined ? { toolBudget: task.toolBudget } : {}),
3546
+ ...(task.outputSchema !== undefined ? { outputSchema: task.outputSchema } : {}),
3547
+ ...(task.acceptance !== undefined ? { acceptance: task.acceptance } : {}),
3548
+ ...(task.agentContract !== undefined || input.agentContract !== undefined ? { agentContract: task.agentContract ?? input.agentContract } : {}),
3549
+ }) },
3550
+ description: `${task.agent} child`,
3178
3551
  ...(model ? { model } : {}),
3179
3552
  ...(thinking ? { thinking } : {}),
3180
3553
  interrupt: () => {
@@ -3191,8 +3564,10 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
3191
3564
  const result = await runSync(input.ctx.cwd, input.agents, task.agent, taskText, compactOptional<Parameters<typeof runSync>[4]>({
3192
3565
  permissions: input.permissions,
3193
3566
  parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
3567
+ llmIntentArbiter: createTaskMutationArbiter(input.ctx),
3194
3568
  ...workflowForegroundSteeringLaunchOptions(input.foregroundControl, index),
3195
3569
  context: input.contextPolicy.contextForAgent(task.agent),
3570
+ runFanoutBudget: { ...input.runFanoutBudget, parentPath: `${input.runFanoutBudget.parentPath ? `${input.runFanoutBudget.parentPath}/` : ""}tasks[${index}]` },
3196
3571
  cwd: taskCwd,
3197
3572
  signal: input.signal,
3198
3573
  interruptSignal: interruptController.signal,
@@ -3237,6 +3612,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
3237
3612
  agentContract: task.agentContract ?? input.agentContract,
3238
3613
  acceptance: task.acceptance,
3239
3614
  acceptanceContext: { mode: "parallel" },
3615
+ onEffectivePrompt: input.foregroundControl ? (prompt) => updateLiveEffectivePrompt(input.foregroundControl!, index, prompt) : undefined,
3240
3616
  timeoutMs: input.timeoutMs,
3241
3617
  deadlineAt: input.deadlineAt,
3242
3618
  turnBudget: input.turnBudget,
@@ -3420,7 +3796,9 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3420
3796
  details: { mode: "parallel" as const, results: [] },
3421
3797
  };
3422
3798
  }
3423
- const id = randomUUID();
3799
+ const id = data.asyncRunId;
3800
+ const capacity = acquireClarifiedBackgroundCapacity(data, deps, "parallel", id);
3801
+ if (capacity.errorResult) return capacity.errorResult;
3424
3802
  const asyncCtx = compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
3425
3803
  pi: deps.pi,
3426
3804
  cwd: ctx.cwd,
@@ -3451,40 +3829,46 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3451
3829
  ...(t.agentContract !== undefined ? { agentContract: t.agentContract } : {}),
3452
3830
  });
3453
3831
  });
3454
- return executeAsyncChain(id, compactOptional<Parameters<typeof executeAsyncChain>[1]>({
3455
- chain: [compactOptional<ParallelStep>({ parallel: parallelTasks, concurrency: parallelConcurrency, worktree: params.worktree })],
3456
- resultMode: "parallel",
3457
- goal: taskTexts[0] ?? "",
3458
- agents,
3459
- ctx: asyncCtx,
3460
- availableModels,
3461
- cwd: effectiveCwd,
3462
- maxOutput: params.maxOutput,
3463
- artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
3464
- artifactConfig,
3465
- shareEnabled,
3466
- sessionRoot,
3467
- chainSkills: [],
3468
- sessionFilesByFlatIndex: tasks.map((task, index) => sessionFileForTask(task.agent, index, modelOverrides[index])),
3469
- thinkingOverridesByFlatIndex: tasks.map((task, index) => thinkingOverrideForTask(task.agent, index, modelOverrides[index])),
3470
- contextForAgent: contextPolicy.contextForAgent,
3471
- maxSubagentDepth: currentMaxSubagentDepth,
3472
- waitToolEnabled: deps.waitToolEnabled,
3473
- worktreeSetupHook: deps.config.worktreeSetupHook,
3474
- worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
3475
- worktreeBaseDir: deps.config.worktreeBaseDir,
3476
- controlConfig,
3477
- agentContract: params.agentContract,
3478
- controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
3479
- childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
3480
- nestedRoute: data.nestedRoute,
3481
- timeoutMs: data.timeoutMs,
3482
- turnBudget: data.turnBudget,
3483
- usageBudget: data.usageBudget,
3484
- toolBudget: data.toolBudget,
3485
- configToolBudget: data.configToolBudget,
3486
- globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
3487
- }));
3832
+ try {
3833
+ return executeAsyncChain(id, compactOptional<Parameters<typeof executeAsyncChain>[1]>({
3834
+ chain: [compactOptional<ParallelStep>({ parallel: parallelTasks, concurrency: parallelConcurrency, worktree: params.worktree })],
3835
+ resultMode: "parallel",
3836
+ goal: taskTexts[0] ?? "",
3837
+ agents,
3838
+ ctx: asyncCtx,
3839
+ availableModels,
3840
+ cwd: effectiveCwd,
3841
+ maxOutput: params.maxOutput,
3842
+ artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
3843
+ artifactConfig,
3844
+ shareEnabled,
3845
+ sessionRoot,
3846
+ chainSkills: [],
3847
+ activeAsyncCapacity: capacity.activeAsyncCapacity,
3848
+ sessionFilesByFlatIndex: tasks.map((task, index) => sessionFileForTask(task.agent, index, modelOverrides[index])),
3849
+ thinkingOverridesByFlatIndex: tasks.map((task, index) => thinkingOverrideForTask(task.agent, index, modelOverrides[index])),
3850
+ contextForAgent: contextPolicy.contextForAgent,
3851
+ maxSubagentDepth: currentMaxSubagentDepth,
3852
+ waitToolEnabled: deps.waitToolEnabled,
3853
+ worktreeSetupHook: deps.config.worktreeSetupHook,
3854
+ worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
3855
+ worktreeBaseDir: deps.config.worktreeBaseDir,
3856
+ controlConfig,
3857
+ agentContract: params.agentContract,
3858
+ controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
3859
+ childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
3860
+ nestedRoute: data.nestedRoute,
3861
+ timeoutMs: data.timeoutMs,
3862
+ turnBudget: data.turnBudget,
3863
+ usageBudget: data.usageBudget,
3864
+ toolBudget: data.toolBudget,
3865
+ configToolBudget: data.configToolBudget,
3866
+ globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
3867
+ runFanoutBudget: data.runFanoutBudget,
3868
+ }));
3869
+ } finally {
3870
+ finalizeClarifiedBackgroundCapacity(data, deps, capacity.activeAsyncCapacity);
3871
+ }
3488
3872
  }
3489
3873
  }
3490
3874
 
@@ -3563,6 +3947,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3563
3947
  intercomEvents: deps.pi.events,
3564
3948
  parentSessionId: data.parentSessionId,
3565
3949
  capabilityCeiling: data.capabilityCeiling,
3950
+ runFanoutBudget: data.runFanoutBudget,
3566
3951
  signal,
3567
3952
  runId,
3568
3953
  sessionDirForIndex,
@@ -3797,7 +4182,9 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3797
4182
  details: { mode: "single" as const, results: [] },
3798
4183
  };
3799
4184
  }
3800
- const id = randomUUID();
4185
+ const id = data.asyncRunId;
4186
+ const capacity = acquireClarifiedBackgroundCapacity(data, deps, "single", id);
4187
+ if (capacity.errorResult) return capacity.errorResult;
3801
4188
  const asyncCtx = compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
3802
4189
  pi: deps.pi,
3803
4190
  cwd: ctx.cwd,
@@ -3809,49 +4196,58 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3809
4196
  interactive: ctx.hasUI,
3810
4197
  permissions: deps.config.permissions,
3811
4198
  });
3812
- return executeAsyncSingle(id, compactOptional<Parameters<typeof executeAsyncSingle>[1]>({
3813
- agent: params.agent!,
3814
- task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(task) : task,
3815
- goal: task,
3816
- agentConfig,
3817
- ctx: asyncCtx,
3818
- availableModels,
3819
- cwd: effectiveCwd,
3820
- maxOutput: params.maxOutput,
3821
- artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
3822
- artifactConfig,
3823
- shareEnabled,
3824
- sessionRoot,
3825
- sessionFile: sessionFileForTask(params.agent!, 0, modelOverride),
3826
- context: contextPolicy.contextForAgent(params.agent!),
3827
- skills: skillOverride === false ? [] : skillOverride,
3828
- output: effectiveOutput,
3829
- outputMode: effectiveOutputMode,
3830
- ...(readsOverride !== undefined ? { reads: readsOverride } : {}),
3831
- outputBaseDir: resolveSingleRunOutputBaseDir(deps, artifactsDir, id),
3832
- modelOverride,
3833
- thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride),
3834
- maxSubagentDepth,
3835
- waitToolEnabled: deps.waitToolEnabled,
3836
- worktreeSetupHook: deps.config.worktreeSetupHook,
3837
- worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
3838
- worktreeBaseDir: deps.config.worktreeBaseDir,
3839
- controlConfig,
3840
- controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
3841
- childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
3842
- nestedRoute: data.nestedRoute,
3843
- agentContract: params.agentContract,
3844
- structuredOutputSchema: params.outputSchema,
3845
- acceptance: params.acceptance,
3846
- timeoutMs: data.timeoutMs,
3847
- turnBudget: data.turnBudget,
3848
- toolBudget: effectiveToolBudget.toolBudget,
3849
- usageBudget: data.usageBudget,
3850
- allowZeroToolBudget: data.allowZeroToolBudget && effectiveToolBudget.toolBudget === data.toolBudget,
3851
- }));
4199
+ try {
4200
+ return executeAsyncSingle(id, compactOptional<Parameters<typeof executeAsyncSingle>[1]>({
4201
+ agent: params.agent!,
4202
+ task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(task) : task,
4203
+ goal: task,
4204
+ agentConfig,
4205
+ recoveryAgentConfig: data.recoveryAgents.find((agent) => agent.name === params.agent),
4206
+ ctx: asyncCtx,
4207
+ availableModels,
4208
+ cwd: effectiveCwd,
4209
+ maxOutput: params.maxOutput,
4210
+ artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
4211
+ artifactConfig,
4212
+ shareEnabled,
4213
+ activeAsyncCapacity: capacity.activeAsyncCapacity,
4214
+ sessionRoot,
4215
+ sessionFile: sessionFileForTask(params.agent!, 0, modelOverride),
4216
+ context: contextPolicy.contextForAgent(params.agent!),
4217
+ skills: skillOverride === false ? [] : skillOverride,
4218
+ output: effectiveOutput,
4219
+ outputMode: effectiveOutputMode,
4220
+ ...(readsOverride !== undefined ? { reads: readsOverride } : {}),
4221
+ outputBaseDir: resolveSingleRunOutputBaseDir(deps, artifactsDir, id),
4222
+ modelOverride,
4223
+ thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride),
4224
+ maxSubagentDepth,
4225
+ waitToolEnabled: deps.waitToolEnabled,
4226
+ worktreeSetupHook: deps.config.worktreeSetupHook,
4227
+ worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
4228
+ worktreeBaseDir: deps.config.worktreeBaseDir,
4229
+ controlConfig,
4230
+ intercomBridge: params.intercomBridge,
4231
+ controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
4232
+ childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
4233
+ nestedRoute: data.nestedRoute,
4234
+ agentContract: params.agentContract,
4235
+ structuredOutputSchema: params.outputSchema,
4236
+ acceptance: params.acceptance,
4237
+ timeoutMs: data.timeoutMs,
4238
+ turnBudget: data.turnBudget,
4239
+ toolBudget: effectiveToolBudget.toolBudget,
4240
+ usageBudget: data.usageBudget,
4241
+ allowZeroToolBudget: data.allowZeroToolBudget && effectiveToolBudget.toolBudget === data.toolBudget,
4242
+ runFanoutBudget: data.runFanoutBudget,
4243
+ }));
4244
+ } finally {
4245
+ finalizeClarifiedBackgroundCapacity(data, deps, capacity.activeAsyncCapacity);
4246
+ }
3852
4247
  }
3853
4248
  }
3854
4249
 
4250
+ const authoredTask = task;
3855
4251
  if (shouldForkAgent(contextPolicy, params.agent!)) {
3856
4252
  task = wrapForkTask(task);
3857
4253
  }
@@ -3888,6 +4284,11 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3888
4284
  beginForegroundChild(foregroundControl, omitUndefinedProperties({
3889
4285
  index: 0,
3890
4286
  agent: params.agent!,
4287
+ authoredTask,
4288
+ effectivePrompt: task,
4289
+ cwd: effectiveCwd,
4290
+ outputPath,
4291
+ rerun: { params: { ...params, task: authoredTask, async: params.async ?? false } },
3891
4292
  description: foregroundControl.description,
3892
4293
  ...(modelOverride ? { model: modelOverride } : {}),
3893
4294
  ...(thinking ? { thinking } : {}),
@@ -3913,8 +4314,10 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3913
4314
  r = await runSync(ctx.cwd, agents, params.agent!, task, compactOptional<Parameters<typeof runSync>[4]>({
3914
4315
  permissions: deps.config.permissions,
3915
4316
  parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
4317
+ llmIntentArbiter: createTaskMutationArbiter(ctx),
3916
4318
  ...workflowForegroundSteeringLaunchOptions(foregroundControl, 0),
3917
4319
  context: data.contextPolicy.contextForAgent(params.agent!),
4320
+ runFanoutBudget: params.runFanoutAdmitted ? data.runFanoutBudget : { ...data.runFanoutBudget, parentPath: `${data.runFanoutBudget.parentPath ? `${data.runFanoutBudget.parentPath}/` : ""}single` },
3918
4321
  cwd: effectiveCwd,
3919
4322
  signal,
3920
4323
  interruptSignal: interruptController.signal,
@@ -3948,6 +4351,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3948
4351
  agentContract: params.agentContract,
3949
4352
  acceptance: params.acceptance,
3950
4353
  acceptanceContext: { mode: "single" },
4354
+ onEffectivePrompt: foregroundControl ? (prompt) => updateLiveEffectivePrompt(foregroundControl, 0, prompt) : undefined,
3951
4355
  onDetachReady: (detach) => {
3952
4356
  detachForeground = detach;
3953
4357
  },
@@ -3996,12 +4400,14 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3996
4400
  if (r.artifactPaths) allArtifactPaths.push(r.artifactPaths);
3997
4401
 
3998
4402
  const fullOutput = getSingleResultOutput(r);
4403
+ const preserveRejectedSavedOutput = r.acceptance?.explicit && r.acceptance.status === "rejected" && r.savedOutputPath !== undefined;
3999
4404
  const finalizedOutput = finalizeSingleOutput(omitUndefinedProperties({
4000
4405
  fullOutput,
4001
4406
  truncatedOutput: r.truncation?.text,
4002
4407
  outputPath,
4003
4408
  outputMode: r.outputMode,
4004
4409
  exitCode: r.exitCode,
4410
+ preserveSavedOutput: preserveRejectedSavedOutput,
4005
4411
  savedPath: r.savedOutputPath,
4006
4412
  outputReference: r.outputReference,
4007
4413
  saveError: r.outputSaveError,
@@ -4092,7 +4498,7 @@ function duplicateSubagentCallResult(params: SubagentParamsLike): AgentToolResul
4092
4498
  };
4093
4499
  }
4094
4500
 
4095
- const workflowLaunchObservers = new WeakMap<object, (launch: { agent: string; sessionFile?: string }) => void>();
4501
+ const workflowLaunchObservers = new WeakMap<object, (launch: { agent: string; sessionFile?: string; async: boolean; runId?: string }) => void>();
4096
4502
 
4097
4503
  function recordMissionWorkflowChild(
4098
4504
  binding: MissionLaunchBinding | undefined,
@@ -4101,8 +4507,9 @@ function recordMissionWorkflowChild(
4101
4507
  update: Omit<MissionWorkflowChildUpdate, "workflowRunId" | "key">,
4102
4508
  ): void {
4103
4509
  if (!binding) return;
4510
+ const { task: _task, ...durableUpdate } = update;
4104
4511
  try {
4105
- updateMission(binding.location, binding.missionId, { upsertWorkflowChildren: [{ workflowRunId, key, ...update }] });
4512
+ updateMission(binding.location, binding.missionId, { upsertWorkflowChildren: [{ workflowRunId, key, ...durableUpdate }] });
4106
4513
  } catch (error) {
4107
4514
  console.warn(`[pi-subagents] Failed to record mission workflow child '${key}': ${error instanceof Error ? error.message : String(error)}`);
4108
4515
  }
@@ -4184,7 +4591,7 @@ export function prepareWorkflowLaunchParams(
4184
4591
  childParams: Record<string, unknown>,
4185
4592
  parentWorkflowRunId: string,
4186
4593
  workflowKey: string,
4187
- options: { missionDetached?: boolean; suppressRoutineResultIntercom?: boolean } = {},
4594
+ options: { missionDetached?: boolean; suppressRoutineResultIntercom?: boolean; runFanoutBudget?: RunFanoutBudgetDescriptor } = {},
4188
4595
  ): SubagentParamsLike {
4189
4596
  if (typeof childParams.resume === "string") {
4190
4597
  if (childParams.gate !== undefined || workflowDefaults.gate !== undefined) {
@@ -4193,16 +4600,19 @@ export function prepareWorkflowLaunchParams(
4193
4600
  const timeoutMs = childParams.timeoutMs ?? childParams.maxRuntimeMs ?? workflowDefaults.timeoutMs ?? workflowDefaults.maxRuntimeMs;
4194
4601
  const turnBudget = childParams.turnBudget ?? workflowDefaults.turnBudget;
4195
4602
  const toolBudget = childParams.toolBudget ?? workflowDefaults.toolBudget;
4603
+ const intercomBridge = childParams.intercomBridge ?? workflowDefaults.intercomBridge;
4196
4604
  return {
4197
4605
  action: "resume",
4198
4606
  id: childParams.resume.trim(),
4199
4607
  message: typeof childParams.task === "string" ? childParams.task.trim() : "",
4200
4608
  workflowParentRunId: parentWorkflowRunId,
4201
4609
  workflowKey,
4610
+ ...(options.runFanoutBudget ? { runFanoutBudget: { ...options.runFanoutBudget, parentPath: `${options.runFanoutBudget.parentPath ? `${options.runFanoutBudget.parentPath}/` : ""}workflow[${workflowKey}]` } } : {}),
4202
4611
  ...(options.missionDetached ? { mission: false } : {}),
4203
4612
  ...(timeoutMs !== undefined ? { timeoutMs: timeoutMs as number } : {}),
4204
4613
  ...(turnBudget !== undefined ? { turnBudget: turnBudget as TurnBudgetConfig } : {}),
4205
4614
  ...(toolBudget !== undefined ? { toolBudget: toolBudget as ToolBudgetConfig } : {}),
4615
+ ...(intercomBridge !== undefined ? { intercomBridge: intercomBridge as IntercomBridgeConfig } : {}),
4206
4616
  };
4207
4617
  }
4208
4618
  const launchParams = {
@@ -4212,6 +4622,7 @@ export function prepareWorkflowLaunchParams(
4212
4622
  ...(options.missionDetached ? { mission: false } : {}),
4213
4623
  workflowParentRunId: parentWorkflowRunId,
4214
4624
  workflowKey,
4625
+ ...(options.runFanoutBudget ? { runFanoutBudget: { ...options.runFanoutBudget, parentPath: `${options.runFanoutBudget.parentPath ? `${options.runFanoutBudget.parentPath}/` : ""}workflow[${workflowKey}]` } } : {}),
4215
4626
  ...(options.suppressRoutineResultIntercom ? { suppressRoutineResultIntercom: true } : {}),
4216
4627
  } as SubagentParamsLike;
4217
4628
  const normalizedGate = normalizeGateParams(launchParams);
@@ -4359,6 +4770,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4359
4770
  } {
4360
4771
  const delegatedThinkingOverrides = new WeakMap<object, AgentConfig["thinking"]>();
4361
4772
  const delegatedZeroToolBudgets = new WeakSet<object>();
4773
+ const delegatedExecutions = new WeakSet<object>();
4362
4774
  const warnedArtifactPackageDirs = new Set<string>();
4363
4775
  const scheduledOwnerExecutors = new Map<string, ReturnType<typeof createSubagentExecutor>>();
4364
4776
  const execute = async (
@@ -4372,6 +4784,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4372
4784
  const workflowLaunchObserver = workflowLaunchObservers.get(params);
4373
4785
  const delegatedThinkingOverride = delegatedThinkingOverrides.get(params);
4374
4786
  const allowZeroToolBudget = delegatedZeroToolBudgets.has(params);
4787
+ const delegatedExecution = delegatedExecutions.has(params);
4375
4788
  if (!preserveActiveSession) deps.state.baseCwd = ctx.cwd;
4376
4789
  deps.state.foregroundRuns ??= new Map();
4377
4790
  deps.state.foregroundControls ??= new Map();
@@ -4382,10 +4795,21 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4382
4795
  const normalizedAction = typeof requestParams.action === "string" ? requestParams.action.trim() : requestParams.action;
4383
4796
  if (requestParams.workflowScript !== undefined && normalizedAction === undefined) {
4384
4797
  const parentCwd = ctx.cwd;
4385
- const timeout = requestParams.timeoutMs ?? requestParams.maxRuntimeMs ?? (requestParams.async === false ? DEFAULT_FOREGROUND_TIMEOUT_MS : undefined);
4798
+ const timeout = requestParams.timeoutMs ?? requestParams.maxRuntimeMs ?? (requestParams.async === false ? resolveConfigDefaultTimeoutMs(deps.config.timeoutMs) ?? DEFAULT_FOREGROUND_TIMEOUT_MS : undefined);
4386
4799
  const workflowUsageBudget = validateUsageBudgetConfig(requestParams.usageBudget ?? deps.config.usageBudget, requestParams.usageBudget ? "usageBudget" : "config.usageBudget");
4387
4800
  if (workflowUsageBudget.error) return buildRequestedModeError(requestParams, workflowUsageBudget.error);
4388
4801
  const workflowCwd = resolveRequestedCwd(parentCwd, requestParams.cwd);
4802
+ const workflowArtifactConfig: ArtifactConfig = omitUndefinedProperties({
4803
+ ...DEFAULT_ARTIFACT_CONFIG,
4804
+ enabled: requestParams.artifacts !== false,
4805
+ dir: deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir,
4806
+ });
4807
+ const workflowArtifactsDir = getArtifactsDir(ctx.sessionManager.getSessionFile() ?? null, workflowCwd, workflowArtifactConfig.dir);
4808
+ if (workflowArtifactConfig.dir === "project" && !warnedArtifactPackageDirs.has(workflowCwd)) {
4809
+ warnedArtifactPackageDirs.add(workflowCwd);
4810
+ const warning = getProjectArtifactPackagingWarning(workflowCwd);
4811
+ if (warning) console.warn(`[pi-subagents] ${warning}`);
4812
+ }
4389
4813
  const workflowPrompts = { render: (ref: string, vars?: unknown) => renderWorkflowPrompt(ref, vars, workflowCwd) };
4390
4814
  const chatProgressResult = resolveWorkflowChatProgress({ requested: requestParams.chatProgress, parentCwd, workflowCwd, background: requestParams.async !== false });
4391
4815
  if (chatProgressResult.error) return { content: [{ type: "text", text: chatProgressResult.error }], isError: true, details: { mode: "workflow", results: [] } };
@@ -4393,11 +4817,45 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4393
4817
  const explicitMission = requestParams.missionId !== undefined || requestParams.mission !== undefined;
4394
4818
  const autoMission = !explicitMission;
4395
4819
  const workflowPreview = autoMission ? previewSimpleWorkflowRun(requestParams.workflowScript) : undefined;
4396
- const previewTask = workflowPreview?.task?.trim() || undefined;
4397
4820
  const previewAgent = workflowPreview?.agent?.trim() || undefined;
4398
4821
  const scriptFirstLine = requestParams.workflowScript.split(/\r?\n/).map((line) => line.trim()).find(Boolean) || "Workflow";
4399
4822
  const boundedScriptPreview = scriptFirstLine.length > 100 ? `${scriptFirstLine.slice(0, 97)}...` : scriptFirstLine;
4400
- const derivedObjective = previewTask || (previewAgent ? `Workflow: ${previewAgent}` : boundedScriptPreview);
4823
+ const derivedObjective = previewAgent ? `Workflow: ${previewAgent}` : boundedScriptPreview;
4824
+ const workflowDepth = checkSubagentDepth(deps.config.maxSubagentDepth).depth;
4825
+ const asyncWorkflow = requestParams.async !== false;
4826
+ const topLevelAsyncWorkflow = asyncWorkflow
4827
+ && workflowDepth === 0
4828
+ && !resolveInheritedNestedRouteFromEnv()
4829
+ && !requestParams.workflowParentRunId;
4830
+ const workflowRunId = asyncWorkflow ? randomUUID() : undefined;
4831
+ let workflowCapacity: ActiveAsyncCapacityHandle | undefined;
4832
+ if (workflowRunId && topLevelAsyncWorkflow) {
4833
+ const currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
4834
+ try {
4835
+ workflowCapacity = acquireActiveAsyncCapacity({
4836
+ sessionId: currentSessionId,
4837
+ limit: resolveMaxActiveAsyncRunsPerSession(deps.config.maxActiveAsyncRunsPerSession),
4838
+ runId: workflowRunId,
4839
+ kind: "workflow",
4840
+ asyncDir: path.join(DIRS.async, workflowRunId),
4841
+ }, { liveWorkflowRunIds: new Set(deps.state.workflowControllers?.keys() ?? []) });
4842
+ } catch (error) {
4843
+ if (error instanceof ActiveAsyncCapacityError) {
4844
+ deps.state.activeAsyncCapacity = error.snapshot;
4845
+ return { content: [{ type: "text", text: error.message }], isError: true, details: { mode: "workflow", results: [], activeAsyncCapacity: error.snapshot } };
4846
+ }
4847
+ throw error;
4848
+ }
4849
+ }
4850
+ let workflowFanoutBudget: RunFanoutBudgetDescriptor;
4851
+ try {
4852
+ workflowFanoutBudget = requestParams.runFanoutBudget
4853
+ ?? decodeRunFanoutBudgetDescriptor(process.env[RUN_FANOUT_BUDGET_ENV])
4854
+ ?? createRunFanoutBudget(_id, resolveMaxSubagentSpawnsPerRun(deps.config.maxSubagentSpawnsPerRun));
4855
+ } catch (error) {
4856
+ workflowCapacity?.rollback();
4857
+ return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, details: { mode: "workflow", results: [] } };
4858
+ }
4401
4859
  let missionBinding: MissionLaunchBinding | undefined;
4402
4860
  let missionWarning: string | undefined;
4403
4861
  try {
@@ -4408,22 +4866,12 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4408
4866
  ownerSessionId: resolveCurrentSessionId(ctx.sessionManager),
4409
4867
  });
4410
4868
  } catch (error) {
4411
- if (explicitMission) return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, details: { mode: "workflow", results: [] } };
4869
+ if (explicitMission) {
4870
+ workflowCapacity?.rollback();
4871
+ return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, details: { mode: "workflow", results: [] } };
4872
+ }
4412
4873
  missionWarning = `Mission tracking unavailable: ${error instanceof Error ? error.message : String(error)}`;
4413
4874
  }
4414
- let shouldPatchMissionObjective = autoMission && previewTask === undefined && missionBinding !== undefined;
4415
- const patchMissionObjective = (task: unknown): void => {
4416
- if (!shouldPatchMissionObjective || !missionBinding || typeof task !== "string" || !task.trim()) return;
4417
- shouldPatchMissionObjective = false;
4418
- const objective = task.trim();
4419
- const firstLine = objective.split(/\r?\n/, 1)[0]?.trim() || objective;
4420
- const title = firstLine.length > 100 ? `${firstLine.slice(0, 97)}...` : firstLine;
4421
- try {
4422
- updateMission(missionBinding.location, missionBinding.missionId, { title, objective });
4423
- } catch (error) {
4424
- console.warn(`[pi-subagents] Failed to update automatic mission objective: ${error instanceof Error ? error.message : String(error)}`);
4425
- }
4426
- };
4427
4875
  const detachWorkflowChildMissions = autoMission || missionBinding !== undefined || requestParams.mission === false;
4428
4876
  const workflowState = missionBinding ? createMissionWorkflowState(missionBinding.location, missionBinding.missionId) : undefined;
4429
4877
  const attachWorkflowMission = (result: AgentToolResult<Details>): AgentToolResult<Details> => {
@@ -4437,20 +4885,27 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4437
4885
  : { ...result, details: { ...result.details, missionWarning: warning } };
4438
4886
  }
4439
4887
  };
4440
- if (requestParams.async !== false) {
4888
+ if (workflowRunId) {
4441
4889
  const toolCallId = _id;
4442
- const workflowRunId = randomUUID();
4443
4890
  const asyncDir = path.join(DIRS.async, workflowRunId);
4444
4891
  const resultPath = path.join(DIRS.results, `${workflowRunId}.json`);
4445
4892
  const statusPath = path.join(asyncDir, "status.json");
4446
4893
  const eventsPath = path.join(asyncDir, "events.jsonl");
4447
4894
  const startedAt = Date.now();
4448
4895
  const currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
4449
- fs.mkdirSync(asyncDir, { recursive: true });
4450
- fs.mkdirSync(DIRS.results, { recursive: true });
4896
+ try {
4897
+ fs.mkdirSync(asyncDir, { recursive: true });
4898
+ writeRunFanoutBudgetDescriptor(asyncDir, workflowFanoutBudget);
4899
+ fs.mkdirSync(DIRS.results, { recursive: true });
4900
+ } catch (error) {
4901
+ workflowCapacity?.rollback();
4902
+ return { content: [{ type: "text", text: `Failed to create async workflow storage: ${error instanceof Error ? error.message : String(error)}` }], isError: true, details: { mode: "workflow", results: [] } };
4903
+ }
4451
4904
  const controller = new AbortController();
4452
4905
  deps.state.workflowControllers ??= new Map();
4453
4906
  deps.state.workflowControllers.set(workflowRunId, controller);
4907
+ workflowCapacity?.markWorkflowStarted();
4908
+ if (workflowCapacity) deps.state.activeAsyncCapacity = getActiveAsyncCapacitySnapshot(currentSessionId, resolveMaxActiveAsyncRunsPerSession(deps.config.maxActiveAsyncRunsPerSession), { liveWorkflowRunIds: new Set(deps.state.workflowControllers.keys()) });
4454
4909
  let status: AsyncStatus = {
4455
4910
  runId: workflowRunId,
4456
4911
  toolCallId,
@@ -4464,6 +4919,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4464
4919
  pid: process.pid,
4465
4920
  steps: [],
4466
4921
  workflow: { trace: [], emits: [], console: [] },
4922
+ runFanoutBudget: getRunFanoutBudgetSnapshot(workflowFanoutBudget),
4467
4923
  };
4468
4924
  const appendWorkflowEvent = (event: Record<string, unknown>) => fs.appendFileSync(eventsPath, `${JSON.stringify({ ts: Date.now(), runId: workflowRunId, ...event })}\n`, "utf-8");
4469
4925
  let indexedState: AsyncStatus["state"] | undefined;
@@ -4527,6 +4983,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4527
4983
  void Promise.resolve().then(async () => {
4528
4984
  const workflowResults: SingleResult[] = [];
4529
4985
  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;
4986
+ const workflowOutput = typeof workflowChildDefaults.output === "string" || typeof workflowChildDefaults.output === "boolean" ? workflowChildDefaults.output : undefined;
4987
+ const configuredOutputBaseDir = resolveConfiguredSingleRunOutputBaseDir(deps);
4988
+ const workflowAggregateOutputPath = resolveWorkflowAggregateOutputPath(workflowOutput, ctx.cwd, workflowCwd, resolveSingleRunOutputBaseDir(deps, workflowArtifactsDir, workflowRunId), configuredOutputBaseDir);
4989
+ const claimedOutputPaths = new Map<string, string>();
4530
4990
  const workflowSteps = new Map<string, NonNullable<AsyncStatus["steps"]>[number]>();
4531
4991
  let projectedTraceLength = 0;
4532
4992
  let projectedTraceTail: NonNullable<Details["workflow"]>["trace"][number] | undefined;
@@ -4546,16 +5006,36 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4546
5006
  if (entry.operation !== "run") continue;
4547
5007
  const existing = workflowSteps.get(entry.key);
4548
5008
  if (entry.state === "reused" && existing) continue;
4549
- const mapped = entry.state === "started" || entry.state === "reused" ? "running" : entry.state === "completed" ? "completed" : "failed";
5009
+ const mapped = entry.state === "started" || entry.state === "reused"
5010
+ ? "running"
5011
+ : entry.state === "completed"
5012
+ ? "completed"
5013
+ : entry.state === "stopped"
5014
+ ? "stopped"
5015
+ : "failed";
4550
5016
  if (existing) {
4551
5017
  existing.status = mapped;
4552
5018
  if (entry.agent) existing.agent = entry.agent;
5019
+ if (entry.runId) existing.runId = entry.runId;
5020
+ if (entry.state === "failed" && !entry.runId && existing.async === undefined) existing.async = false;
5021
+ if (entry.state === "stopped") existing.stopped = true;
5022
+ else delete existing.stopped;
4553
5023
  if (entry.error === undefined) delete existing.error;
4554
5024
  else existing.error = entry.error;
4555
5025
  if (entry.durationMs === undefined) delete existing.durationMs;
4556
5026
  else existing.durationMs = entry.durationMs;
4557
5027
  } else {
4558
- const step: NonNullable<AsyncStatus["steps"]>[number] = { agent: entry.agent ?? entry.key, label: entry.key, workflowKey: entry.key, parentWorkflowRunId: workflowRunId, status: mapped, startedAt: Date.now() };
5028
+ const step: NonNullable<AsyncStatus["steps"]>[number] = {
5029
+ agent: entry.agent ?? entry.key,
5030
+ label: entry.key,
5031
+ workflowKey: entry.key,
5032
+ parentWorkflowRunId: workflowRunId,
5033
+ status: mapped,
5034
+ startedAt: Date.now(),
5035
+ ...(entry.runId ? { runId: entry.runId } : {}),
5036
+ ...(entry.state === "failed" && !entry.runId ? { async: false } : {}),
5037
+ ...(entry.state === "stopped" ? { stopped: true } : {}),
5038
+ };
4559
5039
  status.steps?.push(step);
4560
5040
  workflowSteps.set(entry.key, step);
4561
5041
  }
@@ -4574,30 +5054,35 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4574
5054
  prompts: workflowPrompts,
4575
5055
  ...(workflowState ? { state: workflowState } : {}),
4576
5056
  onTrace: updateTrace,
5057
+ admit: (calls) => {
5058
+ const outputClaims = workflowChildOutputClaims({ ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, workflowRunId, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: deps.discoverAgents, workflowAgentScope: workflowChildDefaults.agentScope, claimedOutputPaths, entries: calls });
5059
+ if (outputClaims.error) throw new Error(outputClaims.error);
5060
+ status.runFanoutBudget = claimRunFanoutBatch(workflowFanoutBudget, calls.map(({ key }) => `workflow[${key}]`));
5061
+ if (outputClaims.claims) applyWorkflowChildOutputClaims(claimedOutputPaths, outputClaims.claims);
5062
+ persist();
5063
+ },
4577
5064
  onEmit: (emits) => {
4578
5065
  // Each emit is validated at the host boundary in runWorkflowScript before onEmit fires.
4579
5066
  status.workflow = { ...(status.workflow ?? { trace: [], console: [] }), emits };
4580
5067
  persist();
4581
5068
  appendWorkflowEvent({ type: "subagent.workflow.emit", value: emits.at(-1) });
4582
5069
  },
4583
- launch: async (key, childParams, workflowSignal) => {
5070
+ launch: async (key, childParams, workflowSignal, admission) => {
4584
5071
  if (workflowUsageBudget.budget && childParams.async === true) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, "workflow usageBudget does not support async runs.run launches."));
4585
5072
  const budgetState = usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults));
4586
5073
  if (budgetState?.exhausted) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, usageBudgetExceededMessage(budgetState)));
4587
- patchMissionObjective(childParams.task);
4588
5074
  const childPhase = typeof childParams.phase === "string" && childParams.phase.trim() ? childParams.phase.trim() : undefined;
4589
5075
  const childLabel = typeof childParams.label === "string" && childParams.label.trim() ? childParams.label.trim() : undefined;
4590
5076
  recordMissionWorkflowChild(missionBinding, workflowRunId, key, {
4591
5077
  status: "running",
4592
5078
  ...(typeof childParams.agent === "string" && childParams.agent.trim() ? { agent: childParams.agent.trim() } : {}),
4593
- ...(typeof childParams.task === "string" && childParams.task.trim() ? { task: childParams.task.trim() } : {}),
4594
5079
  ...(childLabel ? { label: childLabel } : {}),
4595
5080
  ...(childPhase ? { phase: childPhase } : {}),
4596
5081
  heartbeat: { status: "running", ...(childPhase ? { phase: childPhase } : {}) },
4597
5082
  });
4598
5083
  const result = await runMissionWorkflowChild(missionBinding, workflowRunId, key, childPhase, () => {
4599
5084
  const childRequest = bindMissionWorkflowChildAsyncLaunch(
4600
- prepareWorkflowLaunchParams(workflowChildDefaults, childParams, workflowRunId, key, { missionDetached: detachWorkflowChildMissions }),
5085
+ { ...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 },
4601
5086
  missionBinding,
4602
5087
  deps.asyncByDefault,
4603
5088
  );
@@ -4606,6 +5091,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4606
5091
  if (step) {
4607
5092
  step.agent = launch.agent;
4608
5093
  step.sessionFile = launch.sessionFile;
5094
+ step.async = launch.async;
5095
+ if (launch.runId) step.runId = launch.runId;
4609
5096
  persist();
4610
5097
  }
4611
5098
  recordMissionWorkflowChild(missionBinding, workflowRunId, key, { status: "running", agent: launch.agent, ...(launch.sessionFile ? { sessionPath: launch.sessionFile } : {}) });
@@ -4613,7 +5100,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4613
5100
  return execute(randomUUID(), childRequest, workflowSignal, (update) => {
4614
5101
  const progress = update.details.progress?.[0];
4615
5102
  const step = status.steps?.find((candidate) => candidate.workflowKey === key);
4616
- if (!progress || !step) return;
5103
+ if (!progress || !step || step.stopped) return;
4617
5104
  step.status = progress.status === "completed" ? "completed" : progress.status === "failed" ? "failed" : "running";
4618
5105
  step.activityState = progress.activityState;
4619
5106
  step.lastActivityAt = progress.lastActivityAt;
@@ -4637,8 +5124,13 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4637
5124
  }, ctx, preserveActiveSession);
4638
5125
  });
4639
5126
  workflowResults.push(...result.details.results);
4640
- if (result.details.asyncDir && missionBinding) writeMissionAsyncBinding(result.details.asyncDir, missionBinding);
4641
5127
  const child = workflowChildResult(key, result);
5128
+ const step = status.steps?.find((candidate) => candidate.workflowKey === key);
5129
+ if (step) {
5130
+ step.async = Boolean(result.details.asyncId || result.details.asyncDir);
5131
+ if (child.runId) step.runId = child.runId;
5132
+ }
5133
+ if (result.details.asyncDir && missionBinding) writeMissionAsyncBinding(result.details.asyncDir, missionBinding);
4642
5134
  const childStatus = missionWorkflowChildStatus(result);
4643
5135
  recordMissionWorkflowChild(missionBinding, workflowRunId, key, {
4644
5136
  status: childStatus,
@@ -4660,28 +5152,38 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4660
5152
  const returnPreview = formatWorkflowValue(workflow.value).slice(0, 1_000);
4661
5153
  const emitPreview = workflow.emits.length > 0 ? ` Emitted: ${workflow.emits.map(formatWorkflowValue).join(", ").slice(0, 1_000)}` : "";
4662
5154
  const summary = `Workflow completed with ${workflow.children.length} child run(s). Return: ${returnPreview}${emitPreview} Trace: ${workflow.trace.length} event(s).`;
5155
+ const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, summary);
5156
+ const resultSummary = appendWorkflowOutputWarning(summary, outputWarning);
4663
5157
  const workflowUsage = sumResultsUsage(workflowResults);
4664
5158
  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) };
4665
- 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 });
5159
+ writeAtomicJson(resultPath, { 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 });
4666
5160
  persist();
4667
5161
  appendWorkflowEvent({ type: "subagent.workflow.completed", state: "complete" });
4668
5162
  } catch (error) {
4669
5163
  const partial = error instanceof WorkflowScriptError ? error.partial : { trace: [], emits: [], console: [], children: [] };
4670
5164
  const stopped = controller.signal.aborted;
4671
5165
  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 } });
4672
- 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 });
5166
+ const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, status.error ?? "Workflow failed.");
5167
+ const resultSummary = appendWorkflowOutputWarning(status.error ?? "Workflow failed.", outputWarning);
5168
+ writeAtomicJson(resultPath, { id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: false, state: status.state, summary: resultSummary, 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 });
4673
5169
  persist();
4674
5170
  appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, error: status.error });
4675
5171
  } finally {
4676
5172
  deps.state.workflowControllers?.delete(workflowRunId);
5173
+ deps.state.activeAsyncCapacity = workflowCapacity?.reconcile(new Set(deps.state.workflowControllers?.keys() ?? []))
5174
+ ?? deps.state.activeAsyncCapacity;
4677
5175
  }
4678
5176
  });
4679
- return attachWorkflowMission({
5177
+ return attachWorkflowMission(withRunFanoutBudget({
4680
5178
  content: [{ type: "text", text: formatAsyncStartedMessage(`Async workflow [${workflowRunId}]`, ctx.hasUI === true) }],
4681
- details: { mode: "workflow", runId: workflowRunId, toolCallId, asyncId: workflowRunId, asyncDir, results: [], chatProgress },
4682
- });
5179
+ details: { mode: "workflow", runId: workflowRunId, toolCallId, asyncId: workflowRunId, asyncDir, results: [], chatProgress, ...(deps.state.activeAsyncCapacity ? { activeAsyncCapacity: deps.state.activeAsyncCapacity } : {}) },
5180
+ }, workflowFanoutBudget));
4683
5181
  }
4684
5182
  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;
5183
+ const workflowOutput = typeof workflowChildDefaults.output === "string" || typeof workflowChildDefaults.output === "boolean" ? workflowChildDefaults.output : undefined;
5184
+ const configuredOutputBaseDir = resolveConfiguredSingleRunOutputBaseDir(deps);
5185
+ const workflowAggregateOutputPath = resolveWorkflowAggregateOutputPath(workflowOutput, ctx.cwd, workflowCwd, resolveSingleRunOutputBaseDir(deps, workflowArtifactsDir, _id), configuredOutputBaseDir);
5186
+ const claimedOutputPaths = new Map<string, string>();
4685
5187
  const workflowResults: SingleResult[] = [];
4686
5188
  let liveWorkflow: NonNullable<Details["workflow"]> = { trace: [], emits: [], console: [] };
4687
5189
  const sendWorkflowProgress = () => {
@@ -4699,28 +5201,32 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4699
5201
  liveWorkflow = { ...liveWorkflow, trace };
4700
5202
  sendWorkflowProgress();
4701
5203
  },
5204
+ admit: (calls) => {
5205
+ const outputClaims = workflowChildOutputClaims({ ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, workflowRunId: _id, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: deps.discoverAgents, workflowAgentScope: workflowChildDefaults.agentScope, claimedOutputPaths, entries: calls });
5206
+ if (outputClaims.error) throw new Error(outputClaims.error);
5207
+ claimRunFanoutBatch(workflowFanoutBudget, calls.map(({ key }) => `workflow[${key}]`));
5208
+ if (outputClaims.claims) applyWorkflowChildOutputClaims(claimedOutputPaths, outputClaims.claims);
5209
+ },
4702
5210
  onEmit: (emits) => {
4703
5211
  liveWorkflow = { ...liveWorkflow, emits };
4704
5212
  sendWorkflowProgress();
4705
5213
  },
4706
- launch: async (key, childParams, workflowSignal) => {
5214
+ launch: async (key, childParams, workflowSignal, admission) => {
4707
5215
  if (workflowUsageBudget.budget && childParams.async === true) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, "workflow usageBudget does not support async runs.run launches."));
4708
5216
  const budgetState = usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults));
4709
5217
  if (budgetState?.exhausted) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, usageBudgetExceededMessage(budgetState)));
4710
- patchMissionObjective(childParams.task);
4711
5218
  const childPhase = typeof childParams.phase === "string" && childParams.phase.trim() ? childParams.phase.trim() : undefined;
4712
5219
  const childLabel = typeof childParams.label === "string" && childParams.label.trim() ? childParams.label.trim() : undefined;
4713
5220
  recordMissionWorkflowChild(missionBinding, _id, key, {
4714
5221
  status: "running",
4715
5222
  ...(typeof childParams.agent === "string" && childParams.agent.trim() ? { agent: childParams.agent.trim() } : {}),
4716
- ...(typeof childParams.task === "string" && childParams.task.trim() ? { task: childParams.task.trim() } : {}),
4717
5223
  ...(childLabel ? { label: childLabel } : {}),
4718
5224
  ...(childPhase ? { phase: childPhase } : {}),
4719
5225
  heartbeat: { status: "running", ...(childPhase ? { phase: childPhase } : {}) },
4720
5226
  });
4721
5227
  const result = await runMissionWorkflowChild(missionBinding, _id, key, childPhase, () => {
4722
5228
  const childRequest = bindMissionWorkflowChildAsyncLaunch(
4723
- prepareWorkflowLaunchParams(workflowChildDefaults, childParams, _id, key, { missionDetached: detachWorkflowChildMissions, suppressRoutineResultIntercom: chatProgress.mode === "live-card" }),
5229
+ { ...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 },
4724
5230
  missionBinding,
4725
5231
  deps.asyncByDefault,
4726
5232
  );
@@ -4761,10 +5267,13 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4761
5267
  if (workflow.emits.length > 0) sections.push(`Emitted:\n${workflow.emits.map(formatWorkflowValue).join("\n")}`);
4762
5268
  if (workflow.console.length > 0) sections.push(`Console:\n${workflow.console.map((entry) => `[${entry.level}] ${entry.text}`).join("\n")}`);
4763
5269
  if (traceLines.length > 0) sections.push(`Call trace:\n${traceLines.join("\n")}`);
4764
- return attachWorkflowMission({
4765
- content: [{ type: "text", text: sections.join("\n\n") }],
5270
+ const workflowText = sections.join("\n\n");
5271
+ const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, workflowText);
5272
+ const displayText = appendWorkflowOutputWarning(workflowText, outputWarning);
5273
+ return attachWorkflowMission(withRunFanoutBudget({
5274
+ content: [{ type: "text", text: displayText }],
4766
5275
  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 }),
4767
- });
5276
+ }, workflowFanoutBudget));
4768
5277
  } catch (error) {
4769
5278
  const partial = error instanceof WorkflowScriptError ? error.partial : { trace: [], emits: [], console: [], children: [] };
4770
5279
  const text = error instanceof Error ? error.message : String(error);
@@ -4773,11 +5282,14 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4773
5282
  if (partial.emits.length > 0) sections.push(`Emitted:\n${partial.emits.map(formatWorkflowValue).join("\n")}`);
4774
5283
  if (partial.console.length > 0) sections.push(`Console:\n${partial.console.map((entry) => `[${entry.level}] ${entry.text}`).join("\n")}`);
4775
5284
  if (traceLines.length > 0) sections.push(`Call trace:\n${traceLines.join("\n")}`);
4776
- return attachWorkflowMission({
4777
- content: [{ type: "text", text: sections.join("\n\n") }],
5285
+ const workflowText = sections.join("\n\n");
5286
+ const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, workflowText);
5287
+ const displayText = appendWorkflowOutputWarning(workflowText, outputWarning);
5288
+ return attachWorkflowMission(withRunFanoutBudget({
5289
+ content: [{ type: "text", text: displayText }],
4778
5290
  isError: true,
4779
5291
  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 }),
4780
- });
5292
+ }, workflowFanoutBudget));
4781
5293
  }
4782
5294
  }
4783
5295
  const directParams = prepareWorkflowChildParams(requestParams);
@@ -5015,6 +5527,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5015
5527
  if (!sessionError) sessionError = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
5016
5528
  }
5017
5529
  const spawnBudget = getSpawnBudgetSnapshot(deps.state, deps.config, currentSessionId);
5530
+ const activeAsyncCapacity = currentSessionId
5531
+ ? getActiveAsyncCapacitySnapshot(currentSessionId, resolveMaxActiveAsyncRunsPerSession(deps.config.maxActiveAsyncRunsPerSession), { liveWorkflowRunIds: new Set(deps.state.workflowControllers?.keys() ?? []) })
5532
+ : { used: 0, limit: resolveMaxActiveAsyncRunsPerSession(deps.config.maxActiveAsyncRunsPerSession) ?? 0 };
5533
+ deps.state.activeAsyncCapacity = activeAsyncCapacity;
5018
5534
  return {
5019
5535
  content: [{
5020
5536
  type: "text",
@@ -5031,14 +5547,14 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5031
5547
  expandTilde: deps.expandTilde,
5032
5548
  })),
5033
5549
  }],
5034
- details: { mode: "management", results: [], spawnBudget },
5550
+ details: { mode: "management", results: [], spawnBudget, activeAsyncCapacity },
5035
5551
  };
5036
5552
  }
5037
- if (action === "status") {
5553
+ if (action === "status" || action === "debug.run") {
5038
5554
  if (!preserveActiveSession) deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
5039
5555
  const targetRunId = paramsWithResolvedCwd.id ?? paramsWithResolvedCwd.runId;
5040
5556
  const hasDirectoryTarget = Boolean(paramsWithResolvedCwd.dir);
5041
- const targetLabel = formatStatusTargetLabel(paramsWithResolvedCwd, targetRunId);
5557
+ const targetLabel = action === "debug.run" ? "Debug run" : formatStatusTargetLabel(paramsWithResolvedCwd, targetRunId);
5042
5558
  const withBudget = (result: AgentToolResult<Details>) => {
5043
5559
  const budgeted = withSpawnBudgetStatus(result, deps.state, deps.config, deps.state.currentSessionId);
5044
5560
  return {
@@ -5050,6 +5566,15 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5050
5566
  };
5051
5567
  const nestedScope = nestedResolutionScopeForExecutor(deps);
5052
5568
  const sessionRoots = trustedSessionRootsForStatus(ctx, deps);
5569
+ if (action === "debug.run") {
5570
+ if (!targetRunId && !hasDirectoryTarget) {
5571
+ return withBudget({ content: [{ type: "text", text: "action='debug.run' requires id, runId, or dir." }], isError: true, details: { mode: "management", results: [] } });
5572
+ }
5573
+ if (paramsWithResolvedCwd.view) {
5574
+ return withBudget({ content: [{ type: "text", text: "action='debug.run' does not support status views." }], isError: true, details: { mode: "management", results: [] } });
5575
+ }
5576
+ return withBudget(inspectSubagentStatus(paramsWithResolvedCwd, omitUndefinedProperties({ state: deps.state, nested: nestedScope, sessionRoots })));
5577
+ }
5053
5578
  if (paramsWithResolvedCwd.view === "fleet") {
5054
5579
  return withBudget(inspectSubagentStatus(paramsWithResolvedCwd, omitUndefinedProperties({ state: deps.state, nested: nestedScope, sessionRoots })));
5055
5580
  }
@@ -5439,6 +5964,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5439
5964
  const sessionName = resolveIntercomSessionTarget(deps.pi.getSessionName(), ctx.sessionManager.getSessionId());
5440
5965
  const intercomBridge = resolveIntercomBridge({
5441
5966
  config: deps.config.intercomBridge,
5967
+ override: effectiveParams.intercomBridge,
5442
5968
  context: effectiveParams.context ?? (contextPolicy.usesFork ? "fork" : undefined),
5443
5969
  orchestratorTarget: sessionName,
5444
5970
  });
@@ -5448,7 +5974,6 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5448
5974
  const runId = randomUUID().slice(0, 8);
5449
5975
  const inheritedNestedRoute = resolveInheritedNestedRouteFromEnv();
5450
5976
  const nestedParentAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
5451
- const nestedRoute = inheritedNestedRoute ?? createNestedRoute(runId);
5452
5977
  const shareEnabled = effectiveParams.share === true;
5453
5978
  const hasChain = (effectiveParams.chain?.length ?? 0) > 0;
5454
5979
  const hasTasks = (effectiveParams.tasks?.length ?? 0) > 0;
@@ -5532,9 +6057,50 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5532
6057
  const foregroundTimeout = resolveSingleAgentLaunchTimeout(
5533
6058
  effectiveParams,
5534
6059
  effectiveAsync,
6060
+ resolveConfigDefaultTimeoutMs(deps.config.timeoutMs),
5535
6061
  );
5536
6062
  if (foregroundTimeout.error) return buildRequestedModeError(effectiveParams, foregroundTimeout.error);
5537
6063
  const controlConfig = resolveControlConfig(deps.config.control, effectiveParams.control);
6064
+ const requestedWorkflowChildAsyncId = typeof effectiveParams.workflowChildAsyncId === "string" ? effectiveParams.workflowChildAsyncId.trim() : "";
6065
+ const asyncRunId = requestedWorkflowChildAsyncId && path.basename(requestedWorkflowChildAsyncId) === requestedWorkflowChildAsyncId
6066
+ ? requestedWorkflowChildAsyncId
6067
+ : randomUUID();
6068
+ const topLevelAsyncCapacityEligible = depth === 0 && !inheritedNestedRoute && !effectiveParams.workflowParentRunId;
6069
+ const topLevelAsync = effectiveAsync && topLevelAsyncCapacityEligible;
6070
+ let activeAsyncCapacity: ActiveAsyncCapacityHandle | undefined;
6071
+ if (topLevelAsync) {
6072
+ try {
6073
+ const activeLimit = resolveMaxActiveAsyncRunsPerSession(deps.config.maxActiveAsyncRunsPerSession);
6074
+ activeAsyncCapacity = acquireActiveAsyncCapacity({
6075
+ sessionId: requestSessionId,
6076
+ limit: activeLimit,
6077
+ runId: asyncRunId,
6078
+ kind: "runner",
6079
+ asyncDir: path.join(DIRS.async, asyncRunId),
6080
+ }, { liveWorkflowRunIds: new Set(deps.state.workflowControllers?.keys() ?? []) });
6081
+ deps.state.activeAsyncCapacity = getActiveAsyncCapacitySnapshot(requestSessionId, activeLimit, { liveWorkflowRunIds: new Set(deps.state.workflowControllers?.keys() ?? []) });
6082
+ } catch (error) {
6083
+ if (error instanceof ActiveAsyncCapacityError) {
6084
+ deps.state.activeAsyncCapacity = error.snapshot;
6085
+ return { content: [{ type: "text", text: error.message }], isError: true, details: { mode: foregroundMode, results: [], activeAsyncCapacity: error.snapshot } };
6086
+ }
6087
+ throw error;
6088
+ }
6089
+ }
6090
+
6091
+ let runFanoutBudget: RunFanoutBudgetDescriptor;
6092
+ try {
6093
+ const inheritedRunFanoutBudget = effectiveParams.runFanoutBudget ? undefined : decodeRunFanoutBudgetDescriptor(process.env[RUN_FANOUT_BUDGET_ENV]);
6094
+ runFanoutBudget = effectiveParams.runFanoutBudget
6095
+ ?? (inheritedRunFanoutBudget ? { ...inheritedRunFanoutBudget, parentPath: `${inheritedRunFanoutBudget.parentPath ? `${inheritedRunFanoutBudget.parentPath}/` : ""}${runId}` } : undefined)
6096
+ ?? createRunFanoutBudget(runId, resolveMaxSubagentSpawnsPerRun(deps.config.maxSubagentSpawnsPerRun));
6097
+ if (!effectiveParams.runFanoutAdmitted) claimRunFanoutBatch(runFanoutBudget, staticRunFanoutPaths(effectiveParams));
6098
+ } catch (error) {
6099
+ activeAsyncCapacity?.rollback();
6100
+ if (error instanceof RunFanoutLimitError) return runFanoutErrorResult(error, foregroundMode);
6101
+ return buildRequestedModeError(effectiveParams, error instanceof Error ? error.message : String(error));
6102
+ }
6103
+ const nestedRoute = inheritedNestedRoute ?? createNestedRoute(runId);
5538
6104
 
5539
6105
  const artifactConfig: ArtifactConfig = omitUndefinedProperties({
5540
6106
  ...DEFAULT_ARTIFACT_CONFIG,
@@ -5560,6 +6126,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5560
6126
  try {
5561
6127
  fs.mkdirSync(sessionRoot, { recursive: true });
5562
6128
  } catch (error) {
6129
+ activeAsyncCapacity?.rollback();
5563
6130
  const message = error instanceof Error ? error.message : String(error);
5564
6131
  return toExecutionErrorResult(
5565
6132
  effectiveParams,
@@ -5590,10 +6157,14 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5590
6157
  preflightForkSessionsForStaticTasks(effectiveParams, contextPolicy, forkSessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems);
5591
6158
  }
5592
6159
  } catch (error) {
6160
+ activeAsyncCapacity?.rollback();
5593
6161
  return toExecutionErrorResult(effectiveParams, error, contextPolicy.contextSummary);
5594
6162
  }
5595
6163
  const chainBindingsError = validateExecutionChainBindings(effectiveParams, deps.config.chain?.dynamicFanout?.maxItems);
5596
- if (chainBindingsError) return withResolvedContext(chainBindingsError, contextPolicy.contextSummary);
6164
+ if (chainBindingsError) {
6165
+ activeAsyncCapacity?.rollback();
6166
+ return withResolvedContext(chainBindingsError, contextPolicy.contextSummary);
6167
+ }
5597
6168
 
5598
6169
  const onUpdateWithContext = onUpdate
5599
6170
  ? (r: AgentToolResult<Details>) => onUpdate(withResolvedContext({
@@ -5613,14 +6184,17 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5613
6184
  ownerSessionId: requestSessionId,
5614
6185
  });
5615
6186
  } catch (error) {
5616
- if (explicitMission) return toExecutionErrorResult(effectiveParams, error, contextPolicy.contextSummary);
6187
+ if (explicitMission) {
6188
+ activeAsyncCapacity?.rollback();
6189
+ return toExecutionErrorResult(effectiveParams, error, contextPolicy.contextSummary);
6190
+ }
5617
6191
  missionWarning = `Mission tracking unavailable: ${error instanceof Error ? error.message : String(error)}`;
5618
6192
  }
5619
6193
 
5620
6194
  const attachMission = (result: AgentToolResult<Details>): AgentToolResult<Details> => {
5621
6195
  if (!missionBinding) return missionWarning ? { ...result, details: { ...result.details, missionWarning } } : result;
5622
6196
  try {
5623
- return attachMissionToLaunchResult({ binding: missionBinding, result });
6197
+ return attachMissionToLaunchResult({ binding: delegatedExecution ? { ...missionBinding, announceInContent: false } : missionBinding, result });
5624
6198
  } catch (error) {
5625
6199
  const warning = `Mission tracking unavailable after launch: ${error instanceof Error ? error.message : String(error)}`;
5626
6200
  if (explicitMission) {
@@ -5641,7 +6215,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5641
6215
  requestSessionId,
5642
6216
  requestedSpawns,
5643
6217
  );
5644
- if (reservation.error) return attachMission(spawnBudgetErrorResult(reservation.error, foregroundMode));
6218
+ if (reservation.error) {
6219
+ activeAsyncCapacity?.rollback();
6220
+ return attachMission(spawnBudgetErrorResult(reservation.error, foregroundMode));
6221
+ }
5645
6222
 
5646
6223
  const execData: ExecutionContextData = omitUndefinedProperties({
5647
6224
  params: effectiveParams,
@@ -5650,6 +6227,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5650
6227
  signal,
5651
6228
  onUpdate: onUpdateWithContext,
5652
6229
  agents,
6230
+ recoveryAgents: discoveredAgents,
5653
6231
  runId,
5654
6232
  shareEnabled,
5655
6233
  sessionRoot,
@@ -5661,6 +6239,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5661
6239
  artifactsDir,
5662
6240
  backgroundRequestedWhileClarifying,
5663
6241
  effectiveAsync,
6242
+ asyncRunId,
5664
6243
  controlConfig,
5665
6244
  intercomBridge,
5666
6245
  nestedRoute,
@@ -5676,11 +6255,14 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5676
6255
  parentSessionId: requestSessionId,
5677
6256
  parentPiSessionId: requestPiSessionId,
5678
6257
  capabilityCeiling: resolveCurrentSubagentCapabilityCeiling(requestSessionId),
6258
+ runFanoutBudget,
6259
+ topLevelAsyncCapacityEligible,
6260
+ activeAsyncCapacity,
5679
6261
  });
5680
6262
 
5681
- const foregroundDescription = effectiveParams.task?.trim()
5682
- || effectiveParams.tasks?.[0]?.task?.trim()
5683
- || (effectiveParams.chain ? firstRawChainTask(effectiveParams.chain)?.trim() : undefined);
6263
+ const foregroundDescription = selectedAgentNames.length === 1
6264
+ ? `${selectedAgentNames[0]} child`
6265
+ : `${selectedAgentNames.length} live children`;
5684
6266
  const parentWorkflowStatus = effectiveParams.workflowParentRunId
5685
6267
  ? readStatus(path.join(DIRS.async, effectiveParams.workflowParentRunId))
5686
6268
  : null;
@@ -5715,6 +6297,37 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5715
6297
  interrupt: undefined,
5716
6298
  });
5717
6299
  if (foregroundControl) {
6300
+ foregroundControl.promptAuditRedo = async (index, guidance) => {
6301
+ const audit = getLivePromptAudit(foregroundControl, index);
6302
+ if (!audit?.rerun) return { text: "Redo is not safe for this prompt in this slice.", isError: true };
6303
+ const rewrittenTask = await rewritePromptWithGuidance({
6304
+ ctx,
6305
+ authoredTask: audit.authoredTask,
6306
+ runtimeAdditions: audit.runtimeAdditions,
6307
+ finalEffectivePrompt: audit.finalEffectivePrompt,
6308
+ guidance,
6309
+ signal,
6310
+ });
6311
+ const redoParams = promptAuditRedoParams(audit.rerun.params, rewrittenTask);
6312
+ const previousForegroundId = deps.state.lastForegroundControlId;
6313
+ const launch = execute(randomUUID(), redoParams, signal, undefined, ctx, true);
6314
+ const newRunId = deps.state.lastForegroundControlId && deps.state.lastForegroundControlId !== previousForegroundId
6315
+ ? deps.state.lastForegroundControlId
6316
+ : undefined;
6317
+ if (!newRunId) {
6318
+ const result = await launch;
6319
+ return { text: result.content.find((item) => item.type === "text")?.text ?? "Prompt redo could not start.", isError: true };
6320
+ }
6321
+ foregroundControl.supersededByRunId = newRunId;
6322
+ const replacement = deps.state.foregroundControls.get(newRunId);
6323
+ if (replacement) replacement.sourceRunId = foregroundControl.runId;
6324
+ void launch.then((result) => {
6325
+ if (result.isError) console.warn(`[pi-subagents] Prompt redo ${newRunId} failed: ${result.content.find((item) => item.type === "text")?.text ?? "unknown error"}`);
6326
+ }).catch((error) => {
6327
+ console.warn(`[pi-subagents] Prompt redo ${newRunId} failed: ${error instanceof Error ? error.message : String(error)}`);
6328
+ });
6329
+ return { text: `Prompt redo started ${newRunId}.` };
6330
+ };
5718
6331
  deps.state.foregroundControls.set(runId, foregroundControl);
5719
6332
  deps.state.lastForegroundControlId = runId;
5720
6333
  }
@@ -5805,13 +6418,18 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5805
6418
  };
5806
6419
 
5807
6420
  let nestedForegroundStarted = false;
6421
+ let asyncLaunchFailed = false;
5808
6422
  try {
6423
+ if (effectiveAsync) {
6424
+ deps.state.liveAsyncSessionRoots ??= new Map();
6425
+ deps.state.liveAsyncSessionRoots.set(asyncRunId, sessionRoot);
6426
+ }
5809
6427
  if (workflowLaunchObserver) {
5810
6428
  const singleTask = hasTasks && effectiveParams.tasks?.length === 1 ? effectiveParams.tasks[0] : undefined;
5811
6429
  const launch = hasSingle
5812
- ? { agent: effectiveParams.agent!, sessionFile: childSessionFileForTask(effectiveParams.agent!, 0, effectiveParams.model) }
6430
+ ? { agent: effectiveParams.agent!, sessionFile: childSessionFileForTask(effectiveParams.agent!, 0, effectiveParams.model), async: effectiveAsync, ...(effectiveAsync ? { runId: asyncRunId } : {}) }
5813
6431
  : singleTask
5814
- ? { agent: singleTask.agent, sessionFile: childSessionFileForTask(singleTask.agent, 0, singleTask.model) }
6432
+ ? { agent: singleTask.agent, sessionFile: childSessionFileForTask(singleTask.agent, 0, singleTask.model), async: effectiveAsync, ...(effectiveAsync ? { runId: asyncRunId } : {}) }
5815
6433
  : undefined;
5816
6434
  if (launch) {
5817
6435
  workflowLaunchObservers.delete(params);
@@ -5819,31 +6437,38 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5819
6437
  }
5820
6438
  }
5821
6439
  const asyncResult = runAsyncPath(execData, deps);
5822
- if (asyncResult) return attachMission(withResolvedContext(withForkThinkingNotes(asyncResult, forkThinkingDowngrades), contextPolicy.contextSummary));
6440
+ if (asyncResult) {
6441
+ asyncLaunchFailed = asyncResult.isError === true;
6442
+ return attachMission(withRunFanoutBudget(withResolvedContext(withForkThinkingNotes(asyncResult, forkThinkingDowngrades), contextPolicy.contextSummary), runFanoutBudget));
6443
+ }
5823
6444
  if (foregroundControl) {
5824
6445
  writeNestedForegroundEvent("subagent.nested.started");
5825
6446
  nestedForegroundStarted = true;
5826
6447
  }
6448
+ const runFanoutAnnotateContent = !delegatedExecution;
5827
6449
  if (hasChain && effectiveParams.chain) {
5828
6450
  const result = await runChainPath(execData, deps);
5829
6451
  writeNestedForegroundEvent("subagent.nested.completed", result);
5830
- return attachMission(withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary));
6452
+ return attachMission(withRunFanoutBudget(withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary), runFanoutBudget, { annotateContent: runFanoutAnnotateContent }));
5831
6453
  }
5832
6454
  if (hasTasks && effectiveParams.tasks) {
5833
6455
  const result = await runParallelPath(execData, deps);
5834
6456
  writeNestedForegroundEvent("subagent.nested.completed", result);
5835
- return attachMission(withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary));
6457
+ return attachMission(withRunFanoutBudget(withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary), runFanoutBudget, { annotateContent: runFanoutAnnotateContent }));
5836
6458
  }
5837
6459
  if (hasSingle) {
5838
6460
  const result = await runSinglePath(execData, deps);
5839
6461
  writeNestedForegroundEvent("subagent.nested.completed", result);
5840
- return attachMission(withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary));
6462
+ return attachMission(withRunFanoutBudget(withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary), runFanoutBudget, { annotateContent: runFanoutAnnotateContent }));
5841
6463
  }
5842
6464
  } catch (error) {
6465
+ asyncLaunchFailed = effectiveAsync;
5843
6466
  const errorResult = withForkThinkingNotes(toExecutionErrorResult(effectiveParams, error, contextPolicy.contextSummary), forkThinkingDowngrades);
5844
6467
  if (nestedForegroundStarted) writeNestedForegroundEvent("subagent.nested.completed", errorResult);
5845
6468
  return attachMission(errorResult);
5846
6469
  } finally {
6470
+ if (effectiveAsync && (asyncLaunchFailed || (activeAsyncCapacity && !activeAsyncCapacity.owner.runnerStartedAt))) deps.state.liveAsyncSessionRoots?.delete(asyncRunId);
6471
+ if (activeAsyncCapacity && !activeAsyncCapacity.owner.runnerStartedAt) activeAsyncCapacity.rollback();
5847
6472
  if (foregroundControl) {
5848
6473
  settleForegroundSchedulingOwner(foregroundControl);
5849
6474
  removeForegroundControlIfIdle(deps.state, runId);
@@ -5912,6 +6537,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5912
6537
  delete privateParams.delegatedAllowZeroToolBudget;
5913
6538
  if (thinkingOverride !== undefined) delegatedThinkingOverrides.set(delegatedParams, thinkingOverride);
5914
6539
  if (allowZeroToolBudget) delegatedZeroToolBudgets.add(delegatedParams);
6540
+ delegatedExecutions.add(delegatedParams);
5915
6541
  return execute(id, delegatedParams, signal, onUpdate, ctx);
5916
6542
  };
5917
6543