pi-subagents 0.47.0 → 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 (57) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +2 -0
  3. package/agents/reviewer.md +3 -4
  4. package/docs/configuration.md +46 -2
  5. package/docs/observability.md +1 -1
  6. package/docs/tool-reference.md +1 -1
  7. package/package.json +1 -1
  8. package/src/agents/agents.ts +9 -3
  9. package/src/extension/config.ts +16 -0
  10. package/src/extension/doctor.ts +40 -0
  11. package/src/extension/index.ts +30 -11
  12. package/src/extension/public-execution.ts +5 -0
  13. package/src/extension/rpc.ts +7 -9
  14. package/src/extension/schemas.ts +4 -4
  15. package/src/intercom/intercom-bridge.ts +4 -1
  16. package/src/intercom/native-supervisor-channel.ts +102 -4
  17. package/src/missions/lifecycle.ts +4 -7
  18. package/src/missions/store.ts +4 -4
  19. package/src/missions/workflow-state.ts +6 -2
  20. package/src/runs/background/active-async-capacity.ts +374 -0
  21. package/src/runs/background/active-run-index.ts +46 -0
  22. package/src/runs/background/async-execution.ts +117 -29
  23. package/src/runs/background/async-job-tracker.ts +279 -134
  24. package/src/runs/background/async-resume.ts +7 -1
  25. package/src/runs/background/async-status.ts +33 -4
  26. package/src/runs/background/chain-append.ts +33 -15
  27. package/src/runs/background/control-channel.ts +55 -17
  28. package/src/runs/background/owned-process-tree.ts +104 -0
  29. package/src/runs/background/process-terminal.ts +17 -3
  30. package/src/runs/background/result-watcher.ts +87 -6
  31. package/src/runs/background/run-status.ts +23 -1
  32. package/src/runs/background/scheduled-runs.ts +23 -0
  33. package/src/runs/background/stale-run-reconciler.ts +10 -3
  34. package/src/runs/background/subagent-runner.ts +75 -33
  35. package/src/runs/foreground/async-dismiss-action.ts +85 -0
  36. package/src/runs/foreground/async-steering-action.ts +6 -7
  37. package/src/runs/foreground/chain-execution.ts +37 -2
  38. package/src/runs/foreground/execution.ts +90 -19
  39. package/src/runs/foreground/foreground-control.ts +12 -0
  40. package/src/runs/foreground/prompt-audit.ts +171 -0
  41. package/src/runs/foreground/subagent-executor.ts +648 -195
  42. package/src/runs/shared/acceptance.ts +13 -4
  43. package/src/runs/shared/llm-intent-arbiter.ts +286 -0
  44. package/src/runs/shared/parallel-utils.ts +2 -0
  45. package/src/runs/shared/pi-args.ts +44 -1
  46. package/src/runs/shared/run-fanout-budget.ts +280 -0
  47. package/src/runs/shared/single-output.ts +4 -2
  48. package/src/runs/shared/subagent-prompt-runtime.ts +24 -5
  49. package/src/runs/shared/task-intent.ts +19 -3
  50. package/src/runs/shared/worktree.ts +17 -5
  51. package/src/shared/artifacts.ts +1 -1
  52. package/src/shared/file-coalescer.ts +9 -0
  53. package/src/shared/types.ts +97 -1
  54. package/src/shared/utils.ts +3 -1
  55. package/src/tui/fleet-status.ts +7 -5
  56. package/src/tui/fleet.ts +225 -12
  57. package/src/workflows/scripted-workflow.ts +26 -6
@@ -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";
@@ -53,6 +54,8 @@ import {
53
54
  } from "../../shared/settings.ts";
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";
57
+ import { updateActiveRunIndex } from "../background/active-run-index.ts";
58
+ import { acquireActiveAsyncCapacity, ActiveAsyncCapacityError, getActiveAsyncCapacitySnapshot, resolveMaxActiveAsyncRunsPerSession, transferActiveAsyncCapacity, type ActiveAsyncCapacityHandle } from "../background/active-async-capacity.ts";
56
59
  import { isScheduledRunAction, type ScheduledRunAction } from "../background/scheduled-runs.ts";
57
60
  import { enqueueChainAppendRequest, readPendingChainAppendRequests, runnerStepOutputNames } from "../background/chain-append.ts";
58
61
  import { ChainOutputValidationError, validateChainOutputBindingsWithContext } from "../shared/chain-outputs.ts";
@@ -63,6 +66,7 @@ import { applyIntercomBridgeToAgent, INTERCOM_BRIDGE_MARKER, resolveIntercomBrid
63
66
  import { formatControlIntercomMessage, formatControlNoticeMessage, resolveControlConfig, shouldNotifyControlEvent } from "../shared/subagent-control.ts";
64
67
  import { resolveTurnBudgetConfig } from "../shared/turn-budget.ts";
65
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";
66
70
  import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
67
71
  import { usageBudgetExceededMessage, usageBudgetState, validateUsageBudgetConfig } from "../shared/usage-budget.ts";
68
72
  import { intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
@@ -70,6 +74,7 @@ import { isAgentContractV1 } from "../shared/agent-contract.ts";
70
74
  import { finalizeSingleOutput, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
71
75
  import { cleanupStructuredOutputRuntime, createStructuredOutputRuntime } from "../shared/structured-output.ts";
72
76
  import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, readStatus, resolveChildCwd, sumResultsCost, sumResultsUsage } from "../../shared/utils.ts";
77
+ import { createTaskMutationArbiter } from "../shared/llm-intent-arbiter.ts";
73
78
  import { DEFAULT_GLOBAL_CONCURRENCY_LIMIT, Semaphore } from "../shared/parallel-utils.ts";
74
79
  import { discardPreservedWorktrees, formatParallelHandoffError, formatParallelHandoffReference, parallelHandoffPath, writeParallelHandoffGroup, writePendingParallelHandoff } from "../shared/parallel-handoff.ts";
75
80
  import { summarizeContextModes, type ContextMode, type ContextSummary } from "../shared/context-mode.ts";
@@ -93,6 +98,7 @@ import {
93
98
  workflowForegroundSteeringLaunchOptions,
94
99
  } from "./workflow-foreground-steering.ts";
95
100
  import { stopAsyncRun } from "./async-stop-action.ts";
101
+ import { dismissRecoveredWorkflow } from "./async-dismiss-action.ts";
96
102
  import { reconcileAsyncRun } from "../background/stale-run-reconciler.ts";
97
103
  import { resolveAsyncRootResultPath, waitForImportedAsyncRoot } from "../background/chain-root-attachment.ts";
98
104
  import { attachRootChildrenToSteps, createNestedRoute, findNestedControlResult, resolveInheritedNestedRouteFromEnv, resolveNestedAsyncDir, resolveNestedParentAddressFromEnv, snapshotNestedEventFiles, updateForegroundNestedProjection, writeNestedControlRequest, writeNestedEvent, type NestedRunResolutionScope } from "../shared/nested-events.ts";
@@ -141,6 +147,7 @@ import {
141
147
  type ResolvedControlConfig,
142
148
  type ResolvedTurnBudget,
143
149
  type ResolvedToolBudget,
150
+ type RunFanoutBudgetDescriptor,
144
151
  type SingleResult,
145
152
  type ToolBudgetConfig,
146
153
  type TurnBudgetConfig,
@@ -159,10 +166,11 @@ import {
159
166
  resolveTopLevelParallelMaxTasks,
160
167
  resolveChildMaxSubagentDepth,
161
168
  resolveCurrentMaxSubagentDepth,
169
+ resolveMaxSubagentSpawnsPerRun,
162
170
  wrapForkTask,
163
171
  } from "../../shared/types.ts";
164
172
 
165
- const MUTATING_MANAGEMENT_ACTIONS = new Set(["create", "update", "delete", "eject", "disable", "enable", "reset", "grant-spawn-budget", "watchdog.configure", "mission.create", "mission.update", "mission.resolve-decision", "mission.attach-run", "mission.close", "inspector.open", "inspector.close", "project.open", "project.close", "worktree.discard", "refine", "refine.rollback", "schedule.create", "schedule.pause", "schedule.resume", "schedule.run", "schedule.run-due", "schedule.delete"]);
173
+ const MUTATING_MANAGEMENT_ACTIONS = new Set(["create", "update", "delete", "eject", "disable", "enable", "reset", "grant-spawn-budget", "watchdog.configure", "mission.create", "mission.update", "mission.resolve-decision", "mission.attach-run", "mission.close", "inspector.open", "inspector.close", "project.open", "project.close", "worktree.discard", "refine", "refine.rollback", "dismiss", "schedule.create", "schedule.pause", "schedule.resume", "schedule.run", "schedule.run-due", "schedule.delete"]);
166
174
  const DESTRUCTIVE_MANAGEMENT_ACTIONS = new Set(["delete", "eject", "disable", "reset", "mission.close", "worktree.discard", "refine.rollback", "inspector.close", "project.close", "stop", "interrupt", "reject-checkpoint", "schedule.delete"]);
167
175
 
168
176
  function editDistance(left: string, right: string): number {
@@ -282,6 +290,10 @@ export interface SubagentParamsLike {
282
290
  workflowKey?: string;
283
291
  workflowChildAsyncId?: string;
284
292
  suppressRoutineResultIntercom?: boolean;
293
+ /** Internal inherited cumulative run-tree budget. */
294
+ runFanoutBudget?: RunFanoutBudgetDescriptor;
295
+ /** Internal workflow host admission proof. */
296
+ runFanoutAdmitted?: boolean;
285
297
  /** Internal durable-run compatibility fields. Public callers must use workflowScript. */
286
298
  chain?: ChainStep[];
287
299
  tasks?: TaskParam[];
@@ -386,6 +398,7 @@ interface ExecutionContextData {
386
398
  artifactsDir: string;
387
399
  backgroundRequestedWhileClarifying: boolean;
388
400
  effectiveAsync: boolean;
401
+ asyncRunId: string;
389
402
  controlConfig: ResolvedControlConfig;
390
403
  intercomBridge: IntercomBridgeState;
391
404
  nestedRoute?: NestedRouteInfo;
@@ -402,6 +415,9 @@ interface ExecutionContextData {
402
415
  parentSessionId: string | null;
403
416
  parentPiSessionId?: string;
404
417
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
418
+ runFanoutBudget: RunFanoutBudgetDescriptor;
419
+ topLevelAsyncCapacityEligible: boolean;
420
+ activeAsyncCapacity?: ActiveAsyncCapacityHandle;
405
421
  }
406
422
 
407
423
  function resolveRequestedCwd(runtimeCwd: string, requestedCwd: string | undefined): string {
@@ -430,6 +446,17 @@ function getForegroundControl(state: SubagentState, runId: string | undefined) {
430
446
  return newest;
431
447
  }
432
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
+
433
460
  function formatForegroundActivity(control: SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never): string | undefined {
434
461
  const facts: string[] = [];
435
462
  if (control.currentTool && control.currentToolStartedAt) facts.push(`tool ${control.currentTool} for ${Math.floor(Math.max(0, Date.now() - control.currentToolStartedAt) / 1000)}s`);
@@ -481,12 +508,18 @@ function withSpawnBudgetStatus(
481
508
  sessionId: string | null,
482
509
  ): AgentToolResult<Details> {
483
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;
484
517
  return {
485
518
  ...result,
486
519
  content: result.content.map((item, index) => index === 0 && item.type === "text"
487
- ? { ...item, text: `${formatSpawnBudget(spawnBudget)}\n${item.text}` }
520
+ ? { ...item, text: `${formatSpawnBudget(spawnBudget)}\nActive async capacity: ${activeAsyncCapacity.used}/${activeAsyncCapacity.limit || "unlimited"} used\n${item.text}` }
488
521
  : item),
489
- details: { ...result.details, spawnBudget },
522
+ details: { ...result.details, spawnBudget, activeAsyncCapacity },
490
523
  };
491
524
  }
492
525
 
@@ -507,6 +540,31 @@ function countRequestedSubagentSpawns(params: SubagentParamsLike, config: Extens
507
540
  return params.agent ? 1 : 0;
508
541
  }
509
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
+
510
568
  function foregroundStatusResult(control: SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never): AgentToolResult<Details> {
511
569
  let nestedWarning: string | undefined;
512
570
  try {
@@ -1139,18 +1197,30 @@ function appendStepToAsyncChain(input: {
1139
1197
  }
1140
1198
 
1141
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
+ });
1142
1211
  const result = enqueueChainAppendRequest({
1143
- asyncDir: resolved.location.asyncDir,
1212
+ asyncDir,
1144
1213
  runId: resolved.id,
1145
1214
  steps: built.steps,
1215
+ admit: (persist) => claimRunFanoutBatchWithCommit(runFanoutBudget, appendPaths, persist),
1146
1216
  });
1147
1217
  const stepText = built.steps.length === 1 ? "step" : "steps";
1148
1218
  return {
1149
1219
  content: [{
1150
1220
  type: "text",
1151
- 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}` : ""}`,
1152
1222
  }],
1153
- details: { mode: "management", results: [], asyncId: resolved.id, asyncDir: resolved.location.asyncDir },
1223
+ details: { mode: "management", results: [], asyncId: resolved.id, asyncDir },
1154
1224
  };
1155
1225
  } catch (error) {
1156
1226
  const message = error instanceof Error ? error.message : String(error);
@@ -1485,6 +1555,20 @@ async function resumeAsyncRun(input: {
1485
1555
  };
1486
1556
  }
1487
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
+ }
1488
1572
  const artifactConfig: ArtifactConfig = omitUndefinedProperties({ ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false, dir: input.deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir });
1489
1573
  const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo);
1490
1574
  const contextPolicy = resolveExplicitContextPolicy(input.params);
@@ -1536,9 +1620,14 @@ async function resumeAsyncRun(input: {
1536
1620
  controlIntercomTarget: intercomBridge.active ? intercomBridge.orchestratorTarget : undefined,
1537
1621
  childIntercomTarget: intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
1538
1622
  globalConcurrencyLimit: input.deps.config.globalConcurrencyLimit,
1623
+ runFanoutBudget: createRunFanoutBudget(runId, resolveMaxSubagentSpawnsPerRun(input.deps.config.maxSubagentSpawnsPerRun)),
1539
1624
  capabilityCeiling: intersectSubagentCapabilityCeilings("capabilityCeiling" in target ? target.capabilityCeiling : undefined, resolveCurrentSubagentCapabilityCeiling(input.deps.state.currentSessionId)),
1625
+ activeAsyncCapacity,
1540
1626
  }));
1541
- if (result.isError) return result;
1627
+ if (result.isError) {
1628
+ activeAsyncCapacity?.rollback();
1629
+ return result;
1630
+ }
1542
1631
  const attachedId = result.details.asyncId ?? runId;
1543
1632
  const lines = [
1544
1633
  `Attached async subagent ${target.runId} as the first step of a new chain.`,
@@ -1557,12 +1646,38 @@ async function resumeAsyncRun(input: {
1557
1646
  if (!revivalSessionFile) {
1558
1647
  return { content: [{ type: "text", text: `Async child '${target.runId}' has no persisted session file to resume.` }], isError: true, details: { mode: "management", results: [] } };
1559
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
+ }
1560
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
+ }
1561
1675
  const recoveryAgentConfig = recoveryDescriptor ? applySteeringRecoveryAgentConfig(agentConfig, recoveryDescriptor) : agentConfig;
1562
1676
  const artifactConfig: ArtifactConfig = recoveryDescriptor?.artifactConfig ?? omitUndefinedProperties({ ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false, dir: input.deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir });
1563
1677
  const artifactsDir = recoveryDescriptor?.artifactsDir ?? getArtifactsDir(parentSessionFile, effectiveCwd, artifactConfig.dir);
1564
1678
  const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo);
1565
1679
  const parentModel = input.parentModel;
1680
+ const revivalAsyncDir = path.join(DIRS.async, runId);
1566
1681
  const result = executeAsyncSingle(runId, compactOptional<Parameters<typeof executeAsyncSingle>[1]>({
1567
1682
  agent: target.agent,
1568
1683
  task: buildRevivedAsyncTask(target as Parameters<typeof buildRevivedAsyncTask>[0], effectiveFollowUp),
@@ -1616,10 +1731,28 @@ async function resumeAsyncRun(input: {
1616
1731
  ...(input.params.turnBudget !== undefined ? { turnBudget: input.params.turnBudget } : {}),
1617
1732
  ...(input.params.toolBudget !== undefined ? { toolBudget: input.params.toolBudget } : {}),
1618
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)),
1619
1735
  parentWorkflowRunId: input.params.workflowParentRunId,
1620
1736
  workflowKey: input.params.workflowKey,
1737
+ activeAsyncCapacity,
1621
1738
  }));
1622
- 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
+ }
1623
1756
  for (const brief of queuedBriefs) fs.rmSync(brief.path, { force: true });
1624
1757
  if (queuedBriefs.length > 0 && sourceAsyncDir) {
1625
1758
  const sourceStatus = readStatus(sourceAsyncDir);
@@ -2115,6 +2248,27 @@ export const DEFAULT_FOREGROUND_TIMEOUT_MS = 30 * 60 * 1000;
2115
2248
  // stay unbounded as a whole, while each runner child has its own deadline.
2116
2249
  export { DEFAULT_ASYNC_TIMEOUT_MS };
2117
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
+
2118
2272
  export function resolveForegroundTimeout(params: SubagentParamsLike, defaultTimeoutMs?: number): { timeoutMs?: number; error?: string } {
2119
2273
  const rawTimeout = params.timeoutMs;
2120
2274
  const rawMaxRuntime = params.maxRuntimeMs;
@@ -2138,14 +2292,18 @@ export function resolveForegroundTimeout(params: SubagentParamsLike, defaultTime
2138
2292
  * Resolve the effective launch timeout for a single-agent run, applying the
2139
2293
  * async/foreground default when neither the caller nor the agent set one.
2140
2294
  *
2141
- * The async default is deliberately applied only to plain single-agent
2142
- * launches. Composite launches keep their top-level execution unbounded when
2143
- * no timeout is set; their runner children resolve separate deadlines.
2144
- * 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.
2145
2301
  */
2146
- 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 } {
2147
2303
  const isComposite = (params.chain?.length ?? 0) > 0 || (params.tasks?.length ?? 0) > 0 || params.workflowScript !== undefined;
2148
- 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;
2149
2307
  return resolveForegroundTimeout(params, defaultTimeoutMs);
2150
2308
  }
2151
2309
 
@@ -2558,8 +2716,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2558
2716
  details: { mode: "single" as const, results: [] },
2559
2717
  };
2560
2718
  }
2561
- const requestedWorkflowChildAsyncId = typeof params.workflowChildAsyncId === "string" ? params.workflowChildAsyncId.trim() : "";
2562
- const id = requestedWorkflowChildAsyncId && path.basename(requestedWorkflowChildAsyncId) === requestedWorkflowChildAsyncId ? requestedWorkflowChildAsyncId : randomUUID();
2719
+ const id = data.asyncRunId;
2563
2720
  const parentModel = data.parentModel;
2564
2721
  const asyncCtx = compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
2565
2722
  pi: deps.pi,
@@ -2613,6 +2770,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2613
2770
  shareEnabled,
2614
2771
  sessionRoot,
2615
2772
  chainSkills: [],
2773
+ activeAsyncCapacity: data.activeAsyncCapacity,
2616
2774
  sessionFilesByFlatIndex: params.tasks.map((task, index) => sessionFileForTask(task.agent, index, task.model)),
2617
2775
  thinkingOverridesByFlatIndex: params.tasks.map((task, index) => thinkingOverrideForTask(task.agent, index, task.model)),
2618
2776
  contextForAgent: contextPolicy.contextForAgent,
@@ -2632,6 +2790,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2632
2790
  usageBudget: data.usageBudget,
2633
2791
  configToolBudget: data.configToolBudget,
2634
2792
  capabilityCeiling: data.capabilityCeiling,
2793
+ runFanoutBudget: data.runFanoutBudget,
2635
2794
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2636
2795
  parentWorkflowRunId: params.workflowParentRunId,
2637
2796
  workflowKey: params.workflowKey,
@@ -2657,6 +2816,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2657
2816
  shareEnabled,
2658
2817
  sessionRoot,
2659
2818
  chainSkills,
2819
+ activeAsyncCapacity: data.activeAsyncCapacity,
2660
2820
  sessionFilesByFlatIndex: collectChainSessionFiles(chain, sessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems),
2661
2821
  thinkingOverridesByFlatIndex: collectChainThinkingOverrides(chain, thinkingOverrideForTask, deps.config.chain?.dynamicFanout?.maxItems),
2662
2822
  contextForAgent: contextPolicy.contextForAgent,
@@ -2677,6 +2837,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2677
2837
  usageBudget: data.usageBudget,
2678
2838
  configToolBudget: data.configToolBudget,
2679
2839
  capabilityCeiling: data.capabilityCeiling,
2840
+ runFanoutBudget: data.runFanoutBudget,
2680
2841
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2681
2842
  parentWorkflowRunId: params.workflowParentRunId,
2682
2843
  workflowKey: params.workflowKey,
@@ -2698,7 +2859,12 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2698
2859
  const normalizedSkills = normalizeSkillInput(params.skill);
2699
2860
  const skills = normalizedSkills === false ? [] : normalizedSkills;
2700
2861
  const maxSubagentDepth = resolveChildMaxSubagentDepth(currentMaxSubagentDepth, a.maxSubagentDepth);
2701
- 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 });
2702
2868
  return executeAsyncSingle(id, compactOptional<Parameters<typeof executeAsyncSingle>[1]>({
2703
2869
  agent: params.agent!,
2704
2870
  task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(params.task ?? "") : (params.task ?? ""),
@@ -2711,6 +2877,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2711
2877
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
2712
2878
  artifactConfig,
2713
2879
  shareEnabled,
2880
+ activeAsyncCapacity: data.activeAsyncCapacity,
2714
2881
  sessionRoot,
2715
2882
  sessionFile: sessionFileForTask(params.agent!, 0, modelOverride),
2716
2883
  context: contextPolicy.contextForAgent(params.agent!),
@@ -2720,7 +2887,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2720
2887
  ...(params.reads !== undefined ? { reads: params.reads } : {}),
2721
2888
  outputBaseDir: resolveSingleRunOutputBaseDir(deps, artifactsDir, id),
2722
2889
  modelOverride,
2723
- thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride),
2890
+ thinkingOverride: externalRunnerWithoutExplicitModel ? undefined : thinkingOverrideForTask(params.agent!, 0, modelOverride),
2724
2891
  maxSubagentDepth,
2725
2892
  waitToolEnabled: deps.waitToolEnabled,
2726
2893
  worktreeSetupHook: deps.config.worktreeSetupHook,
@@ -2739,6 +2906,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2739
2906
  usageBudget: data.usageBudget,
2740
2907
  configToolBudget: data.configToolBudget,
2741
2908
  capabilityCeiling: data.capabilityCeiling,
2909
+ runFanoutBudget: data.runFanoutBudget,
2742
2910
  parentWorkflowRunId: params.workflowParentRunId,
2743
2911
  workflowKey: params.workflowKey,
2744
2912
  }));
@@ -2829,6 +2997,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2829
2997
  permissions: deps.config.permissions,
2830
2998
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2831
2999
  capabilityCeiling: data.capabilityCeiling,
3000
+ runFanoutBudget: data.runFanoutBudget,
2832
3001
  }));
2833
3002
 
2834
3003
  if (chainResult.requestedAsync) {
@@ -2839,7 +3008,9 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2839
3008
  details: { mode: "chain" as const, results: [] },
2840
3009
  };
2841
3010
  }
2842
- const id = randomUUID();
3011
+ const id = data.asyncRunId;
3012
+ const capacity = acquireClarifiedBackgroundCapacity(data, deps, "chain", id);
3013
+ if (capacity.errorResult) return capacity.errorResult;
2843
3014
  const parentModel = data.parentModel;
2844
3015
  const asyncCtx = compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
2845
3016
  pi: deps.pi,
@@ -2855,42 +3026,48 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2855
3026
  const rawAsyncChain = chainResult.requestedAsync.chain;
2856
3027
  const asyncChain = wrapChainTasksForFork(rawAsyncChain, contextPolicy);
2857
3028
  const firstAgent = firstChainAgent(rawAsyncChain);
2858
- return executeAsyncChain(id, compactOptional<Parameters<typeof executeAsyncChain>[1]>({
2859
- chain: asyncChain,
2860
- task: params.task,
2861
- goal: resolveAsyncEventGoal(params.task, rawAsyncChain, firstAgent ? shouldForkAgent(contextPolicy, firstAgent) : false),
2862
- agents,
2863
- ctx: asyncCtx,
2864
- availableModels: ctx.modelRegistry.getAvailable().map(toModelInfo),
2865
- cwd: effectiveCwd,
2866
- maxOutput: params.maxOutput,
2867
- artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
2868
- artifactConfig,
2869
- shareEnabled,
2870
- sessionRoot,
2871
- chainSkills: chainResult.requestedAsync.chainSkills,
2872
- sessionFilesByFlatIndex: collectChainSessionFiles(asyncChain, sessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems),
2873
- thinkingOverridesByFlatIndex: collectChainThinkingOverrides(asyncChain, thinkingOverrideForTask, deps.config.chain?.dynamicFanout?.maxItems),
2874
- contextForAgent: contextPolicy.contextForAgent,
2875
- dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
2876
- maxSubagentDepth: currentMaxSubagentDepth,
2877
- waitToolEnabled: deps.waitToolEnabled,
2878
- worktreeSetupHook: deps.config.worktreeSetupHook,
2879
- worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
2880
- worktreeBaseDir: deps.config.worktreeBaseDir,
2881
- controlConfig,
2882
- agentContract: params.agentContract,
2883
- controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
2884
- childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
2885
- nestedRoute: data.nestedRoute,
2886
- timeoutMs: data.timeoutMs,
2887
- turnBudget: data.turnBudget,
2888
- toolBudget: data.toolBudget,
2889
- usageBudget: data.usageBudget,
2890
- configToolBudget: data.configToolBudget,
2891
- capabilityCeiling: data.capabilityCeiling,
2892
- globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2893
- }));
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
+ }
2894
3071
  }
2895
3072
 
2896
3073
  const rawChainDetails = chainResult.details ? compactOptional<Details>({ ...chainResult.details, runId, timeoutMs: data.timeoutMs }) : undefined;
@@ -2935,6 +3112,7 @@ interface ForegroundParallelRunInput {
2935
3112
  intercomEvents: IntercomEventBus;
2936
3113
  parentSessionId: string | null;
2937
3114
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
3115
+ runFanoutBudget: RunFanoutBudgetDescriptor;
2938
3116
  signal: AbortSignal;
2939
3117
  runId: string;
2940
3118
  sessionDirForIndex: (idx?: number) => string | undefined;
@@ -2984,6 +3162,48 @@ function buildParallelModeError(message: string): AgentToolResult<Details> {
2984
3162
  };
2985
3163
  }
2986
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
+
2987
3207
  function createParallelWorktreeSetup(
2988
3208
  enabled: boolean | undefined,
2989
3209
  cwd: string,
@@ -3172,7 +3392,33 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
3172
3392
  beginForegroundChild(input.foregroundControl, {
3173
3393
  index,
3174
3394
  agent: task.agent,
3175
- ...(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`,
3176
3422
  ...(model ? { model } : {}),
3177
3423
  ...(thinking ? { thinking } : {}),
3178
3424
  interrupt: () => {
@@ -3189,8 +3435,10 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
3189
3435
  const result = await runSync(input.ctx.cwd, input.agents, task.agent, taskText, compactOptional<Parameters<typeof runSync>[4]>({
3190
3436
  permissions: input.permissions,
3191
3437
  parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
3438
+ llmIntentArbiter: createTaskMutationArbiter(input.ctx),
3192
3439
  ...workflowForegroundSteeringLaunchOptions(input.foregroundControl, index),
3193
3440
  context: input.contextPolicy.contextForAgent(task.agent),
3441
+ runFanoutBudget: { ...input.runFanoutBudget, parentPath: `${input.runFanoutBudget.parentPath ? `${input.runFanoutBudget.parentPath}/` : ""}tasks[${index}]` },
3194
3442
  cwd: taskCwd,
3195
3443
  signal: input.signal,
3196
3444
  interruptSignal: interruptController.signal,
@@ -3235,6 +3483,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
3235
3483
  agentContract: task.agentContract ?? input.agentContract,
3236
3484
  acceptance: task.acceptance,
3237
3485
  acceptanceContext: { mode: "parallel" },
3486
+ onEffectivePrompt: input.foregroundControl ? (prompt) => updateLiveEffectivePrompt(input.foregroundControl!, index, prompt) : undefined,
3238
3487
  timeoutMs: input.timeoutMs,
3239
3488
  deadlineAt: input.deadlineAt,
3240
3489
  turnBudget: input.turnBudget,
@@ -3418,7 +3667,9 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3418
3667
  details: { mode: "parallel" as const, results: [] },
3419
3668
  };
3420
3669
  }
3421
- const id = randomUUID();
3670
+ const id = data.asyncRunId;
3671
+ const capacity = acquireClarifiedBackgroundCapacity(data, deps, "parallel", id);
3672
+ if (capacity.errorResult) return capacity.errorResult;
3422
3673
  const asyncCtx = compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
3423
3674
  pi: deps.pi,
3424
3675
  cwd: ctx.cwd,
@@ -3449,40 +3700,46 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3449
3700
  ...(t.agentContract !== undefined ? { agentContract: t.agentContract } : {}),
3450
3701
  });
3451
3702
  });
3452
- return executeAsyncChain(id, compactOptional<Parameters<typeof executeAsyncChain>[1]>({
3453
- chain: [compactOptional<ParallelStep>({ parallel: parallelTasks, concurrency: parallelConcurrency, worktree: params.worktree })],
3454
- resultMode: "parallel",
3455
- goal: taskTexts[0] ?? "",
3456
- agents,
3457
- ctx: asyncCtx,
3458
- availableModels,
3459
- cwd: effectiveCwd,
3460
- maxOutput: params.maxOutput,
3461
- artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
3462
- artifactConfig,
3463
- shareEnabled,
3464
- sessionRoot,
3465
- chainSkills: [],
3466
- sessionFilesByFlatIndex: tasks.map((task, index) => sessionFileForTask(task.agent, index, modelOverrides[index])),
3467
- thinkingOverridesByFlatIndex: tasks.map((task, index) => thinkingOverrideForTask(task.agent, index, modelOverrides[index])),
3468
- contextForAgent: contextPolicy.contextForAgent,
3469
- maxSubagentDepth: currentMaxSubagentDepth,
3470
- waitToolEnabled: deps.waitToolEnabled,
3471
- worktreeSetupHook: deps.config.worktreeSetupHook,
3472
- worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
3473
- worktreeBaseDir: deps.config.worktreeBaseDir,
3474
- controlConfig,
3475
- agentContract: params.agentContract,
3476
- controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
3477
- childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
3478
- nestedRoute: data.nestedRoute,
3479
- timeoutMs: data.timeoutMs,
3480
- turnBudget: data.turnBudget,
3481
- usageBudget: data.usageBudget,
3482
- toolBudget: data.toolBudget,
3483
- configToolBudget: data.configToolBudget,
3484
- globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
3485
- }));
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
+ }
3486
3743
  }
3487
3744
  }
3488
3745
 
@@ -3561,6 +3818,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3561
3818
  intercomEvents: deps.pi.events,
3562
3819
  parentSessionId: data.parentSessionId,
3563
3820
  capabilityCeiling: data.capabilityCeiling,
3821
+ runFanoutBudget: data.runFanoutBudget,
3564
3822
  signal,
3565
3823
  runId,
3566
3824
  sessionDirForIndex,
@@ -3795,7 +4053,9 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3795
4053
  details: { mode: "single" as const, results: [] },
3796
4054
  };
3797
4055
  }
3798
- const id = randomUUID();
4056
+ const id = data.asyncRunId;
4057
+ const capacity = acquireClarifiedBackgroundCapacity(data, deps, "single", id);
4058
+ if (capacity.errorResult) return capacity.errorResult;
3799
4059
  const asyncCtx = compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
3800
4060
  pi: deps.pi,
3801
4061
  cwd: ctx.cwd,
@@ -3807,49 +4067,56 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3807
4067
  interactive: ctx.hasUI,
3808
4068
  permissions: deps.config.permissions,
3809
4069
  });
3810
- return executeAsyncSingle(id, compactOptional<Parameters<typeof executeAsyncSingle>[1]>({
3811
- agent: params.agent!,
3812
- task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(task) : task,
3813
- goal: task,
3814
- agentConfig,
3815
- ctx: asyncCtx,
3816
- availableModels,
3817
- cwd: effectiveCwd,
3818
- maxOutput: params.maxOutput,
3819
- artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
3820
- artifactConfig,
3821
- shareEnabled,
3822
- sessionRoot,
3823
- sessionFile: sessionFileForTask(params.agent!, 0, modelOverride),
3824
- context: contextPolicy.contextForAgent(params.agent!),
3825
- skills: skillOverride === false ? [] : skillOverride,
3826
- output: effectiveOutput,
3827
- outputMode: effectiveOutputMode,
3828
- ...(readsOverride !== undefined ? { reads: readsOverride } : {}),
3829
- outputBaseDir: resolveSingleRunOutputBaseDir(deps, artifactsDir, id),
3830
- modelOverride,
3831
- thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride),
3832
- maxSubagentDepth,
3833
- waitToolEnabled: deps.waitToolEnabled,
3834
- worktreeSetupHook: deps.config.worktreeSetupHook,
3835
- worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
3836
- worktreeBaseDir: deps.config.worktreeBaseDir,
3837
- controlConfig,
3838
- controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
3839
- childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
3840
- nestedRoute: data.nestedRoute,
3841
- agentContract: params.agentContract,
3842
- structuredOutputSchema: params.outputSchema,
3843
- acceptance: params.acceptance,
3844
- timeoutMs: data.timeoutMs,
3845
- turnBudget: data.turnBudget,
3846
- toolBudget: effectiveToolBudget.toolBudget,
3847
- usageBudget: data.usageBudget,
3848
- allowZeroToolBudget: data.allowZeroToolBudget && effectiveToolBudget.toolBudget === data.toolBudget,
3849
- }));
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
+ }
3850
4116
  }
3851
4117
  }
3852
4118
 
4119
+ const authoredTask = task;
3853
4120
  if (shouldForkAgent(contextPolicy, params.agent!)) {
3854
4121
  task = wrapForkTask(task);
3855
4122
  }
@@ -3886,6 +4153,11 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3886
4153
  beginForegroundChild(foregroundControl, omitUndefinedProperties({
3887
4154
  index: 0,
3888
4155
  agent: params.agent!,
4156
+ authoredTask,
4157
+ effectivePrompt: task,
4158
+ cwd: effectiveCwd,
4159
+ outputPath,
4160
+ rerun: { params: { ...params, task: authoredTask, async: params.async ?? false } },
3889
4161
  description: foregroundControl.description,
3890
4162
  ...(modelOverride ? { model: modelOverride } : {}),
3891
4163
  ...(thinking ? { thinking } : {}),
@@ -3911,8 +4183,10 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3911
4183
  r = await runSync(ctx.cwd, agents, params.agent!, task, compactOptional<Parameters<typeof runSync>[4]>({
3912
4184
  permissions: deps.config.permissions,
3913
4185
  parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
4186
+ llmIntentArbiter: createTaskMutationArbiter(ctx),
3914
4187
  ...workflowForegroundSteeringLaunchOptions(foregroundControl, 0),
3915
4188
  context: data.contextPolicy.contextForAgent(params.agent!),
4189
+ runFanoutBudget: params.runFanoutAdmitted ? data.runFanoutBudget : { ...data.runFanoutBudget, parentPath: `${data.runFanoutBudget.parentPath ? `${data.runFanoutBudget.parentPath}/` : ""}single` },
3916
4190
  cwd: effectiveCwd,
3917
4191
  signal,
3918
4192
  interruptSignal: interruptController.signal,
@@ -3946,6 +4220,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3946
4220
  agentContract: params.agentContract,
3947
4221
  acceptance: params.acceptance,
3948
4222
  acceptanceContext: { mode: "single" },
4223
+ onEffectivePrompt: foregroundControl ? (prompt) => updateLiveEffectivePrompt(foregroundControl, 0, prompt) : undefined,
3949
4224
  onDetachReady: (detach) => {
3950
4225
  detachForeground = detach;
3951
4226
  },
@@ -3994,12 +4269,14 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3994
4269
  if (r.artifactPaths) allArtifactPaths.push(r.artifactPaths);
3995
4270
 
3996
4271
  const fullOutput = getSingleResultOutput(r);
4272
+ const preserveRejectedSavedOutput = r.acceptance?.explicit && r.acceptance.status === "rejected" && r.savedOutputPath !== undefined;
3997
4273
  const finalizedOutput = finalizeSingleOutput(omitUndefinedProperties({
3998
4274
  fullOutput,
3999
4275
  truncatedOutput: r.truncation?.text,
4000
4276
  outputPath,
4001
4277
  outputMode: r.outputMode,
4002
4278
  exitCode: r.exitCode,
4279
+ preserveSavedOutput: preserveRejectedSavedOutput,
4003
4280
  savedPath: r.savedOutputPath,
4004
4281
  outputReference: r.outputReference,
4005
4282
  saveError: r.outputSaveError,
@@ -4090,7 +4367,7 @@ function duplicateSubagentCallResult(params: SubagentParamsLike): AgentToolResul
4090
4367
  };
4091
4368
  }
4092
4369
 
4093
- 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>();
4094
4371
 
4095
4372
  function recordMissionWorkflowChild(
4096
4373
  binding: MissionLaunchBinding | undefined,
@@ -4099,8 +4376,9 @@ function recordMissionWorkflowChild(
4099
4376
  update: Omit<MissionWorkflowChildUpdate, "workflowRunId" | "key">,
4100
4377
  ): void {
4101
4378
  if (!binding) return;
4379
+ const { task: _task, ...durableUpdate } = update;
4102
4380
  try {
4103
- updateMission(binding.location, binding.missionId, { upsertWorkflowChildren: [{ workflowRunId, key, ...update }] });
4381
+ updateMission(binding.location, binding.missionId, { upsertWorkflowChildren: [{ workflowRunId, key, ...durableUpdate }] });
4104
4382
  } catch (error) {
4105
4383
  console.warn(`[pi-subagents] Failed to record mission workflow child '${key}': ${error instanceof Error ? error.message : String(error)}`);
4106
4384
  }
@@ -4182,7 +4460,7 @@ export function prepareWorkflowLaunchParams(
4182
4460
  childParams: Record<string, unknown>,
4183
4461
  parentWorkflowRunId: string,
4184
4462
  workflowKey: string,
4185
- options: { missionDetached?: boolean; suppressRoutineResultIntercom?: boolean } = {},
4463
+ options: { missionDetached?: boolean; suppressRoutineResultIntercom?: boolean; runFanoutBudget?: RunFanoutBudgetDescriptor } = {},
4186
4464
  ): SubagentParamsLike {
4187
4465
  if (typeof childParams.resume === "string") {
4188
4466
  if (childParams.gate !== undefined || workflowDefaults.gate !== undefined) {
@@ -4210,6 +4488,7 @@ export function prepareWorkflowLaunchParams(
4210
4488
  ...(options.missionDetached ? { mission: false } : {}),
4211
4489
  workflowParentRunId: parentWorkflowRunId,
4212
4490
  workflowKey,
4491
+ ...(options.runFanoutBudget ? { runFanoutBudget: { ...options.runFanoutBudget, parentPath: `${options.runFanoutBudget.parentPath ? `${options.runFanoutBudget.parentPath}/` : ""}workflow[${workflowKey}]` } } : {}),
4213
4492
  ...(options.suppressRoutineResultIntercom ? { suppressRoutineResultIntercom: true } : {}),
4214
4493
  } as SubagentParamsLike;
4215
4494
  const normalizedGate = normalizeGateParams(launchParams);
@@ -4357,6 +4636,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4357
4636
  } {
4358
4637
  const delegatedThinkingOverrides = new WeakMap<object, AgentConfig["thinking"]>();
4359
4638
  const delegatedZeroToolBudgets = new WeakSet<object>();
4639
+ const delegatedExecutions = new WeakSet<object>();
4360
4640
  const warnedArtifactPackageDirs = new Set<string>();
4361
4641
  const scheduledOwnerExecutors = new Map<string, ReturnType<typeof createSubagentExecutor>>();
4362
4642
  const execute = async (
@@ -4370,6 +4650,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4370
4650
  const workflowLaunchObserver = workflowLaunchObservers.get(params);
4371
4651
  const delegatedThinkingOverride = delegatedThinkingOverrides.get(params);
4372
4652
  const allowZeroToolBudget = delegatedZeroToolBudgets.has(params);
4653
+ const delegatedExecution = delegatedExecutions.has(params);
4373
4654
  if (!preserveActiveSession) deps.state.baseCwd = ctx.cwd;
4374
4655
  deps.state.foregroundRuns ??= new Map();
4375
4656
  deps.state.foregroundControls ??= new Map();
@@ -4380,7 +4661,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4380
4661
  const normalizedAction = typeof requestParams.action === "string" ? requestParams.action.trim() : requestParams.action;
4381
4662
  if (requestParams.workflowScript !== undefined && normalizedAction === undefined) {
4382
4663
  const parentCwd = ctx.cwd;
4383
- 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);
4384
4665
  const workflowUsageBudget = validateUsageBudgetConfig(requestParams.usageBudget ?? deps.config.usageBudget, requestParams.usageBudget ? "usageBudget" : "config.usageBudget");
4385
4666
  if (workflowUsageBudget.error) return buildRequestedModeError(requestParams, workflowUsageBudget.error);
4386
4667
  const workflowCwd = resolveRequestedCwd(parentCwd, requestParams.cwd);
@@ -4391,11 +4672,45 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4391
4672
  const explicitMission = requestParams.missionId !== undefined || requestParams.mission !== undefined;
4392
4673
  const autoMission = !explicitMission;
4393
4674
  const workflowPreview = autoMission ? previewSimpleWorkflowRun(requestParams.workflowScript) : undefined;
4394
- const previewTask = workflowPreview?.task?.trim() || undefined;
4395
4675
  const previewAgent = workflowPreview?.agent?.trim() || undefined;
4396
4676
  const scriptFirstLine = requestParams.workflowScript.split(/\r?\n/).map((line) => line.trim()).find(Boolean) || "Workflow";
4397
4677
  const boundedScriptPreview = scriptFirstLine.length > 100 ? `${scriptFirstLine.slice(0, 97)}...` : scriptFirstLine;
4398
- 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
+ }
4399
4714
  let missionBinding: MissionLaunchBinding | undefined;
4400
4715
  let missionWarning: string | undefined;
4401
4716
  try {
@@ -4406,22 +4721,12 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4406
4721
  ownerSessionId: resolveCurrentSessionId(ctx.sessionManager),
4407
4722
  });
4408
4723
  } catch (error) {
4409
- 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
+ }
4410
4728
  missionWarning = `Mission tracking unavailable: ${error instanceof Error ? error.message : String(error)}`;
4411
4729
  }
4412
- let shouldPatchMissionObjective = autoMission && previewTask === undefined && missionBinding !== undefined;
4413
- const patchMissionObjective = (task: unknown): void => {
4414
- if (!shouldPatchMissionObjective || !missionBinding || typeof task !== "string" || !task.trim()) return;
4415
- shouldPatchMissionObjective = false;
4416
- const objective = task.trim();
4417
- const firstLine = objective.split(/\r?\n/, 1)[0]?.trim() || objective;
4418
- const title = firstLine.length > 100 ? `${firstLine.slice(0, 97)}...` : firstLine;
4419
- try {
4420
- updateMission(missionBinding.location, missionBinding.missionId, { title, objective });
4421
- } catch (error) {
4422
- console.warn(`[pi-subagents] Failed to update automatic mission objective: ${error instanceof Error ? error.message : String(error)}`);
4423
- }
4424
- };
4425
4730
  const detachWorkflowChildMissions = autoMission || missionBinding !== undefined || requestParams.mission === false;
4426
4731
  const workflowState = missionBinding ? createMissionWorkflowState(missionBinding.location, missionBinding.missionId) : undefined;
4427
4732
  const attachWorkflowMission = (result: AgentToolResult<Details>): AgentToolResult<Details> => {
@@ -4435,20 +4740,27 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4435
4740
  : { ...result, details: { ...result.details, missionWarning: warning } };
4436
4741
  }
4437
4742
  };
4438
- if (requestParams.async !== false) {
4743
+ if (workflowRunId) {
4439
4744
  const toolCallId = _id;
4440
- const workflowRunId = randomUUID();
4441
4745
  const asyncDir = path.join(DIRS.async, workflowRunId);
4442
4746
  const resultPath = path.join(DIRS.results, `${workflowRunId}.json`);
4443
4747
  const statusPath = path.join(asyncDir, "status.json");
4444
4748
  const eventsPath = path.join(asyncDir, "events.jsonl");
4445
4749
  const startedAt = Date.now();
4446
4750
  const currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
4447
- fs.mkdirSync(asyncDir, { recursive: true });
4448
- 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
+ }
4449
4759
  const controller = new AbortController();
4450
4760
  deps.state.workflowControllers ??= new Map();
4451
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()) });
4452
4764
  let status: AsyncStatus = {
4453
4765
  runId: workflowRunId,
4454
4766
  toolCallId,
@@ -4462,11 +4774,17 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4462
4774
  pid: process.pid,
4463
4775
  steps: [],
4464
4776
  workflow: { trace: [], emits: [], console: [] },
4777
+ runFanoutBudget: getRunFanoutBudgetSnapshot(workflowFanoutBudget),
4465
4778
  };
4466
4779
  const appendWorkflowEvent = (event: Record<string, unknown>) => fs.appendFileSync(eventsPath, `${JSON.stringify({ ts: Date.now(), runId: workflowRunId, ...event })}\n`, "utf-8");
4780
+ let indexedState: AsyncStatus["state"] | undefined;
4467
4781
  const persist = () => {
4468
4782
  status.lastUpdate = Date.now();
4469
4783
  writeAtomicJson(statusPath, status);
4784
+ if (indexedState !== status.state) {
4785
+ updateActiveRunIndex(asyncDir, status.state);
4786
+ indexedState = status.state;
4787
+ }
4470
4788
  const job = deps.state.asyncJobs.get(workflowRunId);
4471
4789
  if (job) {
4472
4790
  job.status = status.state;
@@ -4543,12 +4861,23 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4543
4861
  if (existing) {
4544
4862
  existing.status = mapped;
4545
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;
4546
4866
  if (entry.error === undefined) delete existing.error;
4547
4867
  else existing.error = entry.error;
4548
4868
  if (entry.durationMs === undefined) delete existing.durationMs;
4549
4869
  else existing.durationMs = entry.durationMs;
4550
4870
  } else {
4551
- 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
+ };
4552
4881
  status.steps?.push(step);
4553
4882
  workflowSteps.set(entry.key, step);
4554
4883
  }
@@ -4567,30 +4896,32 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4567
4896
  prompts: workflowPrompts,
4568
4897
  ...(workflowState ? { state: workflowState } : {}),
4569
4898
  onTrace: updateTrace,
4899
+ admit: (calls) => {
4900
+ status.runFanoutBudget = claimRunFanoutBatch(workflowFanoutBudget, calls.map(({ key }) => `workflow[${key}]`));
4901
+ persist();
4902
+ },
4570
4903
  onEmit: (emits) => {
4571
4904
  // Each emit is validated at the host boundary in runWorkflowScript before onEmit fires.
4572
4905
  status.workflow = { ...(status.workflow ?? { trace: [], console: [] }), emits };
4573
4906
  persist();
4574
4907
  appendWorkflowEvent({ type: "subagent.workflow.emit", value: emits.at(-1) });
4575
4908
  },
4576
- launch: async (key, childParams, workflowSignal) => {
4909
+ launch: async (key, childParams, workflowSignal, admission) => {
4577
4910
  if (workflowUsageBudget.budget && childParams.async === true) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, "workflow usageBudget does not support async runs.run launches."));
4578
4911
  const budgetState = usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults));
4579
4912
  if (budgetState?.exhausted) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, usageBudgetExceededMessage(budgetState)));
4580
- patchMissionObjective(childParams.task);
4581
4913
  const childPhase = typeof childParams.phase === "string" && childParams.phase.trim() ? childParams.phase.trim() : undefined;
4582
4914
  const childLabel = typeof childParams.label === "string" && childParams.label.trim() ? childParams.label.trim() : undefined;
4583
4915
  recordMissionWorkflowChild(missionBinding, workflowRunId, key, {
4584
4916
  status: "running",
4585
4917
  ...(typeof childParams.agent === "string" && childParams.agent.trim() ? { agent: childParams.agent.trim() } : {}),
4586
- ...(typeof childParams.task === "string" && childParams.task.trim() ? { task: childParams.task.trim() } : {}),
4587
4918
  ...(childLabel ? { label: childLabel } : {}),
4588
4919
  ...(childPhase ? { phase: childPhase } : {}),
4589
4920
  heartbeat: { status: "running", ...(childPhase ? { phase: childPhase } : {}) },
4590
4921
  });
4591
4922
  const result = await runMissionWorkflowChild(missionBinding, workflowRunId, key, childPhase, () => {
4592
4923
  const childRequest = bindMissionWorkflowChildAsyncLaunch(
4593
- prepareWorkflowLaunchParams(workflowChildDefaults, childParams, workflowRunId, key, { missionDetached: detachWorkflowChildMissions }),
4924
+ { ...prepareWorkflowLaunchParams(workflowChildDefaults, childParams, workflowRunId, key, { missionDetached: detachWorkflowChildMissions, runFanoutBudget: workflowFanoutBudget }), runFanoutAdmitted: admission.admitted },
4594
4925
  missionBinding,
4595
4926
  deps.asyncByDefault,
4596
4927
  );
@@ -4599,6 +4930,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4599
4930
  if (step) {
4600
4931
  step.agent = launch.agent;
4601
4932
  step.sessionFile = launch.sessionFile;
4933
+ step.async = launch.async;
4934
+ if (launch.runId) step.runId = launch.runId;
4602
4935
  persist();
4603
4936
  }
4604
4937
  recordMissionWorkflowChild(missionBinding, workflowRunId, key, { status: "running", agent: launch.agent, ...(launch.sessionFile ? { sessionPath: launch.sessionFile } : {}) });
@@ -4630,8 +4963,13 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4630
4963
  }, ctx, preserveActiveSession);
4631
4964
  });
4632
4965
  workflowResults.push(...result.details.results);
4633
- if (result.details.asyncDir && missionBinding) writeMissionAsyncBinding(result.details.asyncDir, missionBinding);
4634
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);
4635
4973
  const childStatus = missionWorkflowChildStatus(result);
4636
4974
  recordMissionWorkflowChild(missionBinding, workflowRunId, key, {
4637
4975
  status: childStatus,
@@ -4667,12 +5005,14 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4667
5005
  appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, error: status.error });
4668
5006
  } finally {
4669
5007
  deps.state.workflowControllers?.delete(workflowRunId);
5008
+ deps.state.activeAsyncCapacity = workflowCapacity?.reconcile(new Set(deps.state.workflowControllers?.keys() ?? []))
5009
+ ?? deps.state.activeAsyncCapacity;
4670
5010
  }
4671
5011
  });
4672
- return attachWorkflowMission({
5012
+ return attachWorkflowMission(withRunFanoutBudget({
4673
5013
  content: [{ type: "text", text: formatAsyncStartedMessage(`Async workflow [${workflowRunId}]`, ctx.hasUI === true) }],
4674
- details: { mode: "workflow", runId: workflowRunId, toolCallId, asyncId: workflowRunId, asyncDir, results: [], chatProgress },
4675
- });
5014
+ details: { mode: "workflow", runId: workflowRunId, toolCallId, asyncId: workflowRunId, asyncDir, results: [], chatProgress, ...(deps.state.activeAsyncCapacity ? { activeAsyncCapacity: deps.state.activeAsyncCapacity } : {}) },
5015
+ }, workflowFanoutBudget));
4676
5016
  }
4677
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;
4678
5018
  const workflowResults: SingleResult[] = [];
@@ -4692,28 +5032,29 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4692
5032
  liveWorkflow = { ...liveWorkflow, trace };
4693
5033
  sendWorkflowProgress();
4694
5034
  },
5035
+ admit: (calls) => {
5036
+ claimRunFanoutBatch(workflowFanoutBudget, calls.map(({ key }) => `workflow[${key}]`));
5037
+ },
4695
5038
  onEmit: (emits) => {
4696
5039
  liveWorkflow = { ...liveWorkflow, emits };
4697
5040
  sendWorkflowProgress();
4698
5041
  },
4699
- launch: async (key, childParams, workflowSignal) => {
5042
+ launch: async (key, childParams, workflowSignal, admission) => {
4700
5043
  if (workflowUsageBudget.budget && childParams.async === true) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, "workflow usageBudget does not support async runs.run launches."));
4701
5044
  const budgetState = usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults));
4702
5045
  if (budgetState?.exhausted) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, usageBudgetExceededMessage(budgetState)));
4703
- patchMissionObjective(childParams.task);
4704
5046
  const childPhase = typeof childParams.phase === "string" && childParams.phase.trim() ? childParams.phase.trim() : undefined;
4705
5047
  const childLabel = typeof childParams.label === "string" && childParams.label.trim() ? childParams.label.trim() : undefined;
4706
5048
  recordMissionWorkflowChild(missionBinding, _id, key, {
4707
5049
  status: "running",
4708
5050
  ...(typeof childParams.agent === "string" && childParams.agent.trim() ? { agent: childParams.agent.trim() } : {}),
4709
- ...(typeof childParams.task === "string" && childParams.task.trim() ? { task: childParams.task.trim() } : {}),
4710
5051
  ...(childLabel ? { label: childLabel } : {}),
4711
5052
  ...(childPhase ? { phase: childPhase } : {}),
4712
5053
  heartbeat: { status: "running", ...(childPhase ? { phase: childPhase } : {}) },
4713
5054
  });
4714
5055
  const result = await runMissionWorkflowChild(missionBinding, _id, key, childPhase, () => {
4715
5056
  const childRequest = bindMissionWorkflowChildAsyncLaunch(
4716
- 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 },
4717
5058
  missionBinding,
4718
5059
  deps.asyncByDefault,
4719
5060
  );
@@ -4754,10 +5095,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4754
5095
  if (workflow.emits.length > 0) sections.push(`Emitted:\n${workflow.emits.map(formatWorkflowValue).join("\n")}`);
4755
5096
  if (workflow.console.length > 0) sections.push(`Console:\n${workflow.console.map((entry) => `[${entry.level}] ${entry.text}`).join("\n")}`);
4756
5097
  if (traceLines.length > 0) sections.push(`Call trace:\n${traceLines.join("\n")}`);
4757
- return attachWorkflowMission({
5098
+ return attachWorkflowMission(withRunFanoutBudget({
4758
5099
  content: [{ type: "text", text: sections.join("\n\n") }],
4759
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 }),
4760
- });
5101
+ }, workflowFanoutBudget));
4761
5102
  } catch (error) {
4762
5103
  const partial = error instanceof WorkflowScriptError ? error.partial : { trace: [], emits: [], console: [], children: [] };
4763
5104
  const text = error instanceof Error ? error.message : String(error);
@@ -4766,11 +5107,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4766
5107
  if (partial.emits.length > 0) sections.push(`Emitted:\n${partial.emits.map(formatWorkflowValue).join("\n")}`);
4767
5108
  if (partial.console.length > 0) sections.push(`Console:\n${partial.console.map((entry) => `[${entry.level}] ${entry.text}`).join("\n")}`);
4768
5109
  if (traceLines.length > 0) sections.push(`Call trace:\n${traceLines.join("\n")}`);
4769
- return attachWorkflowMission({
5110
+ return attachWorkflowMission(withRunFanoutBudget({
4770
5111
  content: [{ type: "text", text: sections.join("\n\n") }],
4771
5112
  isError: true,
4772
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 }),
4773
- });
5114
+ }, workflowFanoutBudget));
4774
5115
  }
4775
5116
  }
4776
5117
  const directParams = prepareWorkflowChildParams(requestParams);
@@ -5008,6 +5349,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5008
5349
  if (!sessionError) sessionError = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
5009
5350
  }
5010
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;
5011
5356
  return {
5012
5357
  content: [{
5013
5358
  type: "text",
@@ -5024,7 +5369,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5024
5369
  expandTilde: deps.expandTilde,
5025
5370
  })),
5026
5371
  }],
5027
- details: { mode: "management", results: [], spawnBudget },
5372
+ details: { mode: "management", results: [], spawnBudget, activeAsyncCapacity },
5028
5373
  };
5029
5374
  }
5030
5375
  if (action === "status") {
@@ -5249,6 +5594,28 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5249
5594
  }
5250
5595
  return deps.handleScheduledRunAction(paramsWithResolvedCwd, ctx);
5251
5596
  }
5597
+ if (deps.allowMutatingManagementActions === false && MUTATING_MANAGEMENT_ACTIONS.has(action)) {
5598
+ return {
5599
+ content: [{ type: "text", text: `Action '${action}' is not available from child-safe subagent fanout mode.` }],
5600
+ isError: true,
5601
+ details: { mode: "management" as const, results: [] },
5602
+ };
5603
+ }
5604
+ if (action === "dismiss") {
5605
+ const targetRunId = paramsWithResolvedCwd.runId ?? paramsWithResolvedCwd.id;
5606
+ if (!targetRunId) return { content: [{ type: "text", text: "action='dismiss' requires id." }], isError: true, details: { mode: "management", results: [] } };
5607
+ let resolved: ResolvedSubagentRunId | undefined;
5608
+ try {
5609
+ resolved = resolveSubagentRunId(targetRunId, omitUndefinedProperties({ state: deps.state, nested: nestedResolutionScopeForExecutor(deps) }));
5610
+ } catch (error) {
5611
+ const message = error instanceof Error ? error.message : String(error);
5612
+ return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
5613
+ }
5614
+ if (resolved?.kind !== "async") {
5615
+ return { content: [{ type: "text", text: `Run '${targetRunId}' is not a recovered workflow.` }], isError: true, details: { mode: "management", results: [] } };
5616
+ }
5617
+ return dismissRecoveredWorkflow(deps.state, resolved.location);
5618
+ }
5252
5619
  if (action === "stop") {
5253
5620
  const targetRunId = paramsWithResolvedCwd.runId ?? paramsWithResolvedCwd.id;
5254
5621
  const workflowController = targetRunId ? deps.state.workflowControllers?.get(targetRunId) : undefined;
@@ -5260,6 +5627,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5260
5627
  if (paramsWithResolvedCwd.dir) {
5261
5628
  try {
5262
5629
  const location = resolveAsyncRunLocation(paramsWithResolvedCwd, DIRS.async, DIRS.results);
5630
+ const existingStatus = readStatus(location.asyncDir ?? "");
5631
+ if (existingStatus?.mode === "workflow" && existingStatus.state === "running") {
5632
+ return { content: [{ type: "text", text: `Workflow ${existingStatus.runId} is not controlled by this extension runtime; reload recovery cannot stop it safely.` }], isError: true, details: { mode: "management", results: [] } };
5633
+ }
5263
5634
  const stopResult = stopAsyncRun(deps.state, location.resolvedId ?? targetRunId ?? path.basename(location.asyncDir ?? paramsWithResolvedCwd.dir), deps.kill, location);
5264
5635
  return stopResult ?? { content: [{ type: "text", text: `No running or queued async run was found for '${targetRunId ?? paramsWithResolvedCwd.dir}'.` }], isError: true, details: { mode: "management", results: [] } };
5265
5636
  } catch (error) {
@@ -5344,13 +5715,6 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5344
5715
  details: { mode: "management" as const, results: [] },
5345
5716
  };
5346
5717
  }
5347
- if (deps.allowMutatingManagementActions === false && MUTATING_MANAGEMENT_ACTIONS.has(action)) {
5348
- return {
5349
- content: [{ type: "text", text: `Action '${action}' is not available from child-safe subagent fanout mode.` }],
5350
- isError: true,
5351
- details: { mode: "management" as const, results: [] },
5352
- };
5353
- }
5354
5718
  return handleManagementAction(action, paramsWithResolvedCwd, {
5355
5719
  ...ctx,
5356
5720
  cwd: requestCwd,
@@ -5422,7 +5786,6 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5422
5786
  const runId = randomUUID().slice(0, 8);
5423
5787
  const inheritedNestedRoute = resolveInheritedNestedRouteFromEnv();
5424
5788
  const nestedParentAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
5425
- const nestedRoute = inheritedNestedRoute ?? createNestedRoute(runId);
5426
5789
  const shareEnabled = effectiveParams.share === true;
5427
5790
  const hasChain = (effectiveParams.chain?.length ?? 0) > 0;
5428
5791
  const hasTasks = (effectiveParams.tasks?.length ?? 0) > 0;
@@ -5506,9 +5869,50 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5506
5869
  const foregroundTimeout = resolveSingleAgentLaunchTimeout(
5507
5870
  effectiveParams,
5508
5871
  effectiveAsync,
5872
+ resolveConfigDefaultTimeoutMs(deps.config.timeoutMs),
5509
5873
  );
5510
5874
  if (foregroundTimeout.error) return buildRequestedModeError(effectiveParams, foregroundTimeout.error);
5511
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);
5512
5916
 
5513
5917
  const artifactConfig: ArtifactConfig = omitUndefinedProperties({
5514
5918
  ...DEFAULT_ARTIFACT_CONFIG,
@@ -5534,6 +5938,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5534
5938
  try {
5535
5939
  fs.mkdirSync(sessionRoot, { recursive: true });
5536
5940
  } catch (error) {
5941
+ activeAsyncCapacity?.rollback();
5537
5942
  const message = error instanceof Error ? error.message : String(error);
5538
5943
  return toExecutionErrorResult(
5539
5944
  effectiveParams,
@@ -5564,10 +5969,14 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5564
5969
  preflightForkSessionsForStaticTasks(effectiveParams, contextPolicy, forkSessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems);
5565
5970
  }
5566
5971
  } catch (error) {
5972
+ activeAsyncCapacity?.rollback();
5567
5973
  return toExecutionErrorResult(effectiveParams, error, contextPolicy.contextSummary);
5568
5974
  }
5569
5975
  const chainBindingsError = validateExecutionChainBindings(effectiveParams, deps.config.chain?.dynamicFanout?.maxItems);
5570
- if (chainBindingsError) return withResolvedContext(chainBindingsError, contextPolicy.contextSummary);
5976
+ if (chainBindingsError) {
5977
+ activeAsyncCapacity?.rollback();
5978
+ return withResolvedContext(chainBindingsError, contextPolicy.contextSummary);
5979
+ }
5571
5980
 
5572
5981
  const onUpdateWithContext = onUpdate
5573
5982
  ? (r: AgentToolResult<Details>) => onUpdate(withResolvedContext({
@@ -5587,14 +5996,17 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5587
5996
  ownerSessionId: requestSessionId,
5588
5997
  });
5589
5998
  } catch (error) {
5590
- if (explicitMission) return toExecutionErrorResult(effectiveParams, error, contextPolicy.contextSummary);
5999
+ if (explicitMission) {
6000
+ activeAsyncCapacity?.rollback();
6001
+ return toExecutionErrorResult(effectiveParams, error, contextPolicy.contextSummary);
6002
+ }
5591
6003
  missionWarning = `Mission tracking unavailable: ${error instanceof Error ? error.message : String(error)}`;
5592
6004
  }
5593
6005
 
5594
6006
  const attachMission = (result: AgentToolResult<Details>): AgentToolResult<Details> => {
5595
6007
  if (!missionBinding) return missionWarning ? { ...result, details: { ...result.details, missionWarning } } : result;
5596
6008
  try {
5597
- return attachMissionToLaunchResult({ binding: missionBinding, result });
6009
+ return attachMissionToLaunchResult({ binding: delegatedExecution ? { ...missionBinding, announceInContent: false } : missionBinding, result });
5598
6010
  } catch (error) {
5599
6011
  const warning = `Mission tracking unavailable after launch: ${error instanceof Error ? error.message : String(error)}`;
5600
6012
  if (explicitMission) {
@@ -5615,7 +6027,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5615
6027
  requestSessionId,
5616
6028
  requestedSpawns,
5617
6029
  );
5618
- if (reservation.error) return attachMission(spawnBudgetErrorResult(reservation.error, foregroundMode));
6030
+ if (reservation.error) {
6031
+ activeAsyncCapacity?.rollback();
6032
+ return attachMission(spawnBudgetErrorResult(reservation.error, foregroundMode));
6033
+ }
5619
6034
 
5620
6035
  const execData: ExecutionContextData = omitUndefinedProperties({
5621
6036
  params: effectiveParams,
@@ -5635,6 +6050,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5635
6050
  artifactsDir,
5636
6051
  backgroundRequestedWhileClarifying,
5637
6052
  effectiveAsync,
6053
+ asyncRunId,
5638
6054
  controlConfig,
5639
6055
  intercomBridge,
5640
6056
  nestedRoute,
@@ -5650,11 +6066,14 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5650
6066
  parentSessionId: requestSessionId,
5651
6067
  parentPiSessionId: requestPiSessionId,
5652
6068
  capabilityCeiling: resolveCurrentSubagentCapabilityCeiling(requestSessionId),
6069
+ runFanoutBudget,
6070
+ topLevelAsyncCapacityEligible,
6071
+ activeAsyncCapacity,
5653
6072
  });
5654
6073
 
5655
- const foregroundDescription = effectiveParams.task?.trim()
5656
- || effectiveParams.tasks?.[0]?.task?.trim()
5657
- || (effectiveParams.chain ? firstRawChainTask(effectiveParams.chain)?.trim() : undefined);
6074
+ const foregroundDescription = selectedAgentNames.length === 1
6075
+ ? `${selectedAgentNames[0]} child`
6076
+ : `${selectedAgentNames.length} live children`;
5658
6077
  const parentWorkflowStatus = effectiveParams.workflowParentRunId
5659
6078
  ? readStatus(path.join(DIRS.async, effectiveParams.workflowParentRunId))
5660
6079
  : null;
@@ -5689,6 +6108,37 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5689
6108
  interrupt: undefined,
5690
6109
  });
5691
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
+ };
5692
6142
  deps.state.foregroundControls.set(runId, foregroundControl);
5693
6143
  deps.state.lastForegroundControlId = runId;
5694
6144
  }
@@ -5783,9 +6233,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5783
6233
  if (workflowLaunchObserver) {
5784
6234
  const singleTask = hasTasks && effectiveParams.tasks?.length === 1 ? effectiveParams.tasks[0] : undefined;
5785
6235
  const launch = hasSingle
5786
- ? { 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 } : {}) }
5787
6237
  : singleTask
5788
- ? { 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 } : {}) }
5789
6239
  : undefined;
5790
6240
  if (launch) {
5791
6241
  workflowLaunchObservers.delete(params);
@@ -5793,31 +6243,33 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5793
6243
  }
5794
6244
  }
5795
6245
  const asyncResult = runAsyncPath(execData, deps);
5796
- if (asyncResult) return attachMission(withResolvedContext(withForkThinkingNotes(asyncResult, forkThinkingDowngrades), contextPolicy.contextSummary));
6246
+ if (asyncResult) return attachMission(withRunFanoutBudget(withResolvedContext(withForkThinkingNotes(asyncResult, forkThinkingDowngrades), contextPolicy.contextSummary), runFanoutBudget));
5797
6247
  if (foregroundControl) {
5798
6248
  writeNestedForegroundEvent("subagent.nested.started");
5799
6249
  nestedForegroundStarted = true;
5800
6250
  }
6251
+ const runFanoutAnnotateContent = !delegatedExecution;
5801
6252
  if (hasChain && effectiveParams.chain) {
5802
6253
  const result = await runChainPath(execData, deps);
5803
6254
  writeNestedForegroundEvent("subagent.nested.completed", result);
5804
- return attachMission(withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary));
6255
+ return attachMission(withRunFanoutBudget(withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary), runFanoutBudget, { annotateContent: runFanoutAnnotateContent }));
5805
6256
  }
5806
6257
  if (hasTasks && effectiveParams.tasks) {
5807
6258
  const result = await runParallelPath(execData, deps);
5808
6259
  writeNestedForegroundEvent("subagent.nested.completed", result);
5809
- return attachMission(withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary));
6260
+ return attachMission(withRunFanoutBudget(withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary), runFanoutBudget, { annotateContent: runFanoutAnnotateContent }));
5810
6261
  }
5811
6262
  if (hasSingle) {
5812
6263
  const result = await runSinglePath(execData, deps);
5813
6264
  writeNestedForegroundEvent("subagent.nested.completed", result);
5814
- return attachMission(withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary));
6265
+ return attachMission(withRunFanoutBudget(withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary), runFanoutBudget, { annotateContent: runFanoutAnnotateContent }));
5815
6266
  }
5816
6267
  } catch (error) {
5817
6268
  const errorResult = withForkThinkingNotes(toExecutionErrorResult(effectiveParams, error, contextPolicy.contextSummary), forkThinkingDowngrades);
5818
6269
  if (nestedForegroundStarted) writeNestedForegroundEvent("subagent.nested.completed", errorResult);
5819
6270
  return attachMission(errorResult);
5820
6271
  } finally {
6272
+ if (activeAsyncCapacity && !activeAsyncCapacity.owner.runnerStartedAt) activeAsyncCapacity.rollback();
5821
6273
  if (foregroundControl) {
5822
6274
  settleForegroundSchedulingOwner(foregroundControl);
5823
6275
  removeForegroundControlIfIdle(deps.state, runId);
@@ -5886,6 +6338,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5886
6338
  delete privateParams.delegatedAllowZeroToolBudget;
5887
6339
  if (thinkingOverride !== undefined) delegatedThinkingOverrides.set(delegatedParams, thinkingOverride);
5888
6340
  if (allowZeroToolBudget) delegatedZeroToolBudgets.add(delegatedParams);
6341
+ delegatedExecutions.add(delegatedParams);
5889
6342
  return execute(id, delegatedParams, signal, onUpdate, ctx);
5890
6343
  };
5891
6344