pi-subagents 0.47.1 → 0.48.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 (46) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +2 -0
  3. package/docs/configuration.md +46 -2
  4. package/docs/observability.md +1 -1
  5. package/docs/tool-reference.md +1 -1
  6. package/package.json +1 -1
  7. package/src/agents/agents.ts +9 -3
  8. package/src/extension/config.ts +6 -0
  9. package/src/extension/doctor.ts +40 -0
  10. package/src/extension/index.ts +21 -0
  11. package/src/extension/public-execution.ts +5 -0
  12. package/src/extension/rpc.ts +3 -9
  13. package/src/extension/schemas.ts +2 -2
  14. package/src/intercom/intercom-bridge.ts +4 -1
  15. package/src/missions/lifecycle.ts +4 -7
  16. package/src/missions/store.ts +4 -4
  17. package/src/missions/workflow-state.ts +6 -2
  18. package/src/runs/background/active-async-capacity.ts +374 -0
  19. package/src/runs/background/active-run-index.ts +9 -5
  20. package/src/runs/background/async-execution.ts +117 -29
  21. package/src/runs/background/async-resume.ts +7 -1
  22. package/src/runs/background/async-status.ts +11 -5
  23. package/src/runs/background/chain-append.ts +33 -15
  24. package/src/runs/background/owned-process-tree.ts +104 -0
  25. package/src/runs/background/process-terminal.ts +17 -3
  26. package/src/runs/background/run-status.ts +5 -1
  27. package/src/runs/background/stale-run-reconciler.ts +3 -3
  28. package/src/runs/background/subagent-runner.ts +56 -31
  29. package/src/runs/foreground/chain-execution.ts +37 -2
  30. package/src/runs/foreground/execution.ts +90 -19
  31. package/src/runs/foreground/foreground-control.ts +12 -0
  32. package/src/runs/foreground/prompt-audit.ts +171 -0
  33. package/src/runs/foreground/subagent-executor.ts +614 -187
  34. package/src/runs/shared/acceptance.ts +13 -4
  35. package/src/runs/shared/llm-intent-arbiter.ts +286 -0
  36. package/src/runs/shared/parallel-utils.ts +2 -0
  37. package/src/runs/shared/pi-args.ts +44 -1
  38. package/src/runs/shared/run-fanout-budget.ts +280 -0
  39. package/src/runs/shared/single-output.ts +4 -2
  40. package/src/runs/shared/task-intent.ts +19 -3
  41. package/src/runs/shared/worktree.ts +17 -5
  42. package/src/shared/types.ts +92 -0
  43. package/src/shared/utils.ts +3 -1
  44. package/src/tui/fleet-status.ts +7 -5
  45. package/src/tui/fleet.ts +225 -12
  46. package/src/workflows/scripted-workflow.ts +22 -5
@@ -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";
@@ -143,6 +147,7 @@ import {
143
147
  type ResolvedControlConfig,
144
148
  type ResolvedTurnBudget,
145
149
  type ResolvedToolBudget,
150
+ type RunFanoutBudgetDescriptor,
146
151
  type SingleResult,
147
152
  type ToolBudgetConfig,
148
153
  type TurnBudgetConfig,
@@ -161,6 +166,7 @@ import {
161
166
  resolveTopLevelParallelMaxTasks,
162
167
  resolveChildMaxSubagentDepth,
163
168
  resolveCurrentMaxSubagentDepth,
169
+ resolveMaxSubagentSpawnsPerRun,
164
170
  wrapForkTask,
165
171
  } from "../../shared/types.ts";
166
172
 
@@ -284,6 +290,10 @@ export interface SubagentParamsLike {
284
290
  workflowKey?: string;
285
291
  workflowChildAsyncId?: string;
286
292
  suppressRoutineResultIntercom?: boolean;
293
+ /** Internal inherited cumulative run-tree budget. */
294
+ runFanoutBudget?: RunFanoutBudgetDescriptor;
295
+ /** Internal workflow host admission proof. */
296
+ runFanoutAdmitted?: boolean;
287
297
  /** Internal durable-run compatibility fields. Public callers must use workflowScript. */
288
298
  chain?: ChainStep[];
289
299
  tasks?: TaskParam[];
@@ -388,6 +398,7 @@ interface ExecutionContextData {
388
398
  artifactsDir: string;
389
399
  backgroundRequestedWhileClarifying: boolean;
390
400
  effectiveAsync: boolean;
401
+ asyncRunId: string;
391
402
  controlConfig: ResolvedControlConfig;
392
403
  intercomBridge: IntercomBridgeState;
393
404
  nestedRoute?: NestedRouteInfo;
@@ -404,6 +415,9 @@ interface ExecutionContextData {
404
415
  parentSessionId: string | null;
405
416
  parentPiSessionId?: string;
406
417
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
418
+ runFanoutBudget: RunFanoutBudgetDescriptor;
419
+ topLevelAsyncCapacityEligible: boolean;
420
+ activeAsyncCapacity?: ActiveAsyncCapacityHandle;
407
421
  }
408
422
 
409
423
  function resolveRequestedCwd(runtimeCwd: string, requestedCwd: string | undefined): string {
@@ -432,6 +446,17 @@ function getForegroundControl(state: SubagentState, runId: string | undefined) {
432
446
  return newest;
433
447
  }
434
448
 
449
+ function promptAuditRedoParams(value: unknown, rewrittenTask: string): SubagentParamsLike {
450
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Prompt redo is missing a safe live launch contract.");
451
+ const params = { ...(value as SubagentParamsLike), task: rewrittenTask, async: false };
452
+ delete params.workflowParentRunId;
453
+ delete params.workflowKey;
454
+ delete params.workflowChildAsyncId;
455
+ delete params.suppressRoutineResultIntercom;
456
+ if (params.worktree === true && Array.isArray(params.tasks)) delete params.cwd;
457
+ return params;
458
+ }
459
+
435
460
  function formatForegroundActivity(control: SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never): string | undefined {
436
461
  const facts: string[] = [];
437
462
  if (control.currentTool && control.currentToolStartedAt) facts.push(`tool ${control.currentTool} for ${Math.floor(Math.max(0, Date.now() - control.currentToolStartedAt) / 1000)}s`);
@@ -483,12 +508,18 @@ function withSpawnBudgetStatus(
483
508
  sessionId: string | null,
484
509
  ): AgentToolResult<Details> {
485
510
  const spawnBudget = getSpawnBudgetSnapshot(state, config, sessionId);
511
+ const activeAsyncCapacity = sessionId
512
+ ? getActiveAsyncCapacitySnapshot(sessionId, resolveMaxActiveAsyncRunsPerSession(config.maxActiveAsyncRunsPerSession), {
513
+ liveWorkflowRunIds: new Set(state.workflowControllers?.keys() ?? []),
514
+ })
515
+ : { used: 0, limit: resolveMaxActiveAsyncRunsPerSession(config.maxActiveAsyncRunsPerSession) ?? 0 };
516
+ state.activeAsyncCapacity = activeAsyncCapacity;
486
517
  return {
487
518
  ...result,
488
519
  content: result.content.map((item, index) => index === 0 && item.type === "text"
489
- ? { ...item, text: `${formatSpawnBudget(spawnBudget)}\n${item.text}` }
520
+ ? { ...item, text: `${formatSpawnBudget(spawnBudget)}\nActive async capacity: ${activeAsyncCapacity.used}/${activeAsyncCapacity.limit || "unlimited"} used\n${item.text}` }
490
521
  : item),
491
- details: { ...result.details, spawnBudget },
522
+ details: { ...result.details, spawnBudget, activeAsyncCapacity },
492
523
  };
493
524
  }
494
525
 
@@ -509,6 +540,31 @@ function countRequestedSubagentSpawns(params: SubagentParamsLike, config: Extens
509
540
  return params.agent ? 1 : 0;
510
541
  }
511
542
 
543
+ function staticRunFanoutPaths(params: SubagentParamsLike): string[] {
544
+ if (params.tasks) return params.tasks.map((_, index) => `tasks[${index}]`);
545
+ if (params.chain) return params.chain.flatMap((step, stepIndex) => {
546
+ if (isDynamicParallelStep(step) || "checkpoint" in step) return [];
547
+ if (isParallelStep(step)) return step.parallel.map((_, itemIndex) => `chain[${stepIndex}].parallel[${itemIndex}]`);
548
+ return [`chain[${stepIndex}]`];
549
+ });
550
+ return params.agent ? ["single"] : [];
551
+ }
552
+
553
+ function runFanoutErrorResult(error: RunFanoutLimitError, mode: "single" | "parallel" | "chain"): AgentToolResult<Details> {
554
+ return { content: [{ type: "text", text: error.message }], isError: true, details: { mode, results: [], runFanoutBudget: error.snapshot, runFanoutRejection: error.rejection } };
555
+ }
556
+
557
+ function withRunFanoutBudget(result: AgentToolResult<Details>, descriptor: RunFanoutBudgetDescriptor, options: { annotateContent?: boolean } = {}): AgentToolResult<Details> {
558
+ const runFanoutBudget = getRunFanoutBudgetSnapshot(descriptor);
559
+ return {
560
+ ...result,
561
+ content: options.annotateContent === false
562
+ ? result.content
563
+ : result.content.map((item, index) => index === 0 && item.type === "text" ? { ...item, text: `${formatRunFanoutBudget(runFanoutBudget)}\n${item.text}` } : item),
564
+ details: { ...result.details, runFanoutBudget },
565
+ };
566
+ }
567
+
512
568
  function foregroundStatusResult(control: SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never): AgentToolResult<Details> {
513
569
  let nestedWarning: string | undefined;
514
570
  try {
@@ -1141,18 +1197,30 @@ function appendStepToAsyncChain(input: {
1141
1197
  }
1142
1198
 
1143
1199
  try {
1200
+ const asyncDir = resolved.location.asyncDir;
1201
+ if (!asyncDir) throw new Error(`Run '${resolved.id}' is missing its async directory.`);
1202
+ const runFanoutBudget = readRunFanoutBudgetDescriptor(asyncDir);
1203
+ if (!runFanoutBudget) throw new Error(`Run '${resolved.id}' is missing its run fan-out budget identity.`);
1204
+ const startIndex = (status.chainStepCount ?? status.steps?.length ?? 0) + pendingAppendRequests.reduce((total, request) => total + request.steps.length, 0);
1205
+ const appendPaths = chain.flatMap((step, localIndex) => {
1206
+ const absoluteIndex = startIndex + localIndex;
1207
+ if (isDynamicParallelStep(step) || "checkpoint" in step) return [];
1208
+ if (isParallelStep(step)) return step.parallel.map((_, itemIndex) => `chain[${absoluteIndex}].parallel[${itemIndex}]`);
1209
+ return [`chain[${absoluteIndex}]`];
1210
+ });
1144
1211
  const result = enqueueChainAppendRequest({
1145
- asyncDir: resolved.location.asyncDir,
1212
+ asyncDir,
1146
1213
  runId: resolved.id,
1147
1214
  steps: built.steps,
1215
+ admit: (persist) => claimRunFanoutBatchWithCommit(runFanoutBudget, appendPaths, persist),
1148
1216
  });
1149
1217
  const stepText = built.steps.length === 1 ? "step" : "steps";
1150
1218
  return {
1151
1219
  content: [{
1152
1220
  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}.`,
1221
+ 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
1222
  }],
1155
- details: { mode: "management", results: [], asyncId: resolved.id, asyncDir: resolved.location.asyncDir },
1223
+ details: { mode: "management", results: [], asyncId: resolved.id, asyncDir },
1156
1224
  };
1157
1225
  } catch (error) {
1158
1226
  const message = error instanceof Error ? error.message : String(error);
@@ -1487,6 +1555,20 @@ async function resumeAsyncRun(input: {
1487
1555
  };
1488
1556
  }
1489
1557
  const runId = randomUUID().slice(0, 8);
1558
+ const topLevelResume = depth === 0 && !resolveInheritedNestedRouteFromEnv() && !input.params.workflowParentRunId;
1559
+ let activeAsyncCapacity: ActiveAsyncCapacityHandle | undefined;
1560
+ try {
1561
+ activeAsyncCapacity = topLevelResume ? acquireActiveAsyncCapacity({
1562
+ sessionId: input.deps.state.currentSessionId!,
1563
+ limit: resolveMaxActiveAsyncRunsPerSession(input.deps.config.maxActiveAsyncRunsPerSession),
1564
+ runId,
1565
+ kind: "runner",
1566
+ asyncDir: path.join(DIRS.async, runId),
1567
+ }, { liveWorkflowRunIds: new Set(input.deps.state.workflowControllers?.keys() ?? []) }) : undefined;
1568
+ } catch (error) {
1569
+ if (error instanceof ActiveAsyncCapacityError) return { content: [{ type: "text", text: error.message }], isError: true, details: { mode: "chain", results: [], activeAsyncCapacity: error.snapshot } };
1570
+ throw error;
1571
+ }
1490
1572
  const artifactConfig: ArtifactConfig = omitUndefinedProperties({ ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false, dir: input.deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir });
1491
1573
  const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo);
1492
1574
  const contextPolicy = resolveExplicitContextPolicy(input.params);
@@ -1538,9 +1620,14 @@ async function resumeAsyncRun(input: {
1538
1620
  controlIntercomTarget: intercomBridge.active ? intercomBridge.orchestratorTarget : undefined,
1539
1621
  childIntercomTarget: intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
1540
1622
  globalConcurrencyLimit: input.deps.config.globalConcurrencyLimit,
1623
+ runFanoutBudget: createRunFanoutBudget(runId, resolveMaxSubagentSpawnsPerRun(input.deps.config.maxSubagentSpawnsPerRun)),
1541
1624
  capabilityCeiling: intersectSubagentCapabilityCeilings("capabilityCeiling" in target ? target.capabilityCeiling : undefined, resolveCurrentSubagentCapabilityCeiling(input.deps.state.currentSessionId)),
1625
+ activeAsyncCapacity,
1542
1626
  }));
1543
- if (result.isError) return result;
1627
+ if (result.isError) {
1628
+ activeAsyncCapacity?.rollback();
1629
+ return result;
1630
+ }
1544
1631
  const attachedId = result.details.asyncId ?? runId;
1545
1632
  const lines = [
1546
1633
  `Attached async subagent ${target.runId} as the first step of a new chain.`,
@@ -1559,12 +1646,38 @@ async function resumeAsyncRun(input: {
1559
1646
  if (!revivalSessionFile) {
1560
1647
  return { content: [{ type: "text", text: `Async child '${target.runId}' has no persisted session file to resume.` }], isError: true, details: { mode: "management", results: [] } };
1561
1648
  }
1649
+ if (target.source === "async" && !recoveryDescriptor) {
1650
+ 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: [] } };
1651
+ }
1562
1652
  const runId = randomUUID().slice(0, 8);
1653
+ const topLevelResume = depth === 0 && !resolveInheritedNestedRouteFromEnv() && !input.params.workflowParentRunId;
1654
+ let activeAsyncCapacity: ActiveAsyncCapacityHandle | undefined;
1655
+ try {
1656
+ activeAsyncCapacity = !topLevelResume ? undefined : target.source === "async"
1657
+ ? transferActiveAsyncCapacity({
1658
+ sessionId: input.deps.state.currentSessionId!,
1659
+ limit: resolveMaxActiveAsyncRunsPerSession(input.deps.config.maxActiveAsyncRunsPerSession),
1660
+ sourceRunId: target.runId,
1661
+ runId,
1662
+ asyncDir: path.join(DIRS.async, runId),
1663
+ })
1664
+ : acquireActiveAsyncCapacity({
1665
+ sessionId: input.deps.state.currentSessionId!,
1666
+ limit: resolveMaxActiveAsyncRunsPerSession(input.deps.config.maxActiveAsyncRunsPerSession),
1667
+ runId,
1668
+ kind: "runner",
1669
+ asyncDir: path.join(DIRS.async, runId),
1670
+ });
1671
+ } catch (error) {
1672
+ if (error instanceof ActiveAsyncCapacityError) return { content: [{ type: "text", text: error.message }], isError: true, details: { mode: "single", results: [], activeAsyncCapacity: error.snapshot } };
1673
+ return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, details: { mode: "single", results: [] } };
1674
+ }
1563
1675
  const recoveryAgentConfig = recoveryDescriptor ? applySteeringRecoveryAgentConfig(agentConfig, recoveryDescriptor) : agentConfig;
1564
1676
  const artifactConfig: ArtifactConfig = recoveryDescriptor?.artifactConfig ?? omitUndefinedProperties({ ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false, dir: input.deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir });
1565
1677
  const artifactsDir = recoveryDescriptor?.artifactsDir ?? getArtifactsDir(parentSessionFile, effectiveCwd, artifactConfig.dir);
1566
1678
  const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo);
1567
1679
  const parentModel = input.parentModel;
1680
+ const revivalAsyncDir = path.join(DIRS.async, runId);
1568
1681
  const result = executeAsyncSingle(runId, compactOptional<Parameters<typeof executeAsyncSingle>[1]>({
1569
1682
  agent: target.agent,
1570
1683
  task: buildRevivedAsyncTask(target as Parameters<typeof buildRevivedAsyncTask>[0], effectiveFollowUp),
@@ -1618,10 +1731,28 @@ async function resumeAsyncRun(input: {
1618
1731
  ...(input.params.turnBudget !== undefined ? { turnBudget: input.params.turnBudget } : {}),
1619
1732
  ...(input.params.toolBudget !== undefined ? { toolBudget: input.params.toolBudget } : {}),
1620
1733
  capabilityCeiling: intersectSubagentCapabilityCeilings("capabilityCeiling" in target ? target.capabilityCeiling : undefined, recoveryDescriptor?.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(input.deps.state.currentSessionId)),
1734
+ runFanoutBudget: recoveryDescriptor?.runFanoutBudget ?? createRunFanoutBudget(runId, resolveMaxSubagentSpawnsPerRun(input.deps.config.maxSubagentSpawnsPerRun)),
1621
1735
  parentWorkflowRunId: input.params.workflowParentRunId,
1622
1736
  workflowKey: input.params.workflowKey,
1737
+ activeAsyncCapacity,
1623
1738
  }));
1624
- if (result.isError) return result;
1739
+ if (result.isError) {
1740
+ const startedStatus = readStatus(revivalAsyncDir);
1741
+ if (input.params.workflowParentRunId !== undefined && startedStatus?.runId === runId && startedStatus.processTerminal?.runnerProcessInstanceId) {
1742
+ return {
1743
+ ...result,
1744
+ details: {
1745
+ ...result.details,
1746
+ runId,
1747
+ asyncId: runId,
1748
+ asyncDir: revivalAsyncDir,
1749
+ ...(target.launchContractDigest ? { sourceLaunchContractDigest: target.launchContractDigest } : {}),
1750
+ },
1751
+ };
1752
+ }
1753
+ activeAsyncCapacity?.rollback();
1754
+ return result;
1755
+ }
1625
1756
  for (const brief of queuedBriefs) fs.rmSync(brief.path, { force: true });
1626
1757
  if (queuedBriefs.length > 0 && sourceAsyncDir) {
1627
1758
  const sourceStatus = readStatus(sourceAsyncDir);
@@ -2117,6 +2248,27 @@ export const DEFAULT_FOREGROUND_TIMEOUT_MS = 30 * 60 * 1000;
2117
2248
  // stay unbounded as a whole, while each runner child has its own deadline.
2118
2249
  export { DEFAULT_ASYNC_TIMEOUT_MS };
2119
2250
 
2251
+ /**
2252
+ * Maximum delay a Node.js timer accepts. Values above the 32-bit signed integer
2253
+ * ceiling overflow `setTimeout`, which silently clamps the delay to ~1ms and
2254
+ * fires almost immediately — so a run configured with a larger deadline would
2255
+ * terminate right away while reporting the long duration. Any timeout destined
2256
+ * for a timer must stay within this bound.
2257
+ */
2258
+ const MAX_TIMER_DELAY_MS = 2_147_483_647;
2259
+
2260
+ /**
2261
+ * Resolve the optional global default runtime deadline from extension config
2262
+ * (`config.timeoutMs`). Returns undefined for unset or invalid values so callers
2263
+ * fall back to the built-in defaults. "Invalid" covers non-positive-integer
2264
+ * values and values above `MAX_TIMER_DELAY_MS`; the latter would overflow the
2265
+ * Node.js timer and expire the run almost immediately instead of running long.
2266
+ */
2267
+ export function resolveConfigDefaultTimeoutMs(raw: unknown): number | undefined {
2268
+ if (typeof raw !== "number" || !Number.isInteger(raw) || raw <= 0 || raw > MAX_TIMER_DELAY_MS) return undefined;
2269
+ return raw;
2270
+ }
2271
+
2120
2272
  export function resolveForegroundTimeout(params: SubagentParamsLike, defaultTimeoutMs?: number): { timeoutMs?: number; error?: string } {
2121
2273
  const rawTimeout = params.timeoutMs;
2122
2274
  const rawMaxRuntime = params.maxRuntimeMs;
@@ -2140,14 +2292,18 @@ export function resolveForegroundTimeout(params: SubagentParamsLike, defaultTime
2140
2292
  * Resolve the effective launch timeout for a single-agent run, applying the
2141
2293
  * async/foreground default when neither the caller nor the agent set one.
2142
2294
  *
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.
2295
+ * A global config default (`config.timeoutMs`, passed as `configDefaultTimeoutMs`)
2296
+ * replaces the built-in 30-minute backstop wherever a concrete default is applied.
2297
+ * The async default is deliberately applied only to plain single-agent launches.
2298
+ * Composite launches keep their top-level execution unbounded when no timeout is
2299
+ * set — even with a config default — while their runner children resolve separate
2300
+ * deadlines. Exported so the executor wiring is directly testable.
2147
2301
  */
2148
- export function resolveSingleAgentLaunchTimeout(params: SubagentParamsLike, async: boolean): { timeoutMs?: number; error?: string } {
2302
+ export function resolveSingleAgentLaunchTimeout(params: SubagentParamsLike, async: boolean, configDefaultTimeoutMs?: number): { timeoutMs?: number; error?: string } {
2149
2303
  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;
2304
+ const foregroundDefault = configDefaultTimeoutMs ?? DEFAULT_FOREGROUND_TIMEOUT_MS;
2305
+ const asyncSingleDefault = configDefaultTimeoutMs ?? DEFAULT_ASYNC_TIMEOUT_MS;
2306
+ const defaultTimeoutMs = !async ? foregroundDefault : isComposite ? undefined : asyncSingleDefault;
2151
2307
  return resolveForegroundTimeout(params, defaultTimeoutMs);
2152
2308
  }
2153
2309
 
@@ -2560,8 +2716,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2560
2716
  details: { mode: "single" as const, results: [] },
2561
2717
  };
2562
2718
  }
2563
- const requestedWorkflowChildAsyncId = typeof params.workflowChildAsyncId === "string" ? params.workflowChildAsyncId.trim() : "";
2564
- const id = requestedWorkflowChildAsyncId && path.basename(requestedWorkflowChildAsyncId) === requestedWorkflowChildAsyncId ? requestedWorkflowChildAsyncId : randomUUID();
2719
+ const id = data.asyncRunId;
2565
2720
  const parentModel = data.parentModel;
2566
2721
  const asyncCtx = compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
2567
2722
  pi: deps.pi,
@@ -2615,6 +2770,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2615
2770
  shareEnabled,
2616
2771
  sessionRoot,
2617
2772
  chainSkills: [],
2773
+ activeAsyncCapacity: data.activeAsyncCapacity,
2618
2774
  sessionFilesByFlatIndex: params.tasks.map((task, index) => sessionFileForTask(task.agent, index, task.model)),
2619
2775
  thinkingOverridesByFlatIndex: params.tasks.map((task, index) => thinkingOverrideForTask(task.agent, index, task.model)),
2620
2776
  contextForAgent: contextPolicy.contextForAgent,
@@ -2634,6 +2790,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2634
2790
  usageBudget: data.usageBudget,
2635
2791
  configToolBudget: data.configToolBudget,
2636
2792
  capabilityCeiling: data.capabilityCeiling,
2793
+ runFanoutBudget: data.runFanoutBudget,
2637
2794
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2638
2795
  parentWorkflowRunId: params.workflowParentRunId,
2639
2796
  workflowKey: params.workflowKey,
@@ -2659,6 +2816,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2659
2816
  shareEnabled,
2660
2817
  sessionRoot,
2661
2818
  chainSkills,
2819
+ activeAsyncCapacity: data.activeAsyncCapacity,
2662
2820
  sessionFilesByFlatIndex: collectChainSessionFiles(chain, sessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems),
2663
2821
  thinkingOverridesByFlatIndex: collectChainThinkingOverrides(chain, thinkingOverrideForTask, deps.config.chain?.dynamicFanout?.maxItems),
2664
2822
  contextForAgent: contextPolicy.contextForAgent,
@@ -2679,6 +2837,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2679
2837
  usageBudget: data.usageBudget,
2680
2838
  configToolBudget: data.configToolBudget,
2681
2839
  capabilityCeiling: data.capabilityCeiling,
2840
+ runFanoutBudget: data.runFanoutBudget,
2682
2841
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2683
2842
  parentWorkflowRunId: params.workflowParentRunId,
2684
2843
  workflowKey: params.workflowKey,
@@ -2700,7 +2859,12 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2700
2859
  const normalizedSkills = normalizeSkillInput(params.skill);
2701
2860
  const skills = normalizedSkills === false ? [] : normalizedSkills;
2702
2861
  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 });
2862
+ const externalRunnerWithoutExplicitModel = a.runner?.type === "external-cli"
2863
+ && params.model === undefined
2864
+ && (a.model === undefined || (a.modelSource?.type === "subagents.defaultModel" && a.model === a.modelSource.model));
2865
+ const modelOverride = externalRunnerWithoutExplicitModel
2866
+ ? undefined
2867
+ : resolveEffectiveSubagentModel(params.model as string | undefined, a.model, parentModel, availableModels, currentProvider, data.modelScope === undefined ? {} : { scope: data.modelScope });
2704
2868
  return executeAsyncSingle(id, compactOptional<Parameters<typeof executeAsyncSingle>[1]>({
2705
2869
  agent: params.agent!,
2706
2870
  task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(params.task ?? "") : (params.task ?? ""),
@@ -2713,6 +2877,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2713
2877
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
2714
2878
  artifactConfig,
2715
2879
  shareEnabled,
2880
+ activeAsyncCapacity: data.activeAsyncCapacity,
2716
2881
  sessionRoot,
2717
2882
  sessionFile: sessionFileForTask(params.agent!, 0, modelOverride),
2718
2883
  context: contextPolicy.contextForAgent(params.agent!),
@@ -2722,7 +2887,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2722
2887
  ...(params.reads !== undefined ? { reads: params.reads } : {}),
2723
2888
  outputBaseDir: resolveSingleRunOutputBaseDir(deps, artifactsDir, id),
2724
2889
  modelOverride,
2725
- thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride),
2890
+ thinkingOverride: externalRunnerWithoutExplicitModel ? undefined : thinkingOverrideForTask(params.agent!, 0, modelOverride),
2726
2891
  maxSubagentDepth,
2727
2892
  waitToolEnabled: deps.waitToolEnabled,
2728
2893
  worktreeSetupHook: deps.config.worktreeSetupHook,
@@ -2741,6 +2906,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2741
2906
  usageBudget: data.usageBudget,
2742
2907
  configToolBudget: data.configToolBudget,
2743
2908
  capabilityCeiling: data.capabilityCeiling,
2909
+ runFanoutBudget: data.runFanoutBudget,
2744
2910
  parentWorkflowRunId: params.workflowParentRunId,
2745
2911
  workflowKey: params.workflowKey,
2746
2912
  }));
@@ -2831,6 +2997,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2831
2997
  permissions: deps.config.permissions,
2832
2998
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2833
2999
  capabilityCeiling: data.capabilityCeiling,
3000
+ runFanoutBudget: data.runFanoutBudget,
2834
3001
  }));
2835
3002
 
2836
3003
  if (chainResult.requestedAsync) {
@@ -2841,7 +3008,9 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2841
3008
  details: { mode: "chain" as const, results: [] },
2842
3009
  };
2843
3010
  }
2844
- const id = randomUUID();
3011
+ const id = data.asyncRunId;
3012
+ const capacity = acquireClarifiedBackgroundCapacity(data, deps, "chain", id);
3013
+ if (capacity.errorResult) return capacity.errorResult;
2845
3014
  const parentModel = data.parentModel;
2846
3015
  const asyncCtx = compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
2847
3016
  pi: deps.pi,
@@ -2857,42 +3026,48 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2857
3026
  const rawAsyncChain = chainResult.requestedAsync.chain;
2858
3027
  const asyncChain = wrapChainTasksForFork(rawAsyncChain, contextPolicy);
2859
3028
  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
- }));
3029
+ try {
3030
+ return executeAsyncChain(id, compactOptional<Parameters<typeof executeAsyncChain>[1]>({
3031
+ chain: asyncChain,
3032
+ task: params.task,
3033
+ goal: resolveAsyncEventGoal(params.task, rawAsyncChain, firstAgent ? shouldForkAgent(contextPolicy, firstAgent) : false),
3034
+ agents,
3035
+ ctx: asyncCtx,
3036
+ availableModels: ctx.modelRegistry.getAvailable().map(toModelInfo),
3037
+ cwd: effectiveCwd,
3038
+ maxOutput: params.maxOutput,
3039
+ artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
3040
+ artifactConfig,
3041
+ shareEnabled,
3042
+ sessionRoot,
3043
+ chainSkills: chainResult.requestedAsync.chainSkills,
3044
+ activeAsyncCapacity: capacity.activeAsyncCapacity,
3045
+ sessionFilesByFlatIndex: collectChainSessionFiles(asyncChain, sessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems),
3046
+ thinkingOverridesByFlatIndex: collectChainThinkingOverrides(asyncChain, thinkingOverrideForTask, deps.config.chain?.dynamicFanout?.maxItems),
3047
+ contextForAgent: contextPolicy.contextForAgent,
3048
+ dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
3049
+ maxSubagentDepth: currentMaxSubagentDepth,
3050
+ waitToolEnabled: deps.waitToolEnabled,
3051
+ worktreeSetupHook: deps.config.worktreeSetupHook,
3052
+ worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
3053
+ worktreeBaseDir: deps.config.worktreeBaseDir,
3054
+ controlConfig,
3055
+ agentContract: params.agentContract,
3056
+ controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
3057
+ childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
3058
+ nestedRoute: data.nestedRoute,
3059
+ timeoutMs: data.timeoutMs,
3060
+ turnBudget: data.turnBudget,
3061
+ toolBudget: data.toolBudget,
3062
+ usageBudget: data.usageBudget,
3063
+ configToolBudget: data.configToolBudget,
3064
+ capabilityCeiling: data.capabilityCeiling,
3065
+ runFanoutBudget: data.runFanoutBudget,
3066
+ globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
3067
+ }));
3068
+ } finally {
3069
+ finalizeClarifiedBackgroundCapacity(data, deps, capacity.activeAsyncCapacity);
3070
+ }
2896
3071
  }
2897
3072
 
2898
3073
  const rawChainDetails = chainResult.details ? compactOptional<Details>({ ...chainResult.details, runId, timeoutMs: data.timeoutMs }) : undefined;
@@ -2937,6 +3112,7 @@ interface ForegroundParallelRunInput {
2937
3112
  intercomEvents: IntercomEventBus;
2938
3113
  parentSessionId: string | null;
2939
3114
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
3115
+ runFanoutBudget: RunFanoutBudgetDescriptor;
2940
3116
  signal: AbortSignal;
2941
3117
  runId: string;
2942
3118
  sessionDirForIndex: (idx?: number) => string | undefined;
@@ -2986,6 +3162,48 @@ function buildParallelModeError(message: string): AgentToolResult<Details> {
2986
3162
  };
2987
3163
  }
2988
3164
 
3165
+ function acquireClarifiedBackgroundCapacity(
3166
+ data: ExecutionContextData,
3167
+ deps: ExecutorDeps,
3168
+ mode: "single" | "parallel" | "chain",
3169
+ asyncRunId: string,
3170
+ ): { activeAsyncCapacity?: ActiveAsyncCapacityHandle; errorResult?: AgentToolResult<Details> } {
3171
+ if (!data.topLevelAsyncCapacityEligible || !data.parentSessionId) return {};
3172
+ const liveWorkflowRunIds = new Set(deps.state.workflowControllers?.keys() ?? []);
3173
+ const activeLimit = resolveMaxActiveAsyncRunsPerSession(deps.config.maxActiveAsyncRunsPerSession);
3174
+ try {
3175
+ const activeAsyncCapacity = acquireActiveAsyncCapacity({
3176
+ sessionId: data.parentSessionId,
3177
+ limit: activeLimit,
3178
+ runId: asyncRunId,
3179
+ kind: "runner",
3180
+ asyncDir: path.join(DIRS.async, asyncRunId),
3181
+ }, { liveWorkflowRunIds });
3182
+ deps.state.activeAsyncCapacity = getActiveAsyncCapacitySnapshot(data.parentSessionId, activeLimit, { liveWorkflowRunIds });
3183
+ return { activeAsyncCapacity };
3184
+ } catch (error) {
3185
+ if (error instanceof ActiveAsyncCapacityError) {
3186
+ deps.state.activeAsyncCapacity = error.snapshot;
3187
+ return { errorResult: { content: [{ type: "text", text: error.message }], isError: true, details: { mode, results: [], activeAsyncCapacity: error.snapshot } } };
3188
+ }
3189
+ throw error;
3190
+ }
3191
+ }
3192
+
3193
+ function finalizeClarifiedBackgroundCapacity(
3194
+ data: ExecutionContextData,
3195
+ deps: ExecutorDeps,
3196
+ activeAsyncCapacity: ActiveAsyncCapacityHandle | undefined,
3197
+ ): void {
3198
+ if (!activeAsyncCapacity || !data.parentSessionId) return;
3199
+ if (!activeAsyncCapacity.owner.runnerStartedAt) activeAsyncCapacity.rollback();
3200
+ deps.state.activeAsyncCapacity = getActiveAsyncCapacitySnapshot(
3201
+ data.parentSessionId,
3202
+ resolveMaxActiveAsyncRunsPerSession(deps.config.maxActiveAsyncRunsPerSession),
3203
+ { liveWorkflowRunIds: new Set(deps.state.workflowControllers?.keys() ?? []) },
3204
+ );
3205
+ }
3206
+
2989
3207
  function createParallelWorktreeSetup(
2990
3208
  enabled: boolean | undefined,
2991
3209
  cwd: string,
@@ -3174,7 +3392,33 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
3174
3392
  beginForegroundChild(input.foregroundControl, {
3175
3393
  index,
3176
3394
  agent: task.agent,
3177
- ...(input.taskDescriptions[index] === undefined ? {} : { description: input.taskDescriptions[index] }),
3395
+ authoredTask: input.taskDescriptions[index] ?? "",
3396
+ effectivePrompt: taskText,
3397
+ cwd: taskCwd,
3398
+ ...(outputPath ? { outputPath } : {}),
3399
+ rerun: { params: compactOptional<Record<string, unknown>>({
3400
+ agent: task.agent,
3401
+ task: input.taskDescriptions[index] ?? "",
3402
+ async: false,
3403
+ cwd: input.worktreeSetup ? undefined : taskCwd,
3404
+ worktree: input.worktreeSetup ? true : false,
3405
+ context: input.contextPolicy.contextForAgent(task.agent),
3406
+ share: input.shareEnabled || undefined,
3407
+ maxOutput: input.maxOutput,
3408
+ timeoutMs: input.timeoutMs,
3409
+ turnBudget: input.turnBudget,
3410
+ ...(model ? { model } : {}),
3411
+ ...(effectiveSkills !== undefined ? { skill: effectiveSkills } : {}),
3412
+ ...(outputPath ? { output: outputPath } : {}),
3413
+ ...(behavior?.outputMode !== undefined ? { outputMode: behavior.outputMode } : {}),
3414
+ ...(behavior?.reads !== undefined ? { reads: behavior.reads } : {}),
3415
+ ...(behavior?.progress !== undefined ? { progress: behavior.progress } : {}),
3416
+ ...(task.toolBudget !== undefined ? { toolBudget: task.toolBudget } : {}),
3417
+ ...(task.outputSchema !== undefined ? { outputSchema: task.outputSchema } : {}),
3418
+ ...(task.acceptance !== undefined ? { acceptance: task.acceptance } : {}),
3419
+ ...(task.agentContract !== undefined || input.agentContract !== undefined ? { agentContract: task.agentContract ?? input.agentContract } : {}),
3420
+ }) },
3421
+ description: `${task.agent} child`,
3178
3422
  ...(model ? { model } : {}),
3179
3423
  ...(thinking ? { thinking } : {}),
3180
3424
  interrupt: () => {
@@ -3191,8 +3435,10 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
3191
3435
  const result = await runSync(input.ctx.cwd, input.agents, task.agent, taskText, compactOptional<Parameters<typeof runSync>[4]>({
3192
3436
  permissions: input.permissions,
3193
3437
  parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
3438
+ llmIntentArbiter: createTaskMutationArbiter(input.ctx),
3194
3439
  ...workflowForegroundSteeringLaunchOptions(input.foregroundControl, index),
3195
3440
  context: input.contextPolicy.contextForAgent(task.agent),
3441
+ runFanoutBudget: { ...input.runFanoutBudget, parentPath: `${input.runFanoutBudget.parentPath ? `${input.runFanoutBudget.parentPath}/` : ""}tasks[${index}]` },
3196
3442
  cwd: taskCwd,
3197
3443
  signal: input.signal,
3198
3444
  interruptSignal: interruptController.signal,
@@ -3237,6 +3483,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
3237
3483
  agentContract: task.agentContract ?? input.agentContract,
3238
3484
  acceptance: task.acceptance,
3239
3485
  acceptanceContext: { mode: "parallel" },
3486
+ onEffectivePrompt: input.foregroundControl ? (prompt) => updateLiveEffectivePrompt(input.foregroundControl!, index, prompt) : undefined,
3240
3487
  timeoutMs: input.timeoutMs,
3241
3488
  deadlineAt: input.deadlineAt,
3242
3489
  turnBudget: input.turnBudget,
@@ -3420,7 +3667,9 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3420
3667
  details: { mode: "parallel" as const, results: [] },
3421
3668
  };
3422
3669
  }
3423
- const id = randomUUID();
3670
+ const id = data.asyncRunId;
3671
+ const capacity = acquireClarifiedBackgroundCapacity(data, deps, "parallel", id);
3672
+ if (capacity.errorResult) return capacity.errorResult;
3424
3673
  const asyncCtx = compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
3425
3674
  pi: deps.pi,
3426
3675
  cwd: ctx.cwd,
@@ -3451,40 +3700,46 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3451
3700
  ...(t.agentContract !== undefined ? { agentContract: t.agentContract } : {}),
3452
3701
  });
3453
3702
  });
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
- }));
3703
+ try {
3704
+ return executeAsyncChain(id, compactOptional<Parameters<typeof executeAsyncChain>[1]>({
3705
+ chain: [compactOptional<ParallelStep>({ parallel: parallelTasks, concurrency: parallelConcurrency, worktree: params.worktree })],
3706
+ resultMode: "parallel",
3707
+ goal: taskTexts[0] ?? "",
3708
+ agents,
3709
+ ctx: asyncCtx,
3710
+ availableModels,
3711
+ cwd: effectiveCwd,
3712
+ maxOutput: params.maxOutput,
3713
+ artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
3714
+ artifactConfig,
3715
+ shareEnabled,
3716
+ sessionRoot,
3717
+ chainSkills: [],
3718
+ activeAsyncCapacity: capacity.activeAsyncCapacity,
3719
+ sessionFilesByFlatIndex: tasks.map((task, index) => sessionFileForTask(task.agent, index, modelOverrides[index])),
3720
+ thinkingOverridesByFlatIndex: tasks.map((task, index) => thinkingOverrideForTask(task.agent, index, modelOverrides[index])),
3721
+ contextForAgent: contextPolicy.contextForAgent,
3722
+ maxSubagentDepth: currentMaxSubagentDepth,
3723
+ waitToolEnabled: deps.waitToolEnabled,
3724
+ worktreeSetupHook: deps.config.worktreeSetupHook,
3725
+ worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
3726
+ worktreeBaseDir: deps.config.worktreeBaseDir,
3727
+ controlConfig,
3728
+ agentContract: params.agentContract,
3729
+ controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
3730
+ childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
3731
+ nestedRoute: data.nestedRoute,
3732
+ timeoutMs: data.timeoutMs,
3733
+ turnBudget: data.turnBudget,
3734
+ usageBudget: data.usageBudget,
3735
+ toolBudget: data.toolBudget,
3736
+ configToolBudget: data.configToolBudget,
3737
+ globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
3738
+ runFanoutBudget: data.runFanoutBudget,
3739
+ }));
3740
+ } finally {
3741
+ finalizeClarifiedBackgroundCapacity(data, deps, capacity.activeAsyncCapacity);
3742
+ }
3488
3743
  }
3489
3744
  }
3490
3745
 
@@ -3563,6 +3818,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3563
3818
  intercomEvents: deps.pi.events,
3564
3819
  parentSessionId: data.parentSessionId,
3565
3820
  capabilityCeiling: data.capabilityCeiling,
3821
+ runFanoutBudget: data.runFanoutBudget,
3566
3822
  signal,
3567
3823
  runId,
3568
3824
  sessionDirForIndex,
@@ -3797,7 +4053,9 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3797
4053
  details: { mode: "single" as const, results: [] },
3798
4054
  };
3799
4055
  }
3800
- const id = randomUUID();
4056
+ const id = data.asyncRunId;
4057
+ const capacity = acquireClarifiedBackgroundCapacity(data, deps, "single", id);
4058
+ if (capacity.errorResult) return capacity.errorResult;
3801
4059
  const asyncCtx = compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
3802
4060
  pi: deps.pi,
3803
4061
  cwd: ctx.cwd,
@@ -3809,49 +4067,56 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3809
4067
  interactive: ctx.hasUI,
3810
4068
  permissions: deps.config.permissions,
3811
4069
  });
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
- }));
4070
+ try {
4071
+ return executeAsyncSingle(id, compactOptional<Parameters<typeof executeAsyncSingle>[1]>({
4072
+ agent: params.agent!,
4073
+ task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(task) : task,
4074
+ goal: task,
4075
+ agentConfig,
4076
+ ctx: asyncCtx,
4077
+ availableModels,
4078
+ cwd: effectiveCwd,
4079
+ maxOutput: params.maxOutput,
4080
+ artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
4081
+ artifactConfig,
4082
+ shareEnabled,
4083
+ activeAsyncCapacity: capacity.activeAsyncCapacity,
4084
+ sessionRoot,
4085
+ sessionFile: sessionFileForTask(params.agent!, 0, modelOverride),
4086
+ context: contextPolicy.contextForAgent(params.agent!),
4087
+ skills: skillOverride === false ? [] : skillOverride,
4088
+ output: effectiveOutput,
4089
+ outputMode: effectiveOutputMode,
4090
+ ...(readsOverride !== undefined ? { reads: readsOverride } : {}),
4091
+ outputBaseDir: resolveSingleRunOutputBaseDir(deps, artifactsDir, id),
4092
+ modelOverride,
4093
+ thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride),
4094
+ maxSubagentDepth,
4095
+ waitToolEnabled: deps.waitToolEnabled,
4096
+ worktreeSetupHook: deps.config.worktreeSetupHook,
4097
+ worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
4098
+ worktreeBaseDir: deps.config.worktreeBaseDir,
4099
+ controlConfig,
4100
+ controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
4101
+ childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
4102
+ nestedRoute: data.nestedRoute,
4103
+ agentContract: params.agentContract,
4104
+ structuredOutputSchema: params.outputSchema,
4105
+ acceptance: params.acceptance,
4106
+ timeoutMs: data.timeoutMs,
4107
+ turnBudget: data.turnBudget,
4108
+ toolBudget: effectiveToolBudget.toolBudget,
4109
+ usageBudget: data.usageBudget,
4110
+ allowZeroToolBudget: data.allowZeroToolBudget && effectiveToolBudget.toolBudget === data.toolBudget,
4111
+ runFanoutBudget: data.runFanoutBudget,
4112
+ }));
4113
+ } finally {
4114
+ finalizeClarifiedBackgroundCapacity(data, deps, capacity.activeAsyncCapacity);
4115
+ }
3852
4116
  }
3853
4117
  }
3854
4118
 
4119
+ const authoredTask = task;
3855
4120
  if (shouldForkAgent(contextPolicy, params.agent!)) {
3856
4121
  task = wrapForkTask(task);
3857
4122
  }
@@ -3888,6 +4153,11 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3888
4153
  beginForegroundChild(foregroundControl, omitUndefinedProperties({
3889
4154
  index: 0,
3890
4155
  agent: params.agent!,
4156
+ authoredTask,
4157
+ effectivePrompt: task,
4158
+ cwd: effectiveCwd,
4159
+ outputPath,
4160
+ rerun: { params: { ...params, task: authoredTask, async: params.async ?? false } },
3891
4161
  description: foregroundControl.description,
3892
4162
  ...(modelOverride ? { model: modelOverride } : {}),
3893
4163
  ...(thinking ? { thinking } : {}),
@@ -3913,8 +4183,10 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3913
4183
  r = await runSync(ctx.cwd, agents, params.agent!, task, compactOptional<Parameters<typeof runSync>[4]>({
3914
4184
  permissions: deps.config.permissions,
3915
4185
  parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
4186
+ llmIntentArbiter: createTaskMutationArbiter(ctx),
3916
4187
  ...workflowForegroundSteeringLaunchOptions(foregroundControl, 0),
3917
4188
  context: data.contextPolicy.contextForAgent(params.agent!),
4189
+ runFanoutBudget: params.runFanoutAdmitted ? data.runFanoutBudget : { ...data.runFanoutBudget, parentPath: `${data.runFanoutBudget.parentPath ? `${data.runFanoutBudget.parentPath}/` : ""}single` },
3918
4190
  cwd: effectiveCwd,
3919
4191
  signal,
3920
4192
  interruptSignal: interruptController.signal,
@@ -3948,6 +4220,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3948
4220
  agentContract: params.agentContract,
3949
4221
  acceptance: params.acceptance,
3950
4222
  acceptanceContext: { mode: "single" },
4223
+ onEffectivePrompt: foregroundControl ? (prompt) => updateLiveEffectivePrompt(foregroundControl, 0, prompt) : undefined,
3951
4224
  onDetachReady: (detach) => {
3952
4225
  detachForeground = detach;
3953
4226
  },
@@ -3996,12 +4269,14 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3996
4269
  if (r.artifactPaths) allArtifactPaths.push(r.artifactPaths);
3997
4270
 
3998
4271
  const fullOutput = getSingleResultOutput(r);
4272
+ const preserveRejectedSavedOutput = r.acceptance?.explicit && r.acceptance.status === "rejected" && r.savedOutputPath !== undefined;
3999
4273
  const finalizedOutput = finalizeSingleOutput(omitUndefinedProperties({
4000
4274
  fullOutput,
4001
4275
  truncatedOutput: r.truncation?.text,
4002
4276
  outputPath,
4003
4277
  outputMode: r.outputMode,
4004
4278
  exitCode: r.exitCode,
4279
+ preserveSavedOutput: preserveRejectedSavedOutput,
4005
4280
  savedPath: r.savedOutputPath,
4006
4281
  outputReference: r.outputReference,
4007
4282
  saveError: r.outputSaveError,
@@ -4092,7 +4367,7 @@ function duplicateSubagentCallResult(params: SubagentParamsLike): AgentToolResul
4092
4367
  };
4093
4368
  }
4094
4369
 
4095
- const workflowLaunchObservers = new WeakMap<object, (launch: { agent: string; sessionFile?: string }) => void>();
4370
+ const workflowLaunchObservers = new WeakMap<object, (launch: { agent: string; sessionFile?: string; async: boolean; runId?: string }) => void>();
4096
4371
 
4097
4372
  function recordMissionWorkflowChild(
4098
4373
  binding: MissionLaunchBinding | undefined,
@@ -4101,8 +4376,9 @@ function recordMissionWorkflowChild(
4101
4376
  update: Omit<MissionWorkflowChildUpdate, "workflowRunId" | "key">,
4102
4377
  ): void {
4103
4378
  if (!binding) return;
4379
+ const { task: _task, ...durableUpdate } = update;
4104
4380
  try {
4105
- updateMission(binding.location, binding.missionId, { upsertWorkflowChildren: [{ workflowRunId, key, ...update }] });
4381
+ updateMission(binding.location, binding.missionId, { upsertWorkflowChildren: [{ workflowRunId, key, ...durableUpdate }] });
4106
4382
  } catch (error) {
4107
4383
  console.warn(`[pi-subagents] Failed to record mission workflow child '${key}': ${error instanceof Error ? error.message : String(error)}`);
4108
4384
  }
@@ -4184,7 +4460,7 @@ export function prepareWorkflowLaunchParams(
4184
4460
  childParams: Record<string, unknown>,
4185
4461
  parentWorkflowRunId: string,
4186
4462
  workflowKey: string,
4187
- options: { missionDetached?: boolean; suppressRoutineResultIntercom?: boolean } = {},
4463
+ options: { missionDetached?: boolean; suppressRoutineResultIntercom?: boolean; runFanoutBudget?: RunFanoutBudgetDescriptor } = {},
4188
4464
  ): SubagentParamsLike {
4189
4465
  if (typeof childParams.resume === "string") {
4190
4466
  if (childParams.gate !== undefined || workflowDefaults.gate !== undefined) {
@@ -4212,6 +4488,7 @@ export function prepareWorkflowLaunchParams(
4212
4488
  ...(options.missionDetached ? { mission: false } : {}),
4213
4489
  workflowParentRunId: parentWorkflowRunId,
4214
4490
  workflowKey,
4491
+ ...(options.runFanoutBudget ? { runFanoutBudget: { ...options.runFanoutBudget, parentPath: `${options.runFanoutBudget.parentPath ? `${options.runFanoutBudget.parentPath}/` : ""}workflow[${workflowKey}]` } } : {}),
4215
4492
  ...(options.suppressRoutineResultIntercom ? { suppressRoutineResultIntercom: true } : {}),
4216
4493
  } as SubagentParamsLike;
4217
4494
  const normalizedGate = normalizeGateParams(launchParams);
@@ -4359,6 +4636,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4359
4636
  } {
4360
4637
  const delegatedThinkingOverrides = new WeakMap<object, AgentConfig["thinking"]>();
4361
4638
  const delegatedZeroToolBudgets = new WeakSet<object>();
4639
+ const delegatedExecutions = new WeakSet<object>();
4362
4640
  const warnedArtifactPackageDirs = new Set<string>();
4363
4641
  const scheduledOwnerExecutors = new Map<string, ReturnType<typeof createSubagentExecutor>>();
4364
4642
  const execute = async (
@@ -4372,6 +4650,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4372
4650
  const workflowLaunchObserver = workflowLaunchObservers.get(params);
4373
4651
  const delegatedThinkingOverride = delegatedThinkingOverrides.get(params);
4374
4652
  const allowZeroToolBudget = delegatedZeroToolBudgets.has(params);
4653
+ const delegatedExecution = delegatedExecutions.has(params);
4375
4654
  if (!preserveActiveSession) deps.state.baseCwd = ctx.cwd;
4376
4655
  deps.state.foregroundRuns ??= new Map();
4377
4656
  deps.state.foregroundControls ??= new Map();
@@ -4382,7 +4661,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4382
4661
  const normalizedAction = typeof requestParams.action === "string" ? requestParams.action.trim() : requestParams.action;
4383
4662
  if (requestParams.workflowScript !== undefined && normalizedAction === undefined) {
4384
4663
  const parentCwd = ctx.cwd;
4385
- const timeout = requestParams.timeoutMs ?? requestParams.maxRuntimeMs ?? (requestParams.async === false ? DEFAULT_FOREGROUND_TIMEOUT_MS : undefined);
4664
+ const timeout = requestParams.timeoutMs ?? requestParams.maxRuntimeMs ?? (requestParams.async === false ? resolveConfigDefaultTimeoutMs(deps.config.timeoutMs) ?? DEFAULT_FOREGROUND_TIMEOUT_MS : undefined);
4386
4665
  const workflowUsageBudget = validateUsageBudgetConfig(requestParams.usageBudget ?? deps.config.usageBudget, requestParams.usageBudget ? "usageBudget" : "config.usageBudget");
4387
4666
  if (workflowUsageBudget.error) return buildRequestedModeError(requestParams, workflowUsageBudget.error);
4388
4667
  const workflowCwd = resolveRequestedCwd(parentCwd, requestParams.cwd);
@@ -4393,11 +4672,45 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4393
4672
  const explicitMission = requestParams.missionId !== undefined || requestParams.mission !== undefined;
4394
4673
  const autoMission = !explicitMission;
4395
4674
  const workflowPreview = autoMission ? previewSimpleWorkflowRun(requestParams.workflowScript) : undefined;
4396
- const previewTask = workflowPreview?.task?.trim() || undefined;
4397
4675
  const previewAgent = workflowPreview?.agent?.trim() || undefined;
4398
4676
  const scriptFirstLine = requestParams.workflowScript.split(/\r?\n/).map((line) => line.trim()).find(Boolean) || "Workflow";
4399
4677
  const boundedScriptPreview = scriptFirstLine.length > 100 ? `${scriptFirstLine.slice(0, 97)}...` : scriptFirstLine;
4400
- const derivedObjective = previewTask || (previewAgent ? `Workflow: ${previewAgent}` : boundedScriptPreview);
4678
+ const derivedObjective = previewAgent ? `Workflow: ${previewAgent}` : boundedScriptPreview;
4679
+ const workflowDepth = checkSubagentDepth(deps.config.maxSubagentDepth).depth;
4680
+ const asyncWorkflow = requestParams.async !== false;
4681
+ const topLevelAsyncWorkflow = asyncWorkflow
4682
+ && workflowDepth === 0
4683
+ && !resolveInheritedNestedRouteFromEnv()
4684
+ && !requestParams.workflowParentRunId;
4685
+ const workflowRunId = asyncWorkflow ? randomUUID() : undefined;
4686
+ let workflowCapacity: ActiveAsyncCapacityHandle | undefined;
4687
+ if (workflowRunId && topLevelAsyncWorkflow) {
4688
+ const currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
4689
+ try {
4690
+ workflowCapacity = acquireActiveAsyncCapacity({
4691
+ sessionId: currentSessionId,
4692
+ limit: resolveMaxActiveAsyncRunsPerSession(deps.config.maxActiveAsyncRunsPerSession),
4693
+ runId: workflowRunId,
4694
+ kind: "workflow",
4695
+ asyncDir: path.join(DIRS.async, workflowRunId),
4696
+ }, { liveWorkflowRunIds: new Set(deps.state.workflowControllers?.keys() ?? []) });
4697
+ } catch (error) {
4698
+ if (error instanceof ActiveAsyncCapacityError) {
4699
+ deps.state.activeAsyncCapacity = error.snapshot;
4700
+ return { content: [{ type: "text", text: error.message }], isError: true, details: { mode: "workflow", results: [], activeAsyncCapacity: error.snapshot } };
4701
+ }
4702
+ throw error;
4703
+ }
4704
+ }
4705
+ let workflowFanoutBudget: RunFanoutBudgetDescriptor;
4706
+ try {
4707
+ workflowFanoutBudget = requestParams.runFanoutBudget
4708
+ ?? decodeRunFanoutBudgetDescriptor(process.env[RUN_FANOUT_BUDGET_ENV])
4709
+ ?? createRunFanoutBudget(_id, resolveMaxSubagentSpawnsPerRun(deps.config.maxSubagentSpawnsPerRun));
4710
+ } catch (error) {
4711
+ workflowCapacity?.rollback();
4712
+ return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, details: { mode: "workflow", results: [] } };
4713
+ }
4401
4714
  let missionBinding: MissionLaunchBinding | undefined;
4402
4715
  let missionWarning: string | undefined;
4403
4716
  try {
@@ -4408,22 +4721,12 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4408
4721
  ownerSessionId: resolveCurrentSessionId(ctx.sessionManager),
4409
4722
  });
4410
4723
  } catch (error) {
4411
- if (explicitMission) return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, details: { mode: "workflow", results: [] } };
4724
+ if (explicitMission) {
4725
+ workflowCapacity?.rollback();
4726
+ return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, details: { mode: "workflow", results: [] } };
4727
+ }
4412
4728
  missionWarning = `Mission tracking unavailable: ${error instanceof Error ? error.message : String(error)}`;
4413
4729
  }
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
4730
  const detachWorkflowChildMissions = autoMission || missionBinding !== undefined || requestParams.mission === false;
4428
4731
  const workflowState = missionBinding ? createMissionWorkflowState(missionBinding.location, missionBinding.missionId) : undefined;
4429
4732
  const attachWorkflowMission = (result: AgentToolResult<Details>): AgentToolResult<Details> => {
@@ -4437,20 +4740,27 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4437
4740
  : { ...result, details: { ...result.details, missionWarning: warning } };
4438
4741
  }
4439
4742
  };
4440
- if (requestParams.async !== false) {
4743
+ if (workflowRunId) {
4441
4744
  const toolCallId = _id;
4442
- const workflowRunId = randomUUID();
4443
4745
  const asyncDir = path.join(DIRS.async, workflowRunId);
4444
4746
  const resultPath = path.join(DIRS.results, `${workflowRunId}.json`);
4445
4747
  const statusPath = path.join(asyncDir, "status.json");
4446
4748
  const eventsPath = path.join(asyncDir, "events.jsonl");
4447
4749
  const startedAt = Date.now();
4448
4750
  const currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
4449
- fs.mkdirSync(asyncDir, { recursive: true });
4450
- fs.mkdirSync(DIRS.results, { recursive: true });
4751
+ try {
4752
+ fs.mkdirSync(asyncDir, { recursive: true });
4753
+ writeRunFanoutBudgetDescriptor(asyncDir, workflowFanoutBudget);
4754
+ fs.mkdirSync(DIRS.results, { recursive: true });
4755
+ } catch (error) {
4756
+ workflowCapacity?.rollback();
4757
+ return { content: [{ type: "text", text: `Failed to create async workflow storage: ${error instanceof Error ? error.message : String(error)}` }], isError: true, details: { mode: "workflow", results: [] } };
4758
+ }
4451
4759
  const controller = new AbortController();
4452
4760
  deps.state.workflowControllers ??= new Map();
4453
4761
  deps.state.workflowControllers.set(workflowRunId, controller);
4762
+ workflowCapacity?.markWorkflowStarted();
4763
+ if (workflowCapacity) deps.state.activeAsyncCapacity = getActiveAsyncCapacitySnapshot(currentSessionId, resolveMaxActiveAsyncRunsPerSession(deps.config.maxActiveAsyncRunsPerSession), { liveWorkflowRunIds: new Set(deps.state.workflowControllers.keys()) });
4454
4764
  let status: AsyncStatus = {
4455
4765
  runId: workflowRunId,
4456
4766
  toolCallId,
@@ -4464,6 +4774,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4464
4774
  pid: process.pid,
4465
4775
  steps: [],
4466
4776
  workflow: { trace: [], emits: [], console: [] },
4777
+ runFanoutBudget: getRunFanoutBudgetSnapshot(workflowFanoutBudget),
4467
4778
  };
4468
4779
  const appendWorkflowEvent = (event: Record<string, unknown>) => fs.appendFileSync(eventsPath, `${JSON.stringify({ ts: Date.now(), runId: workflowRunId, ...event })}\n`, "utf-8");
4469
4780
  let indexedState: AsyncStatus["state"] | undefined;
@@ -4550,12 +4861,23 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4550
4861
  if (existing) {
4551
4862
  existing.status = mapped;
4552
4863
  if (entry.agent) existing.agent = entry.agent;
4864
+ if (entry.runId) existing.runId = entry.runId;
4865
+ if (entry.state === "failed" && !entry.runId && existing.async === undefined) existing.async = false;
4553
4866
  if (entry.error === undefined) delete existing.error;
4554
4867
  else existing.error = entry.error;
4555
4868
  if (entry.durationMs === undefined) delete existing.durationMs;
4556
4869
  else existing.durationMs = entry.durationMs;
4557
4870
  } 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() };
4871
+ const step: NonNullable<AsyncStatus["steps"]>[number] = {
4872
+ agent: entry.agent ?? entry.key,
4873
+ label: entry.key,
4874
+ workflowKey: entry.key,
4875
+ parentWorkflowRunId: workflowRunId,
4876
+ status: mapped,
4877
+ startedAt: Date.now(),
4878
+ ...(entry.runId ? { runId: entry.runId } : {}),
4879
+ ...(entry.state === "failed" && !entry.runId ? { async: false } : {}),
4880
+ };
4559
4881
  status.steps?.push(step);
4560
4882
  workflowSteps.set(entry.key, step);
4561
4883
  }
@@ -4574,30 +4896,32 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4574
4896
  prompts: workflowPrompts,
4575
4897
  ...(workflowState ? { state: workflowState } : {}),
4576
4898
  onTrace: updateTrace,
4899
+ admit: (calls) => {
4900
+ status.runFanoutBudget = claimRunFanoutBatch(workflowFanoutBudget, calls.map(({ key }) => `workflow[${key}]`));
4901
+ persist();
4902
+ },
4577
4903
  onEmit: (emits) => {
4578
4904
  // Each emit is validated at the host boundary in runWorkflowScript before onEmit fires.
4579
4905
  status.workflow = { ...(status.workflow ?? { trace: [], console: [] }), emits };
4580
4906
  persist();
4581
4907
  appendWorkflowEvent({ type: "subagent.workflow.emit", value: emits.at(-1) });
4582
4908
  },
4583
- launch: async (key, childParams, workflowSignal) => {
4909
+ launch: async (key, childParams, workflowSignal, admission) => {
4584
4910
  if (workflowUsageBudget.budget && childParams.async === true) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, "workflow usageBudget does not support async runs.run launches."));
4585
4911
  const budgetState = usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults));
4586
4912
  if (budgetState?.exhausted) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, usageBudgetExceededMessage(budgetState)));
4587
- patchMissionObjective(childParams.task);
4588
4913
  const childPhase = typeof childParams.phase === "string" && childParams.phase.trim() ? childParams.phase.trim() : undefined;
4589
4914
  const childLabel = typeof childParams.label === "string" && childParams.label.trim() ? childParams.label.trim() : undefined;
4590
4915
  recordMissionWorkflowChild(missionBinding, workflowRunId, key, {
4591
4916
  status: "running",
4592
4917
  ...(typeof childParams.agent === "string" && childParams.agent.trim() ? { agent: childParams.agent.trim() } : {}),
4593
- ...(typeof childParams.task === "string" && childParams.task.trim() ? { task: childParams.task.trim() } : {}),
4594
4918
  ...(childLabel ? { label: childLabel } : {}),
4595
4919
  ...(childPhase ? { phase: childPhase } : {}),
4596
4920
  heartbeat: { status: "running", ...(childPhase ? { phase: childPhase } : {}) },
4597
4921
  });
4598
4922
  const result = await runMissionWorkflowChild(missionBinding, workflowRunId, key, childPhase, () => {
4599
4923
  const childRequest = bindMissionWorkflowChildAsyncLaunch(
4600
- prepareWorkflowLaunchParams(workflowChildDefaults, childParams, workflowRunId, key, { missionDetached: detachWorkflowChildMissions }),
4924
+ { ...prepareWorkflowLaunchParams(workflowChildDefaults, childParams, workflowRunId, key, { missionDetached: detachWorkflowChildMissions, runFanoutBudget: workflowFanoutBudget }), runFanoutAdmitted: admission.admitted },
4601
4925
  missionBinding,
4602
4926
  deps.asyncByDefault,
4603
4927
  );
@@ -4606,6 +4930,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4606
4930
  if (step) {
4607
4931
  step.agent = launch.agent;
4608
4932
  step.sessionFile = launch.sessionFile;
4933
+ step.async = launch.async;
4934
+ if (launch.runId) step.runId = launch.runId;
4609
4935
  persist();
4610
4936
  }
4611
4937
  recordMissionWorkflowChild(missionBinding, workflowRunId, key, { status: "running", agent: launch.agent, ...(launch.sessionFile ? { sessionPath: launch.sessionFile } : {}) });
@@ -4637,8 +4963,13 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4637
4963
  }, ctx, preserveActiveSession);
4638
4964
  });
4639
4965
  workflowResults.push(...result.details.results);
4640
- if (result.details.asyncDir && missionBinding) writeMissionAsyncBinding(result.details.asyncDir, missionBinding);
4641
4966
  const child = workflowChildResult(key, result);
4967
+ const step = status.steps?.find((candidate) => candidate.workflowKey === key);
4968
+ if (step) {
4969
+ step.async = Boolean(result.details.asyncId || result.details.asyncDir);
4970
+ if (child.runId) step.runId = child.runId;
4971
+ }
4972
+ if (result.details.asyncDir && missionBinding) writeMissionAsyncBinding(result.details.asyncDir, missionBinding);
4642
4973
  const childStatus = missionWorkflowChildStatus(result);
4643
4974
  recordMissionWorkflowChild(missionBinding, workflowRunId, key, {
4644
4975
  status: childStatus,
@@ -4674,12 +5005,14 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4674
5005
  appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, error: status.error });
4675
5006
  } finally {
4676
5007
  deps.state.workflowControllers?.delete(workflowRunId);
5008
+ deps.state.activeAsyncCapacity = workflowCapacity?.reconcile(new Set(deps.state.workflowControllers?.keys() ?? []))
5009
+ ?? deps.state.activeAsyncCapacity;
4677
5010
  }
4678
5011
  });
4679
- return attachWorkflowMission({
5012
+ return attachWorkflowMission(withRunFanoutBudget({
4680
5013
  content: [{ type: "text", text: formatAsyncStartedMessage(`Async workflow [${workflowRunId}]`, ctx.hasUI === true) }],
4681
- details: { mode: "workflow", runId: workflowRunId, toolCallId, asyncId: workflowRunId, asyncDir, results: [], chatProgress },
4682
- });
5014
+ details: { mode: "workflow", runId: workflowRunId, toolCallId, asyncId: workflowRunId, asyncDir, results: [], chatProgress, ...(deps.state.activeAsyncCapacity ? { activeAsyncCapacity: deps.state.activeAsyncCapacity } : {}) },
5015
+ }, workflowFanoutBudget));
4683
5016
  }
4684
5017
  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;
4685
5018
  const workflowResults: SingleResult[] = [];
@@ -4699,28 +5032,29 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4699
5032
  liveWorkflow = { ...liveWorkflow, trace };
4700
5033
  sendWorkflowProgress();
4701
5034
  },
5035
+ admit: (calls) => {
5036
+ claimRunFanoutBatch(workflowFanoutBudget, calls.map(({ key }) => `workflow[${key}]`));
5037
+ },
4702
5038
  onEmit: (emits) => {
4703
5039
  liveWorkflow = { ...liveWorkflow, emits };
4704
5040
  sendWorkflowProgress();
4705
5041
  },
4706
- launch: async (key, childParams, workflowSignal) => {
5042
+ launch: async (key, childParams, workflowSignal, admission) => {
4707
5043
  if (workflowUsageBudget.budget && childParams.async === true) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, "workflow usageBudget does not support async runs.run launches."));
4708
5044
  const budgetState = usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults));
4709
5045
  if (budgetState?.exhausted) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, usageBudgetExceededMessage(budgetState)));
4710
- patchMissionObjective(childParams.task);
4711
5046
  const childPhase = typeof childParams.phase === "string" && childParams.phase.trim() ? childParams.phase.trim() : undefined;
4712
5047
  const childLabel = typeof childParams.label === "string" && childParams.label.trim() ? childParams.label.trim() : undefined;
4713
5048
  recordMissionWorkflowChild(missionBinding, _id, key, {
4714
5049
  status: "running",
4715
5050
  ...(typeof childParams.agent === "string" && childParams.agent.trim() ? { agent: childParams.agent.trim() } : {}),
4716
- ...(typeof childParams.task === "string" && childParams.task.trim() ? { task: childParams.task.trim() } : {}),
4717
5051
  ...(childLabel ? { label: childLabel } : {}),
4718
5052
  ...(childPhase ? { phase: childPhase } : {}),
4719
5053
  heartbeat: { status: "running", ...(childPhase ? { phase: childPhase } : {}) },
4720
5054
  });
4721
5055
  const result = await runMissionWorkflowChild(missionBinding, _id, key, childPhase, () => {
4722
5056
  const childRequest = bindMissionWorkflowChildAsyncLaunch(
4723
- prepareWorkflowLaunchParams(workflowChildDefaults, childParams, _id, key, { missionDetached: detachWorkflowChildMissions, suppressRoutineResultIntercom: chatProgress.mode === "live-card" }),
5057
+ { ...prepareWorkflowLaunchParams(workflowChildDefaults, childParams, _id, key, { missionDetached: detachWorkflowChildMissions, suppressRoutineResultIntercom: chatProgress.mode === "live-card", runFanoutBudget: workflowFanoutBudget }), runFanoutAdmitted: admission.admitted },
4724
5058
  missionBinding,
4725
5059
  deps.asyncByDefault,
4726
5060
  );
@@ -4761,10 +5095,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4761
5095
  if (workflow.emits.length > 0) sections.push(`Emitted:\n${workflow.emits.map(formatWorkflowValue).join("\n")}`);
4762
5096
  if (workflow.console.length > 0) sections.push(`Console:\n${workflow.console.map((entry) => `[${entry.level}] ${entry.text}`).join("\n")}`);
4763
5097
  if (traceLines.length > 0) sections.push(`Call trace:\n${traceLines.join("\n")}`);
4764
- return attachWorkflowMission({
5098
+ return attachWorkflowMission(withRunFanoutBudget({
4765
5099
  content: [{ type: "text", text: sections.join("\n\n") }],
4766
5100
  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
- });
5101
+ }, workflowFanoutBudget));
4768
5102
  } catch (error) {
4769
5103
  const partial = error instanceof WorkflowScriptError ? error.partial : { trace: [], emits: [], console: [], children: [] };
4770
5104
  const text = error instanceof Error ? error.message : String(error);
@@ -4773,11 +5107,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4773
5107
  if (partial.emits.length > 0) sections.push(`Emitted:\n${partial.emits.map(formatWorkflowValue).join("\n")}`);
4774
5108
  if (partial.console.length > 0) sections.push(`Console:\n${partial.console.map((entry) => `[${entry.level}] ${entry.text}`).join("\n")}`);
4775
5109
  if (traceLines.length > 0) sections.push(`Call trace:\n${traceLines.join("\n")}`);
4776
- return attachWorkflowMission({
5110
+ return attachWorkflowMission(withRunFanoutBudget({
4777
5111
  content: [{ type: "text", text: sections.join("\n\n") }],
4778
5112
  isError: true,
4779
5113
  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
- });
5114
+ }, workflowFanoutBudget));
4781
5115
  }
4782
5116
  }
4783
5117
  const directParams = prepareWorkflowChildParams(requestParams);
@@ -5015,6 +5349,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5015
5349
  if (!sessionError) sessionError = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
5016
5350
  }
5017
5351
  const spawnBudget = getSpawnBudgetSnapshot(deps.state, deps.config, currentSessionId);
5352
+ const activeAsyncCapacity = currentSessionId
5353
+ ? getActiveAsyncCapacitySnapshot(currentSessionId, resolveMaxActiveAsyncRunsPerSession(deps.config.maxActiveAsyncRunsPerSession), { liveWorkflowRunIds: new Set(deps.state.workflowControllers?.keys() ?? []) })
5354
+ : { used: 0, limit: resolveMaxActiveAsyncRunsPerSession(deps.config.maxActiveAsyncRunsPerSession) ?? 0 };
5355
+ deps.state.activeAsyncCapacity = activeAsyncCapacity;
5018
5356
  return {
5019
5357
  content: [{
5020
5358
  type: "text",
@@ -5031,7 +5369,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5031
5369
  expandTilde: deps.expandTilde,
5032
5370
  })),
5033
5371
  }],
5034
- details: { mode: "management", results: [], spawnBudget },
5372
+ details: { mode: "management", results: [], spawnBudget, activeAsyncCapacity },
5035
5373
  };
5036
5374
  }
5037
5375
  if (action === "status") {
@@ -5448,7 +5786,6 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5448
5786
  const runId = randomUUID().slice(0, 8);
5449
5787
  const inheritedNestedRoute = resolveInheritedNestedRouteFromEnv();
5450
5788
  const nestedParentAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
5451
- const nestedRoute = inheritedNestedRoute ?? createNestedRoute(runId);
5452
5789
  const shareEnabled = effectiveParams.share === true;
5453
5790
  const hasChain = (effectiveParams.chain?.length ?? 0) > 0;
5454
5791
  const hasTasks = (effectiveParams.tasks?.length ?? 0) > 0;
@@ -5532,9 +5869,50 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5532
5869
  const foregroundTimeout = resolveSingleAgentLaunchTimeout(
5533
5870
  effectiveParams,
5534
5871
  effectiveAsync,
5872
+ resolveConfigDefaultTimeoutMs(deps.config.timeoutMs),
5535
5873
  );
5536
5874
  if (foregroundTimeout.error) return buildRequestedModeError(effectiveParams, foregroundTimeout.error);
5537
5875
  const controlConfig = resolveControlConfig(deps.config.control, effectiveParams.control);
5876
+ const requestedWorkflowChildAsyncId = typeof effectiveParams.workflowChildAsyncId === "string" ? effectiveParams.workflowChildAsyncId.trim() : "";
5877
+ const asyncRunId = requestedWorkflowChildAsyncId && path.basename(requestedWorkflowChildAsyncId) === requestedWorkflowChildAsyncId
5878
+ ? requestedWorkflowChildAsyncId
5879
+ : randomUUID();
5880
+ const topLevelAsyncCapacityEligible = depth === 0 && !inheritedNestedRoute && !effectiveParams.workflowParentRunId;
5881
+ const topLevelAsync = effectiveAsync && topLevelAsyncCapacityEligible;
5882
+ let activeAsyncCapacity: ActiveAsyncCapacityHandle | undefined;
5883
+ if (topLevelAsync) {
5884
+ try {
5885
+ const activeLimit = resolveMaxActiveAsyncRunsPerSession(deps.config.maxActiveAsyncRunsPerSession);
5886
+ activeAsyncCapacity = acquireActiveAsyncCapacity({
5887
+ sessionId: requestSessionId,
5888
+ limit: activeLimit,
5889
+ runId: asyncRunId,
5890
+ kind: "runner",
5891
+ asyncDir: path.join(DIRS.async, asyncRunId),
5892
+ }, { liveWorkflowRunIds: new Set(deps.state.workflowControllers?.keys() ?? []) });
5893
+ deps.state.activeAsyncCapacity = getActiveAsyncCapacitySnapshot(requestSessionId, activeLimit, { liveWorkflowRunIds: new Set(deps.state.workflowControllers?.keys() ?? []) });
5894
+ } catch (error) {
5895
+ if (error instanceof ActiveAsyncCapacityError) {
5896
+ deps.state.activeAsyncCapacity = error.snapshot;
5897
+ return { content: [{ type: "text", text: error.message }], isError: true, details: { mode: foregroundMode, results: [], activeAsyncCapacity: error.snapshot } };
5898
+ }
5899
+ throw error;
5900
+ }
5901
+ }
5902
+
5903
+ let runFanoutBudget: RunFanoutBudgetDescriptor;
5904
+ try {
5905
+ const inheritedRunFanoutBudget = effectiveParams.runFanoutBudget ? undefined : decodeRunFanoutBudgetDescriptor(process.env[RUN_FANOUT_BUDGET_ENV]);
5906
+ runFanoutBudget = effectiveParams.runFanoutBudget
5907
+ ?? (inheritedRunFanoutBudget ? { ...inheritedRunFanoutBudget, parentPath: `${inheritedRunFanoutBudget.parentPath ? `${inheritedRunFanoutBudget.parentPath}/` : ""}${runId}` } : undefined)
5908
+ ?? createRunFanoutBudget(runId, resolveMaxSubagentSpawnsPerRun(deps.config.maxSubagentSpawnsPerRun));
5909
+ if (!effectiveParams.runFanoutAdmitted) claimRunFanoutBatch(runFanoutBudget, staticRunFanoutPaths(effectiveParams));
5910
+ } catch (error) {
5911
+ activeAsyncCapacity?.rollback();
5912
+ if (error instanceof RunFanoutLimitError) return runFanoutErrorResult(error, foregroundMode);
5913
+ return buildRequestedModeError(effectiveParams, error instanceof Error ? error.message : String(error));
5914
+ }
5915
+ const nestedRoute = inheritedNestedRoute ?? createNestedRoute(runId);
5538
5916
 
5539
5917
  const artifactConfig: ArtifactConfig = omitUndefinedProperties({
5540
5918
  ...DEFAULT_ARTIFACT_CONFIG,
@@ -5560,6 +5938,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5560
5938
  try {
5561
5939
  fs.mkdirSync(sessionRoot, { recursive: true });
5562
5940
  } catch (error) {
5941
+ activeAsyncCapacity?.rollback();
5563
5942
  const message = error instanceof Error ? error.message : String(error);
5564
5943
  return toExecutionErrorResult(
5565
5944
  effectiveParams,
@@ -5590,10 +5969,14 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5590
5969
  preflightForkSessionsForStaticTasks(effectiveParams, contextPolicy, forkSessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems);
5591
5970
  }
5592
5971
  } catch (error) {
5972
+ activeAsyncCapacity?.rollback();
5593
5973
  return toExecutionErrorResult(effectiveParams, error, contextPolicy.contextSummary);
5594
5974
  }
5595
5975
  const chainBindingsError = validateExecutionChainBindings(effectiveParams, deps.config.chain?.dynamicFanout?.maxItems);
5596
- if (chainBindingsError) return withResolvedContext(chainBindingsError, contextPolicy.contextSummary);
5976
+ if (chainBindingsError) {
5977
+ activeAsyncCapacity?.rollback();
5978
+ return withResolvedContext(chainBindingsError, contextPolicy.contextSummary);
5979
+ }
5597
5980
 
5598
5981
  const onUpdateWithContext = onUpdate
5599
5982
  ? (r: AgentToolResult<Details>) => onUpdate(withResolvedContext({
@@ -5613,14 +5996,17 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5613
5996
  ownerSessionId: requestSessionId,
5614
5997
  });
5615
5998
  } catch (error) {
5616
- if (explicitMission) return toExecutionErrorResult(effectiveParams, error, contextPolicy.contextSummary);
5999
+ if (explicitMission) {
6000
+ activeAsyncCapacity?.rollback();
6001
+ return toExecutionErrorResult(effectiveParams, error, contextPolicy.contextSummary);
6002
+ }
5617
6003
  missionWarning = `Mission tracking unavailable: ${error instanceof Error ? error.message : String(error)}`;
5618
6004
  }
5619
6005
 
5620
6006
  const attachMission = (result: AgentToolResult<Details>): AgentToolResult<Details> => {
5621
6007
  if (!missionBinding) return missionWarning ? { ...result, details: { ...result.details, missionWarning } } : result;
5622
6008
  try {
5623
- return attachMissionToLaunchResult({ binding: missionBinding, result });
6009
+ return attachMissionToLaunchResult({ binding: delegatedExecution ? { ...missionBinding, announceInContent: false } : missionBinding, result });
5624
6010
  } catch (error) {
5625
6011
  const warning = `Mission tracking unavailable after launch: ${error instanceof Error ? error.message : String(error)}`;
5626
6012
  if (explicitMission) {
@@ -5641,7 +6027,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5641
6027
  requestSessionId,
5642
6028
  requestedSpawns,
5643
6029
  );
5644
- if (reservation.error) return attachMission(spawnBudgetErrorResult(reservation.error, foregroundMode));
6030
+ if (reservation.error) {
6031
+ activeAsyncCapacity?.rollback();
6032
+ return attachMission(spawnBudgetErrorResult(reservation.error, foregroundMode));
6033
+ }
5645
6034
 
5646
6035
  const execData: ExecutionContextData = omitUndefinedProperties({
5647
6036
  params: effectiveParams,
@@ -5661,6 +6050,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5661
6050
  artifactsDir,
5662
6051
  backgroundRequestedWhileClarifying,
5663
6052
  effectiveAsync,
6053
+ asyncRunId,
5664
6054
  controlConfig,
5665
6055
  intercomBridge,
5666
6056
  nestedRoute,
@@ -5676,11 +6066,14 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5676
6066
  parentSessionId: requestSessionId,
5677
6067
  parentPiSessionId: requestPiSessionId,
5678
6068
  capabilityCeiling: resolveCurrentSubagentCapabilityCeiling(requestSessionId),
6069
+ runFanoutBudget,
6070
+ topLevelAsyncCapacityEligible,
6071
+ activeAsyncCapacity,
5679
6072
  });
5680
6073
 
5681
- const foregroundDescription = effectiveParams.task?.trim()
5682
- || effectiveParams.tasks?.[0]?.task?.trim()
5683
- || (effectiveParams.chain ? firstRawChainTask(effectiveParams.chain)?.trim() : undefined);
6074
+ const foregroundDescription = selectedAgentNames.length === 1
6075
+ ? `${selectedAgentNames[0]} child`
6076
+ : `${selectedAgentNames.length} live children`;
5684
6077
  const parentWorkflowStatus = effectiveParams.workflowParentRunId
5685
6078
  ? readStatus(path.join(DIRS.async, effectiveParams.workflowParentRunId))
5686
6079
  : null;
@@ -5715,6 +6108,37 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5715
6108
  interrupt: undefined,
5716
6109
  });
5717
6110
  if (foregroundControl) {
6111
+ foregroundControl.promptAuditRedo = async (index, guidance) => {
6112
+ const audit = getLivePromptAudit(foregroundControl, index);
6113
+ if (!audit?.rerun) return { text: "Redo is not safe for this prompt in this slice.", isError: true };
6114
+ const rewrittenTask = await rewritePromptWithGuidance({
6115
+ ctx,
6116
+ authoredTask: audit.authoredTask,
6117
+ runtimeAdditions: audit.runtimeAdditions,
6118
+ finalEffectivePrompt: audit.finalEffectivePrompt,
6119
+ guidance,
6120
+ signal,
6121
+ });
6122
+ const redoParams = promptAuditRedoParams(audit.rerun.params, rewrittenTask);
6123
+ const previousForegroundId = deps.state.lastForegroundControlId;
6124
+ const launch = execute(randomUUID(), redoParams, signal, undefined, ctx, true);
6125
+ const newRunId = deps.state.lastForegroundControlId && deps.state.lastForegroundControlId !== previousForegroundId
6126
+ ? deps.state.lastForegroundControlId
6127
+ : undefined;
6128
+ if (!newRunId) {
6129
+ const result = await launch;
6130
+ return { text: result.content.find((item) => item.type === "text")?.text ?? "Prompt redo could not start.", isError: true };
6131
+ }
6132
+ foregroundControl.supersededByRunId = newRunId;
6133
+ const replacement = deps.state.foregroundControls.get(newRunId);
6134
+ if (replacement) replacement.sourceRunId = foregroundControl.runId;
6135
+ void launch.then((result) => {
6136
+ if (result.isError) console.warn(`[pi-subagents] Prompt redo ${newRunId} failed: ${result.content.find((item) => item.type === "text")?.text ?? "unknown error"}`);
6137
+ }).catch((error) => {
6138
+ console.warn(`[pi-subagents] Prompt redo ${newRunId} failed: ${error instanceof Error ? error.message : String(error)}`);
6139
+ });
6140
+ return { text: `Prompt redo started ${newRunId}.` };
6141
+ };
5718
6142
  deps.state.foregroundControls.set(runId, foregroundControl);
5719
6143
  deps.state.lastForegroundControlId = runId;
5720
6144
  }
@@ -5809,9 +6233,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5809
6233
  if (workflowLaunchObserver) {
5810
6234
  const singleTask = hasTasks && effectiveParams.tasks?.length === 1 ? effectiveParams.tasks[0] : undefined;
5811
6235
  const launch = hasSingle
5812
- ? { agent: effectiveParams.agent!, sessionFile: childSessionFileForTask(effectiveParams.agent!, 0, effectiveParams.model) }
6236
+ ? { agent: effectiveParams.agent!, sessionFile: childSessionFileForTask(effectiveParams.agent!, 0, effectiveParams.model), async: effectiveAsync, ...(effectiveAsync ? { runId: asyncRunId } : {}) }
5813
6237
  : singleTask
5814
- ? { agent: singleTask.agent, sessionFile: childSessionFileForTask(singleTask.agent, 0, singleTask.model) }
6238
+ ? { agent: singleTask.agent, sessionFile: childSessionFileForTask(singleTask.agent, 0, singleTask.model), async: effectiveAsync, ...(effectiveAsync ? { runId: asyncRunId } : {}) }
5815
6239
  : undefined;
5816
6240
  if (launch) {
5817
6241
  workflowLaunchObservers.delete(params);
@@ -5819,31 +6243,33 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5819
6243
  }
5820
6244
  }
5821
6245
  const asyncResult = runAsyncPath(execData, deps);
5822
- if (asyncResult) return attachMission(withResolvedContext(withForkThinkingNotes(asyncResult, forkThinkingDowngrades), contextPolicy.contextSummary));
6246
+ if (asyncResult) return attachMission(withRunFanoutBudget(withResolvedContext(withForkThinkingNotes(asyncResult, forkThinkingDowngrades), contextPolicy.contextSummary), runFanoutBudget));
5823
6247
  if (foregroundControl) {
5824
6248
  writeNestedForegroundEvent("subagent.nested.started");
5825
6249
  nestedForegroundStarted = true;
5826
6250
  }
6251
+ const runFanoutAnnotateContent = !delegatedExecution;
5827
6252
  if (hasChain && effectiveParams.chain) {
5828
6253
  const result = await runChainPath(execData, deps);
5829
6254
  writeNestedForegroundEvent("subagent.nested.completed", result);
5830
- return attachMission(withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary));
6255
+ return attachMission(withRunFanoutBudget(withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary), runFanoutBudget, { annotateContent: runFanoutAnnotateContent }));
5831
6256
  }
5832
6257
  if (hasTasks && effectiveParams.tasks) {
5833
6258
  const result = await runParallelPath(execData, deps);
5834
6259
  writeNestedForegroundEvent("subagent.nested.completed", result);
5835
- return attachMission(withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary));
6260
+ return attachMission(withRunFanoutBudget(withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary), runFanoutBudget, { annotateContent: runFanoutAnnotateContent }));
5836
6261
  }
5837
6262
  if (hasSingle) {
5838
6263
  const result = await runSinglePath(execData, deps);
5839
6264
  writeNestedForegroundEvent("subagent.nested.completed", result);
5840
- return attachMission(withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary));
6265
+ return attachMission(withRunFanoutBudget(withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary), runFanoutBudget, { annotateContent: runFanoutAnnotateContent }));
5841
6266
  }
5842
6267
  } catch (error) {
5843
6268
  const errorResult = withForkThinkingNotes(toExecutionErrorResult(effectiveParams, error, contextPolicy.contextSummary), forkThinkingDowngrades);
5844
6269
  if (nestedForegroundStarted) writeNestedForegroundEvent("subagent.nested.completed", errorResult);
5845
6270
  return attachMission(errorResult);
5846
6271
  } finally {
6272
+ if (activeAsyncCapacity && !activeAsyncCapacity.owner.runnerStartedAt) activeAsyncCapacity.rollback();
5847
6273
  if (foregroundControl) {
5848
6274
  settleForegroundSchedulingOwner(foregroundControl);
5849
6275
  removeForegroundControlIfIdle(deps.state, runId);
@@ -5912,6 +6338,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5912
6338
  delete privateParams.delegatedAllowZeroToolBudget;
5913
6339
  if (thinkingOverride !== undefined) delegatedThinkingOverrides.set(delegatedParams, thinkingOverride);
5914
6340
  if (allowZeroToolBudget) delegatedZeroToolBudgets.add(delegatedParams);
6341
+ delegatedExecutions.add(delegatedParams);
5915
6342
  return execute(id, delegatedParams, signal, onUpdate, ctx);
5916
6343
  };
5917
6344