@selesai/code 0.8.4 → 0.8.6

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 (147) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +7 -1
  3. package/dist/core/slash-commands.js +1 -0
  4. package/dist/defaults/models.json +46 -55
  5. package/dist/defaults/settings.json +7 -8
  6. package/dist/extensions/agent-browser.test.ts +178 -2
  7. package/dist/extensions/agent-browser.ts +2 -0
  8. package/dist/extensions/copy-turn.test.ts +229 -0
  9. package/dist/extensions/copy-turn.ts +11 -0
  10. package/dist/extensions/grep-app/index.test.ts +356 -1
  11. package/dist/extensions/grep-app/index.ts +6 -1
  12. package/dist/extensions/handoff-new.test.ts +351 -161
  13. package/dist/extensions/handoff-new.ts +3 -0
  14. package/dist/extensions/inline-skills.test.ts +93 -0
  15. package/dist/extensions/inline-skills.ts +3 -0
  16. package/dist/extensions/pi-subagents/CHANGELOG.md +26 -3
  17. package/dist/extensions/pi-subagents/README.md +2 -0
  18. package/dist/extensions/pi-subagents/agents/builder.md +5 -2
  19. package/dist/extensions/pi-subagents/agents/commentator.md +1 -1
  20. package/dist/extensions/pi-subagents/docs/configuration.md +54 -2
  21. package/dist/extensions/pi-subagents/docs/observability.md +1 -1
  22. package/dist/extensions/pi-subagents/docs/tool-reference.md +1 -1
  23. package/dist/extensions/pi-subagents/docs/workflows.md +1 -1
  24. package/dist/extensions/pi-subagents/package-lock.json +2 -2
  25. package/dist/extensions/pi-subagents/package.json +1 -1
  26. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +1 -1
  27. package/dist/extensions/pi-subagents/src/agents/agents.ts +9 -3
  28. package/dist/extensions/pi-subagents/src/extension/config.ts +12 -0
  29. package/dist/extensions/pi-subagents/src/extension/doctor.ts +40 -0
  30. package/dist/extensions/pi-subagents/src/extension/index.ts +104 -14
  31. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +5 -0
  32. package/dist/extensions/pi-subagents/src/extension/rpc.ts +3 -9
  33. package/dist/extensions/pi-subagents/src/extension/schemas.ts +2 -2
  34. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -1
  35. package/dist/extensions/pi-subagents/src/missions/lifecycle.ts +4 -7
  36. package/dist/extensions/pi-subagents/src/missions/store.ts +4 -4
  37. package/dist/extensions/pi-subagents/src/missions/workflow-state.ts +6 -2
  38. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +374 -0
  39. package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +9 -5
  40. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +117 -29
  41. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +12 -6
  42. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +7 -1
  43. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +14 -5
  44. package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +33 -15
  45. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +5 -2
  46. package/dist/extensions/pi-subagents/src/runs/background/owned-process-tree.ts +104 -0
  47. package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +17 -3
  48. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +19 -1
  49. package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +2 -2
  50. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +5 -2
  51. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +12 -6
  52. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +3 -3
  53. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +84 -36
  54. package/dist/extensions/pi-subagents/src/runs/background/wait-subscriptions.ts +2 -1
  55. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +37 -2
  56. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +135 -22
  57. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +12 -0
  58. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +7 -4
  59. package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +171 -0
  60. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +653 -196
  61. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +13 -4
  62. package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +286 -0
  63. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +23 -0
  64. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
  65. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +44 -1
  66. package/dist/extensions/pi-subagents/src/runs/shared/run-fanout-budget.ts +280 -0
  67. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +4 -2
  68. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +19 -3
  69. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +17 -5
  70. package/dist/extensions/pi-subagents/src/shared/session-lineage.ts +71 -0
  71. package/dist/extensions/pi-subagents/src/shared/types.ts +108 -0
  72. package/dist/extensions/pi-subagents/src/shared/utils.ts +3 -1
  73. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +7 -5
  74. package/dist/extensions/pi-subagents/src/tui/fleet.ts +226 -13
  75. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +22 -5
  76. package/dist/extensions/pi-subagents/src/workflows/workflow-auto-relaunch.ts +28 -0
  77. package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +87 -0
  78. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +96 -6
  79. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +28 -5
  80. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +8 -5
  81. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +83 -2
  82. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +211 -15
  83. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +41 -3
  84. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +314 -10
  85. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +31 -1
  86. package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +317 -0
  87. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +21 -2
  88. package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +16 -1
  89. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +6 -1
  90. package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +75 -4
  91. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +18 -0
  92. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +29 -1
  93. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +31 -5
  94. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +139 -1
  95. package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +10 -0
  96. package/dist/extensions/pi-subagents/test/unit/handoff-adoption.test.ts +103 -0
  97. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +22 -0
  98. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +2 -1
  99. package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +171 -0
  100. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +30 -4
  101. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +14 -0
  102. package/dist/extensions/pi-subagents/test/unit/owned-process-tree.test.ts +69 -0
  103. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +83 -0
  104. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +2 -2
  105. package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +55 -2
  106. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +12 -1
  107. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +7 -7
  108. package/dist/extensions/pi-subagents/test/unit/run-fanout-budget.test.ts +121 -0
  109. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +9 -0
  110. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +4 -3
  111. package/dist/extensions/pi-subagents/test/unit/session-lineage.test.ts +73 -0
  112. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +28 -0
  113. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +10 -1
  114. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +39 -0
  115. package/dist/extensions/pi-subagents/test/unit/timeout-defaults.test.ts +65 -0
  116. package/dist/extensions/pi-subagents/test/unit/workflow-auto-relaunch.test.ts +45 -0
  117. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +61 -0
  118. package/dist/extensions/ponytail/index.js +11 -0
  119. package/dist/extensions/ponytail/ponytail-config.cjs +2 -0
  120. package/dist/extensions/ponytail/ponytail-instructions.cjs +6 -0
  121. package/dist/extensions/ponytail/test/extension.test.js +274 -140
  122. package/dist/extensions/ponytail/test/helpers.test.js +280 -92
  123. package/dist/extensions/question/index.ts +18 -0
  124. package/dist/extensions/question/question-list.ts +22 -0
  125. package/dist/extensions/question/tests/batch.test.ts +103 -65
  126. package/dist/extensions/question/tests/helpers.test.ts +68 -35
  127. package/dist/extensions/question/tests/question-list.test.ts +506 -204
  128. package/dist/extensions/question/tests/row-layout.test.ts +180 -114
  129. package/dist/extensions/question/tests/schemas.test.ts +42 -19
  130. package/dist/extensions/question/tests/shortcuts.test.ts +106 -84
  131. package/dist/extensions/question/tests/wizard.test.ts +771 -50
  132. package/dist/extensions/rtk.test.ts +180 -1
  133. package/dist/extensions/tokenin-onboarding.ts +3 -0
  134. package/dist/extensions/undo.test.ts +720 -0
  135. package/dist/extensions/undo.ts +6 -0
  136. package/dist/extensions/web-agent-onboarding.test.ts +415 -0
  137. package/dist/extensions/workflow/extension.ts +3 -3
  138. package/dist/extensions/workflow/modes.ts +41 -19
  139. package/dist/modes/interactive/interactive-mode.d.ts +1 -0
  140. package/dist/modes/interactive/interactive-mode.js +48 -1
  141. package/dist/skills/pi-subagents/references/execution-controls.md +1 -1
  142. package/docs/plans/workflow-autoloop-reference.md +1 -2
  143. package/docs/plans/workflow-handoff-carryover.md +108 -0
  144. package/docs/quickstart.md +27 -43
  145. package/docs/settings.md +4 -0
  146. package/docs/usage.md +1 -0
  147. package/package.json +5 -1
@@ -17,6 +17,7 @@ import {
17
17
  settleForegroundSchedulingOwner,
18
18
  updateForegroundChild,
19
19
  } from "./foreground-control.ts";
20
+ import { getLivePromptAudit, rewritePromptWithGuidance, updateLiveEffectivePrompt } from "./prompt-audit.ts";
20
21
  import { persistForegroundRunHistory, MAX_REMEMBERED_FOREGROUND_RUNS } from "./foreground-history.ts";
21
22
  import { resolveExecutionAgentScope } from "../../agents/agent-scope.ts";
22
23
  import { handleManagementAction } from "../../agents/agent-management.ts";
@@ -54,6 +55,7 @@ import {
54
55
  import { discoverAvailableSkills, normalizeSkillInput } from "../../agents/skills.ts";
55
56
  import { buildAsyncRunnerSteps, DEFAULT_ASYNC_TIMEOUT_MS, executeAsyncChain, executeAsyncSingle, formatAsyncStartedMessage, isAsyncAvailable, workflowAwaitedAsyncResultPath } from "../background/async-execution.ts";
56
57
  import { updateActiveRunIndex } from "../background/active-run-index.ts";
58
+ import { acquireActiveAsyncCapacity, ActiveAsyncCapacityError, getActiveAsyncCapacitySnapshot, resolveMaxActiveAsyncRunsPerSession, transferActiveAsyncCapacity, type ActiveAsyncCapacityHandle } from "../background/active-async-capacity.ts";
57
59
  import { isScheduledRunAction, type ScheduledRunAction } from "../background/scheduled-runs.ts";
58
60
  import { enqueueChainAppendRequest, readPendingChainAppendRequests, runnerStepOutputNames } from "../background/chain-append.ts";
59
61
  import { ChainOutputValidationError, validateChainOutputBindingsWithContext } from "../shared/chain-outputs.ts";
@@ -64,6 +66,7 @@ import { applyIntercomBridgeToAgent, INTERCOM_BRIDGE_MARKER, resolveIntercomBrid
64
66
  import { formatControlIntercomMessage, formatControlNoticeMessage, resolveControlConfig, shouldNotifyControlEvent } from "../shared/subagent-control.ts";
65
67
  import { resolveTurnBudgetConfig } from "../shared/turn-budget.ts";
66
68
  import { formatSpawnBudget, getSpawnBudgetSnapshot, grantSpawnBudget, preflightSpawnBudget, preflightSpawnBudgetGrant, reserveSpawnBudget } from "../shared/spawn-budget.ts";
69
+ import { claimRunFanoutBatch, claimRunFanoutBatchWithCommit, createRunFanoutBudget, decodeRunFanoutBudgetDescriptor, formatRunFanoutBudget, getRunFanoutBudgetSnapshot, readRunFanoutBudgetDescriptor, RunFanoutLimitError, RUN_FANOUT_BUDGET_ENV, writeRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
67
70
  import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
68
71
  import { usageBudgetExceededMessage, usageBudgetState, validateUsageBudgetConfig } from "../shared/usage-budget.ts";
69
72
  import { intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
@@ -71,6 +74,7 @@ import { isAgentContractV1 } from "../shared/agent-contract.ts";
71
74
  import { finalizeSingleOutput, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
72
75
  import { cleanupStructuredOutputRuntime, createStructuredOutputRuntime } from "../shared/structured-output.ts";
73
76
  import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, readStatus, resolveChildCwd, sumResultsCost, sumResultsUsage } from "../../shared/utils.ts";
77
+ import { createTaskMutationArbiter } from "../shared/llm-intent-arbiter.ts";
74
78
  import { DEFAULT_GLOBAL_CONCURRENCY_LIMIT, Semaphore } from "../shared/parallel-utils.ts";
75
79
  import { discardPreservedWorktrees, formatParallelHandoffError, formatParallelHandoffReference, parallelHandoffPath, writeParallelHandoffGroup, writePendingParallelHandoff } from "../shared/parallel-handoff.ts";
76
80
  import { summarizeContextModes, type ContextMode, type ContextSummary } from "../shared/context-mode.ts";
@@ -111,6 +115,7 @@ import { resolveAuthorityDecision } from "../../policy/authority.ts";
111
115
  import { handleHerdrInspectorAction, HERDR_INSPECTOR_ACTIONS } from "../../inspectors/herdr/actions.ts";
112
116
  import { handleHerdrProjectPaneAction, HERDR_PROJECT_PANE_ACTIONS } from "../../inspectors/herdr/project-panes.ts";
113
117
  import { previewSimpleWorkflowRun, runWorkflowScript, WorkflowScriptError, type WorkflowScriptChildResult } from "../../workflows/scripted-workflow.ts";
118
+ import { resolveAutoRelaunchDecision, resolveMaxWorkflowAutoRelaunches } from "../../workflows/workflow-auto-relaunch.ts";
114
119
  import { renderWorkflowPrompt } from "../../shared/prompt-resources.ts";
115
120
  import { resolveWorkflowChatProgress, type WorkflowChatProgressProjection } from "../../workflows/chat-progress.ts";
116
121
  import {
@@ -143,6 +148,7 @@ import {
143
148
  type ResolvedControlConfig,
144
149
  type ResolvedTurnBudget,
145
150
  type ResolvedToolBudget,
151
+ type RunFanoutBudgetDescriptor,
146
152
  type SingleResult,
147
153
  type ToolBudgetConfig,
148
154
  type TurnBudgetConfig,
@@ -161,6 +167,7 @@ import {
161
167
  resolveTopLevelParallelMaxTasks,
162
168
  resolveChildMaxSubagentDepth,
163
169
  resolveCurrentMaxSubagentDepth,
170
+ resolveMaxSubagentSpawnsPerRun,
164
171
  wrapForkTask,
165
172
  } from "../../shared/types.ts";
166
173
 
@@ -284,6 +291,10 @@ export interface SubagentParamsLike {
284
291
  workflowKey?: string;
285
292
  workflowChildAsyncId?: string;
286
293
  suppressRoutineResultIntercom?: boolean;
294
+ /** Internal inherited cumulative run-tree budget. */
295
+ runFanoutBudget?: RunFanoutBudgetDescriptor;
296
+ /** Internal workflow host admission proof. */
297
+ runFanoutAdmitted?: boolean;
287
298
  /** Internal durable-run compatibility fields. Public callers must use workflowScript. */
288
299
  chain?: ChainStep[];
289
300
  tasks?: TaskParam[];
@@ -388,6 +399,7 @@ interface ExecutionContextData {
388
399
  artifactsDir: string;
389
400
  backgroundRequestedWhileClarifying: boolean;
390
401
  effectiveAsync: boolean;
402
+ asyncRunId: string;
391
403
  controlConfig: ResolvedControlConfig;
392
404
  intercomBridge: IntercomBridgeState;
393
405
  nestedRoute?: NestedRouteInfo;
@@ -404,6 +416,9 @@ interface ExecutionContextData {
404
416
  parentSessionId: string | null;
405
417
  parentPiSessionId?: string;
406
418
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
419
+ runFanoutBudget: RunFanoutBudgetDescriptor;
420
+ topLevelAsyncCapacityEligible: boolean;
421
+ activeAsyncCapacity?: ActiveAsyncCapacityHandle;
407
422
  }
408
423
 
409
424
  function resolveRequestedCwd(runtimeCwd: string, requestedCwd: string | undefined): string {
@@ -432,6 +447,17 @@ function getForegroundControl(state: SubagentState, runId: string | undefined) {
432
447
  return newest;
433
448
  }
434
449
 
450
+ function promptAuditRedoParams(value: unknown, rewrittenTask: string): SubagentParamsLike {
451
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Prompt redo is missing a safe live launch contract.");
452
+ const params = { ...(value as SubagentParamsLike), task: rewrittenTask, async: false };
453
+ delete params.workflowParentRunId;
454
+ delete params.workflowKey;
455
+ delete params.workflowChildAsyncId;
456
+ delete params.suppressRoutineResultIntercom;
457
+ if (params.worktree === true && Array.isArray(params.tasks)) delete params.cwd;
458
+ return params;
459
+ }
460
+
435
461
  function formatForegroundActivity(control: SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never): string | undefined {
436
462
  const facts: string[] = [];
437
463
  if (control.currentTool && control.currentToolStartedAt) facts.push(`tool ${control.currentTool} for ${Math.floor(Math.max(0, Date.now() - control.currentToolStartedAt) / 1000)}s`);
@@ -483,12 +509,18 @@ function withSpawnBudgetStatus(
483
509
  sessionId: string | null,
484
510
  ): AgentToolResult<Details> {
485
511
  const spawnBudget = getSpawnBudgetSnapshot(state, config, sessionId);
512
+ const activeAsyncCapacity = sessionId
513
+ ? getActiveAsyncCapacitySnapshot(sessionId, resolveMaxActiveAsyncRunsPerSession(config.maxActiveAsyncRunsPerSession), {
514
+ liveWorkflowRunIds: new Set(state.workflowControllers?.keys() ?? []),
515
+ })
516
+ : { used: 0, limit: resolveMaxActiveAsyncRunsPerSession(config.maxActiveAsyncRunsPerSession) ?? 0 };
517
+ state.activeAsyncCapacity = activeAsyncCapacity;
486
518
  return {
487
519
  ...result,
488
520
  content: result.content.map((item, index) => index === 0 && item.type === "text"
489
- ? { ...item, text: `${formatSpawnBudget(spawnBudget)}\n${item.text}` }
521
+ ? { ...item, text: `${formatSpawnBudget(spawnBudget)}\nActive async capacity: ${activeAsyncCapacity.used}/${activeAsyncCapacity.limit || "unlimited"} used\n${item.text}` }
490
522
  : item),
491
- details: { ...result.details, spawnBudget },
523
+ details: { ...result.details, spawnBudget, activeAsyncCapacity },
492
524
  };
493
525
  }
494
526
 
@@ -509,6 +541,31 @@ function countRequestedSubagentSpawns(params: SubagentParamsLike, config: Extens
509
541
  return params.agent ? 1 : 0;
510
542
  }
511
543
 
544
+ function staticRunFanoutPaths(params: SubagentParamsLike): string[] {
545
+ if (params.tasks) return params.tasks.map((_, index) => `tasks[${index}]`);
546
+ if (params.chain) return params.chain.flatMap((step, stepIndex) => {
547
+ if (isDynamicParallelStep(step) || "checkpoint" in step) return [];
548
+ if (isParallelStep(step)) return step.parallel.map((_, itemIndex) => `chain[${stepIndex}].parallel[${itemIndex}]`);
549
+ return [`chain[${stepIndex}]`];
550
+ });
551
+ return params.agent ? ["single"] : [];
552
+ }
553
+
554
+ function runFanoutErrorResult(error: RunFanoutLimitError, mode: "single" | "parallel" | "chain"): AgentToolResult<Details> {
555
+ return { content: [{ type: "text", text: error.message }], isError: true, details: { mode, results: [], runFanoutBudget: error.snapshot, runFanoutRejection: error.rejection } };
556
+ }
557
+
558
+ function withRunFanoutBudget(result: AgentToolResult<Details>, descriptor: RunFanoutBudgetDescriptor, options: { annotateContent?: boolean } = {}): AgentToolResult<Details> {
559
+ const runFanoutBudget = getRunFanoutBudgetSnapshot(descriptor);
560
+ return {
561
+ ...result,
562
+ content: options.annotateContent === false
563
+ ? result.content
564
+ : result.content.map((item, index) => index === 0 && item.type === "text" ? { ...item, text: `${formatRunFanoutBudget(runFanoutBudget)}\n${item.text}` } : item),
565
+ details: { ...result.details, runFanoutBudget },
566
+ };
567
+ }
568
+
512
569
  function foregroundStatusResult(control: SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never): AgentToolResult<Details> {
513
570
  let nestedWarning: string | undefined;
514
571
  try {
@@ -725,7 +782,8 @@ function updateRememberedForegroundChild(state: SubagentState, input: { runId: s
725
782
  function resolveForegroundResumeTarget(params: SubagentParamsLike, state: SubagentState): { runId: string; mode: SubagentRunMode; state: "complete"; agent: string; index: number; cwd: string; sessionFile: string; model?: string; thinking?: string; launchContractDigest?: string; capabilityCeiling?: ResolvedSubagentCapabilityCeiling } | undefined {
726
783
  const requested = (params.id ?? params.runId)?.trim();
727
784
  if (!requested || !state.foregroundRuns?.size || !state.currentSessionId) return undefined;
728
- const sessionRuns = [...state.foregroundRuns.values()].filter((run) => run.sessionId === state.currentSessionId);
785
+ const acceptedSessions = new Set(state.sessionLineage?.length ? state.sessionLineage : [state.currentSessionId]);
786
+ const sessionRuns = [...state.foregroundRuns.values()].filter((run) => run.sessionId !== undefined && acceptedSessions.has(run.sessionId));
729
787
  const direct = sessionRuns.find((run) => run.runId === requested);
730
788
  const matches = direct ? [direct] : sessionRuns.filter((run) => run.runId.startsWith(requested));
731
789
  if (matches.length === 0) return undefined;
@@ -1141,18 +1199,30 @@ function appendStepToAsyncChain(input: {
1141
1199
  }
1142
1200
 
1143
1201
  try {
1202
+ const asyncDir = resolved.location.asyncDir;
1203
+ if (!asyncDir) throw new Error(`Run '${resolved.id}' is missing its async directory.`);
1204
+ const runFanoutBudget = readRunFanoutBudgetDescriptor(asyncDir);
1205
+ if (!runFanoutBudget) throw new Error(`Run '${resolved.id}' is missing its run fan-out budget identity.`);
1206
+ const startIndex = (status.chainStepCount ?? status.steps?.length ?? 0) + pendingAppendRequests.reduce((total, request) => total + request.steps.length, 0);
1207
+ const appendPaths = chain.flatMap((step, localIndex) => {
1208
+ const absoluteIndex = startIndex + localIndex;
1209
+ if (isDynamicParallelStep(step) || "checkpoint" in step) return [];
1210
+ if (isParallelStep(step)) return step.parallel.map((_, itemIndex) => `chain[${absoluteIndex}].parallel[${itemIndex}]`);
1211
+ return [`chain[${absoluteIndex}]`];
1212
+ });
1144
1213
  const result = enqueueChainAppendRequest({
1145
- asyncDir: resolved.location.asyncDir,
1214
+ asyncDir,
1146
1215
  runId: resolved.id,
1147
1216
  steps: built.steps,
1217
+ admit: (persist) => claimRunFanoutBatchWithCommit(runFanoutBudget, appendPaths, persist),
1148
1218
  });
1149
1219
  const stepText = built.steps.length === 1 ? "step" : "steps";
1150
1220
  return {
1151
1221
  content: [{
1152
1222
  type: "text",
1153
- text: `Append queued for chain run ${resolved.id}: ${built.steps.length} ${stepText}. It becomes eligible after the chain's already-queued steps finish. Pending appends: ${result.pendingCount}.`,
1223
+ text: `Append queued for chain run ${resolved.id}: ${built.steps.length} ${stepText}. It becomes eligible after the chain's already-queued steps finish. Pending appends: ${result.pendingCount}.${result.bookkeepingError ? ` Bookkeeping warning: ${result.bookkeepingError}` : ""}`,
1154
1224
  }],
1155
- details: { mode: "management", results: [], asyncId: resolved.id, asyncDir: resolved.location.asyncDir },
1225
+ details: { mode: "management", results: [], asyncId: resolved.id, asyncDir },
1156
1226
  };
1157
1227
  } catch (error) {
1158
1228
  const message = error instanceof Error ? error.message : String(error);
@@ -1487,6 +1557,20 @@ async function resumeAsyncRun(input: {
1487
1557
  };
1488
1558
  }
1489
1559
  const runId = randomUUID().slice(0, 8);
1560
+ const topLevelResume = depth === 0 && !resolveInheritedNestedRouteFromEnv() && !input.params.workflowParentRunId;
1561
+ let activeAsyncCapacity: ActiveAsyncCapacityHandle | undefined;
1562
+ try {
1563
+ activeAsyncCapacity = topLevelResume ? acquireActiveAsyncCapacity({
1564
+ sessionId: input.deps.state.currentSessionId!,
1565
+ limit: resolveMaxActiveAsyncRunsPerSession(input.deps.config.maxActiveAsyncRunsPerSession),
1566
+ runId,
1567
+ kind: "runner",
1568
+ asyncDir: path.join(DIRS.async, runId),
1569
+ }, { liveWorkflowRunIds: new Set(input.deps.state.workflowControllers?.keys() ?? []) }) : undefined;
1570
+ } catch (error) {
1571
+ if (error instanceof ActiveAsyncCapacityError) return { content: [{ type: "text", text: error.message }], isError: true, details: { mode: "chain", results: [], activeAsyncCapacity: error.snapshot } };
1572
+ throw error;
1573
+ }
1490
1574
  const artifactConfig: ArtifactConfig = omitUndefinedProperties({ ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false, dir: input.deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir });
1491
1575
  const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo);
1492
1576
  const contextPolicy = resolveExplicitContextPolicy(input.params);
@@ -1538,9 +1622,14 @@ async function resumeAsyncRun(input: {
1538
1622
  controlIntercomTarget: intercomBridge.active ? intercomBridge.orchestratorTarget : undefined,
1539
1623
  childIntercomTarget: intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
1540
1624
  globalConcurrencyLimit: input.deps.config.globalConcurrencyLimit,
1625
+ runFanoutBudget: createRunFanoutBudget(runId, resolveMaxSubagentSpawnsPerRun(input.deps.config.maxSubagentSpawnsPerRun)),
1541
1626
  capabilityCeiling: intersectSubagentCapabilityCeilings("capabilityCeiling" in target ? target.capabilityCeiling : undefined, resolveCurrentSubagentCapabilityCeiling(input.deps.state.currentSessionId)),
1627
+ activeAsyncCapacity,
1542
1628
  }));
1543
- if (result.isError) return result;
1629
+ if (result.isError) {
1630
+ activeAsyncCapacity?.rollback();
1631
+ return result;
1632
+ }
1544
1633
  const attachedId = result.details.asyncId ?? runId;
1545
1634
  const lines = [
1546
1635
  `Attached async subagent ${target.runId} as the first step of a new chain.`,
@@ -1559,12 +1648,38 @@ async function resumeAsyncRun(input: {
1559
1648
  if (!revivalSessionFile) {
1560
1649
  return { content: [{ type: "text", text: `Async child '${target.runId}' has no persisted session file to resume.` }], isError: true, details: { mode: "management", results: [] } };
1561
1650
  }
1651
+ if (target.source === "async" && !recoveryDescriptor) {
1652
+ 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: [] } };
1653
+ }
1562
1654
  const runId = randomUUID().slice(0, 8);
1655
+ const topLevelResume = depth === 0 && !resolveInheritedNestedRouteFromEnv() && !input.params.workflowParentRunId;
1656
+ let activeAsyncCapacity: ActiveAsyncCapacityHandle | undefined;
1657
+ try {
1658
+ activeAsyncCapacity = !topLevelResume ? undefined : target.source === "async"
1659
+ ? transferActiveAsyncCapacity({
1660
+ sessionId: input.deps.state.currentSessionId!,
1661
+ limit: resolveMaxActiveAsyncRunsPerSession(input.deps.config.maxActiveAsyncRunsPerSession),
1662
+ sourceRunId: target.runId,
1663
+ runId,
1664
+ asyncDir: path.join(DIRS.async, runId),
1665
+ })
1666
+ : acquireActiveAsyncCapacity({
1667
+ sessionId: input.deps.state.currentSessionId!,
1668
+ limit: resolveMaxActiveAsyncRunsPerSession(input.deps.config.maxActiveAsyncRunsPerSession),
1669
+ runId,
1670
+ kind: "runner",
1671
+ asyncDir: path.join(DIRS.async, runId),
1672
+ });
1673
+ } catch (error) {
1674
+ if (error instanceof ActiveAsyncCapacityError) return { content: [{ type: "text", text: error.message }], isError: true, details: { mode: "single", results: [], activeAsyncCapacity: error.snapshot } };
1675
+ return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, details: { mode: "single", results: [] } };
1676
+ }
1563
1677
  const recoveryAgentConfig = recoveryDescriptor ? applySteeringRecoveryAgentConfig(agentConfig, recoveryDescriptor) : agentConfig;
1564
1678
  const artifactConfig: ArtifactConfig = recoveryDescriptor?.artifactConfig ?? omitUndefinedProperties({ ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false, dir: input.deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir });
1565
1679
  const artifactsDir = recoveryDescriptor?.artifactsDir ?? getArtifactsDir(parentSessionFile, effectiveCwd, artifactConfig.dir);
1566
1680
  const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo);
1567
1681
  const parentModel = input.parentModel;
1682
+ const revivalAsyncDir = path.join(DIRS.async, runId);
1568
1683
  const result = executeAsyncSingle(runId, compactOptional<Parameters<typeof executeAsyncSingle>[1]>({
1569
1684
  agent: target.agent,
1570
1685
  task: buildRevivedAsyncTask(target as Parameters<typeof buildRevivedAsyncTask>[0], effectiveFollowUp),
@@ -1618,10 +1733,28 @@ async function resumeAsyncRun(input: {
1618
1733
  ...(input.params.turnBudget !== undefined ? { turnBudget: input.params.turnBudget } : {}),
1619
1734
  ...(input.params.toolBudget !== undefined ? { toolBudget: input.params.toolBudget } : {}),
1620
1735
  capabilityCeiling: intersectSubagentCapabilityCeilings("capabilityCeiling" in target ? target.capabilityCeiling : undefined, recoveryDescriptor?.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(input.deps.state.currentSessionId)),
1736
+ runFanoutBudget: recoveryDescriptor?.runFanoutBudget ?? createRunFanoutBudget(runId, resolveMaxSubagentSpawnsPerRun(input.deps.config.maxSubagentSpawnsPerRun)),
1621
1737
  parentWorkflowRunId: input.params.workflowParentRunId,
1622
1738
  workflowKey: input.params.workflowKey,
1739
+ activeAsyncCapacity,
1623
1740
  }));
1624
- if (result.isError) return result;
1741
+ if (result.isError) {
1742
+ const startedStatus = readStatus(revivalAsyncDir);
1743
+ if (input.params.workflowParentRunId !== undefined && startedStatus?.runId === runId && startedStatus.processTerminal?.runnerProcessInstanceId) {
1744
+ return {
1745
+ ...result,
1746
+ details: {
1747
+ ...result.details,
1748
+ runId,
1749
+ asyncId: runId,
1750
+ asyncDir: revivalAsyncDir,
1751
+ ...(target.launchContractDigest ? { sourceLaunchContractDigest: target.launchContractDigest } : {}),
1752
+ },
1753
+ };
1754
+ }
1755
+ activeAsyncCapacity?.rollback();
1756
+ return result;
1757
+ }
1625
1758
  for (const brief of queuedBriefs) fs.rmSync(brief.path, { force: true });
1626
1759
  if (queuedBriefs.length > 0 && sourceAsyncDir) {
1627
1760
  const sourceStatus = readStatus(sourceAsyncDir);
@@ -2117,6 +2250,27 @@ export const DEFAULT_FOREGROUND_TIMEOUT_MS = 30 * 60 * 1000;
2117
2250
  // stay unbounded as a whole, while each runner child has its own deadline.
2118
2251
  export { DEFAULT_ASYNC_TIMEOUT_MS };
2119
2252
 
2253
+ /**
2254
+ * Maximum delay a Node.js timer accepts. Values above the 32-bit signed integer
2255
+ * ceiling overflow `setTimeout`, which silently clamps the delay to ~1ms and
2256
+ * fires almost immediately — so a run configured with a larger deadline would
2257
+ * terminate right away while reporting the long duration. Any timeout destined
2258
+ * for a timer must stay within this bound.
2259
+ */
2260
+ const MAX_TIMER_DELAY_MS = 2_147_483_647;
2261
+
2262
+ /**
2263
+ * Resolve the optional global default runtime deadline from extension config
2264
+ * (`config.timeoutMs`). Returns undefined for unset or invalid values so callers
2265
+ * fall back to the built-in defaults. "Invalid" covers non-positive-integer
2266
+ * values and values above `MAX_TIMER_DELAY_MS`; the latter would overflow the
2267
+ * Node.js timer and expire the run almost immediately instead of running long.
2268
+ */
2269
+ export function resolveConfigDefaultTimeoutMs(raw: unknown): number | undefined {
2270
+ if (typeof raw !== "number" || !Number.isInteger(raw) || raw <= 0 || raw > MAX_TIMER_DELAY_MS) return undefined;
2271
+ return raw;
2272
+ }
2273
+
2120
2274
  export function resolveForegroundTimeout(params: SubagentParamsLike, defaultTimeoutMs?: number): { timeoutMs?: number; error?: string } {
2121
2275
  const rawTimeout = params.timeoutMs;
2122
2276
  const rawMaxRuntime = params.maxRuntimeMs;
@@ -2140,14 +2294,18 @@ export function resolveForegroundTimeout(params: SubagentParamsLike, defaultTime
2140
2294
  * Resolve the effective launch timeout for a single-agent run, applying the
2141
2295
  * async/foreground default when neither the caller nor the agent set one.
2142
2296
  *
2143
- * The async default is deliberately applied only to plain single-agent
2144
- * launches. Composite launches keep their top-level execution unbounded when
2145
- * no timeout is set; their runner children resolve separate deadlines.
2146
- * Exported so the executor wiring is directly testable.
2297
+ * A global config default (`config.timeoutMs`, passed as `configDefaultTimeoutMs`)
2298
+ * replaces the built-in 30-minute backstop wherever a concrete default is applied.
2299
+ * The async default is deliberately applied only to plain single-agent launches.
2300
+ * Composite launches keep their top-level execution unbounded when no timeout is
2301
+ * set — even with a config default — while their runner children resolve separate
2302
+ * deadlines. Exported so the executor wiring is directly testable.
2147
2303
  */
2148
- export function resolveSingleAgentLaunchTimeout(params: SubagentParamsLike, async: boolean): { timeoutMs?: number; error?: string } {
2304
+ export function resolveSingleAgentLaunchTimeout(params: SubagentParamsLike, async: boolean, configDefaultTimeoutMs?: number): { timeoutMs?: number; error?: string } {
2149
2305
  const isComposite = (params.chain?.length ?? 0) > 0 || (params.tasks?.length ?? 0) > 0 || params.workflowScript !== undefined;
2150
- const defaultTimeoutMs = !async ? DEFAULT_FOREGROUND_TIMEOUT_MS : isComposite ? undefined : DEFAULT_ASYNC_TIMEOUT_MS;
2306
+ const foregroundDefault = configDefaultTimeoutMs ?? DEFAULT_FOREGROUND_TIMEOUT_MS;
2307
+ const asyncSingleDefault = configDefaultTimeoutMs ?? DEFAULT_ASYNC_TIMEOUT_MS;
2308
+ const defaultTimeoutMs = !async ? foregroundDefault : isComposite ? undefined : asyncSingleDefault;
2151
2309
  return resolveForegroundTimeout(params, defaultTimeoutMs);
2152
2310
  }
2153
2311
 
@@ -2560,8 +2718,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2560
2718
  details: { mode: "single" as const, results: [] },
2561
2719
  };
2562
2720
  }
2563
- const requestedWorkflowChildAsyncId = typeof params.workflowChildAsyncId === "string" ? params.workflowChildAsyncId.trim() : "";
2564
- const id = requestedWorkflowChildAsyncId && path.basename(requestedWorkflowChildAsyncId) === requestedWorkflowChildAsyncId ? requestedWorkflowChildAsyncId : randomUUID();
2721
+ const id = data.asyncRunId;
2565
2722
  const parentModel = data.parentModel;
2566
2723
  const asyncCtx = compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
2567
2724
  pi: deps.pi,
@@ -2615,6 +2772,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2615
2772
  shareEnabled,
2616
2773
  sessionRoot,
2617
2774
  chainSkills: [],
2775
+ activeAsyncCapacity: data.activeAsyncCapacity,
2618
2776
  sessionFilesByFlatIndex: params.tasks.map((task, index) => sessionFileForTask(task.agent, index, task.model)),
2619
2777
  thinkingOverridesByFlatIndex: params.tasks.map((task, index) => thinkingOverrideForTask(task.agent, index, task.model)),
2620
2778
  contextForAgent: contextPolicy.contextForAgent,
@@ -2634,6 +2792,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2634
2792
  usageBudget: data.usageBudget,
2635
2793
  configToolBudget: data.configToolBudget,
2636
2794
  capabilityCeiling: data.capabilityCeiling,
2795
+ runFanoutBudget: data.runFanoutBudget,
2637
2796
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2638
2797
  parentWorkflowRunId: params.workflowParentRunId,
2639
2798
  workflowKey: params.workflowKey,
@@ -2659,6 +2818,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2659
2818
  shareEnabled,
2660
2819
  sessionRoot,
2661
2820
  chainSkills,
2821
+ activeAsyncCapacity: data.activeAsyncCapacity,
2662
2822
  sessionFilesByFlatIndex: collectChainSessionFiles(chain, sessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems),
2663
2823
  thinkingOverridesByFlatIndex: collectChainThinkingOverrides(chain, thinkingOverrideForTask, deps.config.chain?.dynamicFanout?.maxItems),
2664
2824
  contextForAgent: contextPolicy.contextForAgent,
@@ -2679,6 +2839,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2679
2839
  usageBudget: data.usageBudget,
2680
2840
  configToolBudget: data.configToolBudget,
2681
2841
  capabilityCeiling: data.capabilityCeiling,
2842
+ runFanoutBudget: data.runFanoutBudget,
2682
2843
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2683
2844
  parentWorkflowRunId: params.workflowParentRunId,
2684
2845
  workflowKey: params.workflowKey,
@@ -2700,7 +2861,12 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2700
2861
  const normalizedSkills = normalizeSkillInput(params.skill);
2701
2862
  const skills = normalizedSkills === false ? [] : normalizedSkills;
2702
2863
  const maxSubagentDepth = resolveChildMaxSubagentDepth(currentMaxSubagentDepth, a.maxSubagentDepth);
2703
- const modelOverride = resolveEffectiveSubagentModel(params.model as string | undefined, a.model, parentModel, availableModels, currentProvider, data.modelScope === undefined ? {} : { scope: data.modelScope });
2864
+ const externalRunnerWithoutExplicitModel = a.runner?.type === "external-cli"
2865
+ && params.model === undefined
2866
+ && (a.model === undefined || (a.modelSource?.type === "subagents.defaultModel" && a.model === a.modelSource.model));
2867
+ const modelOverride = externalRunnerWithoutExplicitModel
2868
+ ? undefined
2869
+ : resolveEffectiveSubagentModel(params.model as string | undefined, a.model, parentModel, availableModels, currentProvider, data.modelScope === undefined ? {} : { scope: data.modelScope });
2704
2870
  return executeAsyncSingle(id, compactOptional<Parameters<typeof executeAsyncSingle>[1]>({
2705
2871
  agent: params.agent!,
2706
2872
  task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(params.task ?? "") : (params.task ?? ""),
@@ -2713,6 +2879,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2713
2879
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
2714
2880
  artifactConfig,
2715
2881
  shareEnabled,
2882
+ activeAsyncCapacity: data.activeAsyncCapacity,
2716
2883
  sessionRoot,
2717
2884
  sessionFile: sessionFileForTask(params.agent!, 0, modelOverride),
2718
2885
  context: contextPolicy.contextForAgent(params.agent!),
@@ -2722,7 +2889,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2722
2889
  ...(params.reads !== undefined ? { reads: params.reads } : {}),
2723
2890
  outputBaseDir: resolveSingleRunOutputBaseDir(deps, artifactsDir, id),
2724
2891
  modelOverride,
2725
- thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride),
2892
+ thinkingOverride: externalRunnerWithoutExplicitModel ? undefined : thinkingOverrideForTask(params.agent!, 0, modelOverride),
2726
2893
  maxSubagentDepth,
2727
2894
  waitToolEnabled: deps.waitToolEnabled,
2728
2895
  worktreeSetupHook: deps.config.worktreeSetupHook,
@@ -2741,6 +2908,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2741
2908
  usageBudget: data.usageBudget,
2742
2909
  configToolBudget: data.configToolBudget,
2743
2910
  capabilityCeiling: data.capabilityCeiling,
2911
+ runFanoutBudget: data.runFanoutBudget,
2744
2912
  parentWorkflowRunId: params.workflowParentRunId,
2745
2913
  workflowKey: params.workflowKey,
2746
2914
  }));
@@ -2831,6 +2999,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2831
2999
  permissions: deps.config.permissions,
2832
3000
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2833
3001
  capabilityCeiling: data.capabilityCeiling,
3002
+ runFanoutBudget: data.runFanoutBudget,
2834
3003
  }));
2835
3004
 
2836
3005
  if (chainResult.requestedAsync) {
@@ -2841,7 +3010,9 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2841
3010
  details: { mode: "chain" as const, results: [] },
2842
3011
  };
2843
3012
  }
2844
- const id = randomUUID();
3013
+ const id = data.asyncRunId;
3014
+ const capacity = acquireClarifiedBackgroundCapacity(data, deps, "chain", id);
3015
+ if (capacity.errorResult) return capacity.errorResult;
2845
3016
  const parentModel = data.parentModel;
2846
3017
  const asyncCtx = compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
2847
3018
  pi: deps.pi,
@@ -2857,42 +3028,48 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2857
3028
  const rawAsyncChain = chainResult.requestedAsync.chain;
2858
3029
  const asyncChain = wrapChainTasksForFork(rawAsyncChain, contextPolicy);
2859
3030
  const firstAgent = firstChainAgent(rawAsyncChain);
2860
- return executeAsyncChain(id, compactOptional<Parameters<typeof executeAsyncChain>[1]>({
2861
- chain: asyncChain,
2862
- task: params.task,
2863
- goal: resolveAsyncEventGoal(params.task, rawAsyncChain, firstAgent ? shouldForkAgent(contextPolicy, firstAgent) : false),
2864
- agents,
2865
- ctx: asyncCtx,
2866
- availableModels: ctx.modelRegistry.getAvailable().map(toModelInfo),
2867
- cwd: effectiveCwd,
2868
- maxOutput: params.maxOutput,
2869
- artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
2870
- artifactConfig,
2871
- shareEnabled,
2872
- sessionRoot,
2873
- chainSkills: chainResult.requestedAsync.chainSkills,
2874
- sessionFilesByFlatIndex: collectChainSessionFiles(asyncChain, sessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems),
2875
- thinkingOverridesByFlatIndex: collectChainThinkingOverrides(asyncChain, thinkingOverrideForTask, deps.config.chain?.dynamicFanout?.maxItems),
2876
- contextForAgent: contextPolicy.contextForAgent,
2877
- dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
2878
- maxSubagentDepth: currentMaxSubagentDepth,
2879
- waitToolEnabled: deps.waitToolEnabled,
2880
- worktreeSetupHook: deps.config.worktreeSetupHook,
2881
- worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
2882
- worktreeBaseDir: deps.config.worktreeBaseDir,
2883
- controlConfig,
2884
- agentContract: params.agentContract,
2885
- controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
2886
- childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
2887
- nestedRoute: data.nestedRoute,
2888
- timeoutMs: data.timeoutMs,
2889
- turnBudget: data.turnBudget,
2890
- toolBudget: data.toolBudget,
2891
- usageBudget: data.usageBudget,
2892
- configToolBudget: data.configToolBudget,
2893
- capabilityCeiling: data.capabilityCeiling,
2894
- globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2895
- }));
3031
+ try {
3032
+ return executeAsyncChain(id, compactOptional<Parameters<typeof executeAsyncChain>[1]>({
3033
+ chain: asyncChain,
3034
+ task: params.task,
3035
+ goal: resolveAsyncEventGoal(params.task, rawAsyncChain, firstAgent ? shouldForkAgent(contextPolicy, firstAgent) : false),
3036
+ agents,
3037
+ ctx: asyncCtx,
3038
+ availableModels: ctx.modelRegistry.getAvailable().map(toModelInfo),
3039
+ cwd: effectiveCwd,
3040
+ maxOutput: params.maxOutput,
3041
+ artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
3042
+ artifactConfig,
3043
+ shareEnabled,
3044
+ sessionRoot,
3045
+ chainSkills: chainResult.requestedAsync.chainSkills,
3046
+ activeAsyncCapacity: capacity.activeAsyncCapacity,
3047
+ sessionFilesByFlatIndex: collectChainSessionFiles(asyncChain, sessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems),
3048
+ thinkingOverridesByFlatIndex: collectChainThinkingOverrides(asyncChain, thinkingOverrideForTask, deps.config.chain?.dynamicFanout?.maxItems),
3049
+ contextForAgent: contextPolicy.contextForAgent,
3050
+ dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
3051
+ maxSubagentDepth: currentMaxSubagentDepth,
3052
+ waitToolEnabled: deps.waitToolEnabled,
3053
+ worktreeSetupHook: deps.config.worktreeSetupHook,
3054
+ worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
3055
+ worktreeBaseDir: deps.config.worktreeBaseDir,
3056
+ controlConfig,
3057
+ agentContract: params.agentContract,
3058
+ controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
3059
+ childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
3060
+ nestedRoute: data.nestedRoute,
3061
+ timeoutMs: data.timeoutMs,
3062
+ turnBudget: data.turnBudget,
3063
+ toolBudget: data.toolBudget,
3064
+ usageBudget: data.usageBudget,
3065
+ configToolBudget: data.configToolBudget,
3066
+ capabilityCeiling: data.capabilityCeiling,
3067
+ runFanoutBudget: data.runFanoutBudget,
3068
+ globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
3069
+ }));
3070
+ } finally {
3071
+ finalizeClarifiedBackgroundCapacity(data, deps, capacity.activeAsyncCapacity);
3072
+ }
2896
3073
  }
2897
3074
 
2898
3075
  const rawChainDetails = chainResult.details ? compactOptional<Details>({ ...chainResult.details, runId, timeoutMs: data.timeoutMs }) : undefined;
@@ -2937,6 +3114,7 @@ interface ForegroundParallelRunInput {
2937
3114
  intercomEvents: IntercomEventBus;
2938
3115
  parentSessionId: string | null;
2939
3116
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
3117
+ runFanoutBudget: RunFanoutBudgetDescriptor;
2940
3118
  signal: AbortSignal;
2941
3119
  runId: string;
2942
3120
  sessionDirForIndex: (idx?: number) => string | undefined;
@@ -2986,6 +3164,48 @@ function buildParallelModeError(message: string): AgentToolResult<Details> {
2986
3164
  };
2987
3165
  }
2988
3166
 
3167
+ function acquireClarifiedBackgroundCapacity(
3168
+ data: ExecutionContextData,
3169
+ deps: ExecutorDeps,
3170
+ mode: "single" | "parallel" | "chain",
3171
+ asyncRunId: string,
3172
+ ): { activeAsyncCapacity?: ActiveAsyncCapacityHandle; errorResult?: AgentToolResult<Details> } {
3173
+ if (!data.topLevelAsyncCapacityEligible || !data.parentSessionId) return {};
3174
+ const liveWorkflowRunIds = new Set(deps.state.workflowControllers?.keys() ?? []);
3175
+ const activeLimit = resolveMaxActiveAsyncRunsPerSession(deps.config.maxActiveAsyncRunsPerSession);
3176
+ try {
3177
+ const activeAsyncCapacity = acquireActiveAsyncCapacity({
3178
+ sessionId: data.parentSessionId,
3179
+ limit: activeLimit,
3180
+ runId: asyncRunId,
3181
+ kind: "runner",
3182
+ asyncDir: path.join(DIRS.async, asyncRunId),
3183
+ }, { liveWorkflowRunIds });
3184
+ deps.state.activeAsyncCapacity = getActiveAsyncCapacitySnapshot(data.parentSessionId, activeLimit, { liveWorkflowRunIds });
3185
+ return { activeAsyncCapacity };
3186
+ } catch (error) {
3187
+ if (error instanceof ActiveAsyncCapacityError) {
3188
+ deps.state.activeAsyncCapacity = error.snapshot;
3189
+ return { errorResult: { content: [{ type: "text", text: error.message }], isError: true, details: { mode, results: [], activeAsyncCapacity: error.snapshot } } };
3190
+ }
3191
+ throw error;
3192
+ }
3193
+ }
3194
+
3195
+ function finalizeClarifiedBackgroundCapacity(
3196
+ data: ExecutionContextData,
3197
+ deps: ExecutorDeps,
3198
+ activeAsyncCapacity: ActiveAsyncCapacityHandle | undefined,
3199
+ ): void {
3200
+ if (!activeAsyncCapacity || !data.parentSessionId) return;
3201
+ if (!activeAsyncCapacity.owner.runnerStartedAt) activeAsyncCapacity.rollback();
3202
+ deps.state.activeAsyncCapacity = getActiveAsyncCapacitySnapshot(
3203
+ data.parentSessionId,
3204
+ resolveMaxActiveAsyncRunsPerSession(deps.config.maxActiveAsyncRunsPerSession),
3205
+ { liveWorkflowRunIds: new Set(deps.state.workflowControllers?.keys() ?? []) },
3206
+ );
3207
+ }
3208
+
2989
3209
  function createParallelWorktreeSetup(
2990
3210
  enabled: boolean | undefined,
2991
3211
  cwd: string,
@@ -3174,7 +3394,33 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
3174
3394
  beginForegroundChild(input.foregroundControl, {
3175
3395
  index,
3176
3396
  agent: task.agent,
3177
- ...(input.taskDescriptions[index] === undefined ? {} : { description: input.taskDescriptions[index] }),
3397
+ authoredTask: input.taskDescriptions[index] ?? "",
3398
+ effectivePrompt: taskText,
3399
+ cwd: taskCwd,
3400
+ ...(outputPath ? { outputPath } : {}),
3401
+ rerun: { params: compactOptional<Record<string, unknown>>({
3402
+ agent: task.agent,
3403
+ task: input.taskDescriptions[index] ?? "",
3404
+ async: false,
3405
+ cwd: input.worktreeSetup ? undefined : taskCwd,
3406
+ worktree: input.worktreeSetup ? true : false,
3407
+ context: input.contextPolicy.contextForAgent(task.agent),
3408
+ share: input.shareEnabled || undefined,
3409
+ maxOutput: input.maxOutput,
3410
+ timeoutMs: input.timeoutMs,
3411
+ turnBudget: input.turnBudget,
3412
+ ...(model ? { model } : {}),
3413
+ ...(effectiveSkills !== undefined ? { skill: effectiveSkills } : {}),
3414
+ ...(outputPath ? { output: outputPath } : {}),
3415
+ ...(behavior?.outputMode !== undefined ? { outputMode: behavior.outputMode } : {}),
3416
+ ...(behavior?.reads !== undefined ? { reads: behavior.reads } : {}),
3417
+ ...(behavior?.progress !== undefined ? { progress: behavior.progress } : {}),
3418
+ ...(task.toolBudget !== undefined ? { toolBudget: task.toolBudget } : {}),
3419
+ ...(task.outputSchema !== undefined ? { outputSchema: task.outputSchema } : {}),
3420
+ ...(task.acceptance !== undefined ? { acceptance: task.acceptance } : {}),
3421
+ ...(task.agentContract !== undefined || input.agentContract !== undefined ? { agentContract: task.agentContract ?? input.agentContract } : {}),
3422
+ }) },
3423
+ description: `${task.agent} child`,
3178
3424
  ...(model ? { model } : {}),
3179
3425
  ...(thinking ? { thinking } : {}),
3180
3426
  interrupt: () => {
@@ -3191,8 +3437,10 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
3191
3437
  const result = await runSync(input.ctx.cwd, input.agents, task.agent, taskText, compactOptional<Parameters<typeof runSync>[4]>({
3192
3438
  permissions: input.permissions,
3193
3439
  parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
3440
+ llmIntentArbiter: createTaskMutationArbiter(input.ctx),
3194
3441
  ...workflowForegroundSteeringLaunchOptions(input.foregroundControl, index),
3195
3442
  context: input.contextPolicy.contextForAgent(task.agent),
3443
+ runFanoutBudget: { ...input.runFanoutBudget, parentPath: `${input.runFanoutBudget.parentPath ? `${input.runFanoutBudget.parentPath}/` : ""}tasks[${index}]` },
3196
3444
  cwd: taskCwd,
3197
3445
  signal: input.signal,
3198
3446
  interruptSignal: interruptController.signal,
@@ -3237,6 +3485,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
3237
3485
  agentContract: task.agentContract ?? input.agentContract,
3238
3486
  acceptance: task.acceptance,
3239
3487
  acceptanceContext: { mode: "parallel" },
3488
+ onEffectivePrompt: input.foregroundControl ? (prompt) => updateLiveEffectivePrompt(input.foregroundControl!, index, prompt) : undefined,
3240
3489
  timeoutMs: input.timeoutMs,
3241
3490
  deadlineAt: input.deadlineAt,
3242
3491
  turnBudget: input.turnBudget,
@@ -3420,7 +3669,9 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3420
3669
  details: { mode: "parallel" as const, results: [] },
3421
3670
  };
3422
3671
  }
3423
- const id = randomUUID();
3672
+ const id = data.asyncRunId;
3673
+ const capacity = acquireClarifiedBackgroundCapacity(data, deps, "parallel", id);
3674
+ if (capacity.errorResult) return capacity.errorResult;
3424
3675
  const asyncCtx = compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
3425
3676
  pi: deps.pi,
3426
3677
  cwd: ctx.cwd,
@@ -3451,40 +3702,46 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3451
3702
  ...(t.agentContract !== undefined ? { agentContract: t.agentContract } : {}),
3452
3703
  });
3453
3704
  });
3454
- return executeAsyncChain(id, compactOptional<Parameters<typeof executeAsyncChain>[1]>({
3455
- chain: [compactOptional<ParallelStep>({ parallel: parallelTasks, concurrency: parallelConcurrency, worktree: params.worktree })],
3456
- resultMode: "parallel",
3457
- goal: taskTexts[0] ?? "",
3458
- agents,
3459
- ctx: asyncCtx,
3460
- availableModels,
3461
- cwd: effectiveCwd,
3462
- maxOutput: params.maxOutput,
3463
- artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
3464
- artifactConfig,
3465
- shareEnabled,
3466
- sessionRoot,
3467
- chainSkills: [],
3468
- sessionFilesByFlatIndex: tasks.map((task, index) => sessionFileForTask(task.agent, index, modelOverrides[index])),
3469
- thinkingOverridesByFlatIndex: tasks.map((task, index) => thinkingOverrideForTask(task.agent, index, modelOverrides[index])),
3470
- contextForAgent: contextPolicy.contextForAgent,
3471
- maxSubagentDepth: currentMaxSubagentDepth,
3472
- waitToolEnabled: deps.waitToolEnabled,
3473
- worktreeSetupHook: deps.config.worktreeSetupHook,
3474
- worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
3475
- worktreeBaseDir: deps.config.worktreeBaseDir,
3476
- controlConfig,
3477
- agentContract: params.agentContract,
3478
- controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
3479
- childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
3480
- nestedRoute: data.nestedRoute,
3481
- timeoutMs: data.timeoutMs,
3482
- turnBudget: data.turnBudget,
3483
- usageBudget: data.usageBudget,
3484
- toolBudget: data.toolBudget,
3485
- configToolBudget: data.configToolBudget,
3486
- globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
3487
- }));
3705
+ try {
3706
+ return executeAsyncChain(id, compactOptional<Parameters<typeof executeAsyncChain>[1]>({
3707
+ chain: [compactOptional<ParallelStep>({ parallel: parallelTasks, concurrency: parallelConcurrency, worktree: params.worktree })],
3708
+ resultMode: "parallel",
3709
+ goal: taskTexts[0] ?? "",
3710
+ agents,
3711
+ ctx: asyncCtx,
3712
+ availableModels,
3713
+ cwd: effectiveCwd,
3714
+ maxOutput: params.maxOutput,
3715
+ artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
3716
+ artifactConfig,
3717
+ shareEnabled,
3718
+ sessionRoot,
3719
+ chainSkills: [],
3720
+ activeAsyncCapacity: capacity.activeAsyncCapacity,
3721
+ sessionFilesByFlatIndex: tasks.map((task, index) => sessionFileForTask(task.agent, index, modelOverrides[index])),
3722
+ thinkingOverridesByFlatIndex: tasks.map((task, index) => thinkingOverrideForTask(task.agent, index, modelOverrides[index])),
3723
+ contextForAgent: contextPolicy.contextForAgent,
3724
+ maxSubagentDepth: currentMaxSubagentDepth,
3725
+ waitToolEnabled: deps.waitToolEnabled,
3726
+ worktreeSetupHook: deps.config.worktreeSetupHook,
3727
+ worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
3728
+ worktreeBaseDir: deps.config.worktreeBaseDir,
3729
+ controlConfig,
3730
+ agentContract: params.agentContract,
3731
+ controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
3732
+ childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
3733
+ nestedRoute: data.nestedRoute,
3734
+ timeoutMs: data.timeoutMs,
3735
+ turnBudget: data.turnBudget,
3736
+ usageBudget: data.usageBudget,
3737
+ toolBudget: data.toolBudget,
3738
+ configToolBudget: data.configToolBudget,
3739
+ globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
3740
+ runFanoutBudget: data.runFanoutBudget,
3741
+ }));
3742
+ } finally {
3743
+ finalizeClarifiedBackgroundCapacity(data, deps, capacity.activeAsyncCapacity);
3744
+ }
3488
3745
  }
3489
3746
  }
3490
3747
 
@@ -3563,6 +3820,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3563
3820
  intercomEvents: deps.pi.events,
3564
3821
  parentSessionId: data.parentSessionId,
3565
3822
  capabilityCeiling: data.capabilityCeiling,
3823
+ runFanoutBudget: data.runFanoutBudget,
3566
3824
  signal,
3567
3825
  runId,
3568
3826
  sessionDirForIndex,
@@ -3797,7 +4055,9 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3797
4055
  details: { mode: "single" as const, results: [] },
3798
4056
  };
3799
4057
  }
3800
- const id = randomUUID();
4058
+ const id = data.asyncRunId;
4059
+ const capacity = acquireClarifiedBackgroundCapacity(data, deps, "single", id);
4060
+ if (capacity.errorResult) return capacity.errorResult;
3801
4061
  const asyncCtx = compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
3802
4062
  pi: deps.pi,
3803
4063
  cwd: ctx.cwd,
@@ -3809,49 +4069,56 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3809
4069
  interactive: ctx.hasUI,
3810
4070
  permissions: deps.config.permissions,
3811
4071
  });
3812
- return executeAsyncSingle(id, compactOptional<Parameters<typeof executeAsyncSingle>[1]>({
3813
- agent: params.agent!,
3814
- task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(task) : task,
3815
- goal: task,
3816
- agentConfig,
3817
- ctx: asyncCtx,
3818
- availableModels,
3819
- cwd: effectiveCwd,
3820
- maxOutput: params.maxOutput,
3821
- artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
3822
- artifactConfig,
3823
- shareEnabled,
3824
- sessionRoot,
3825
- sessionFile: sessionFileForTask(params.agent!, 0, modelOverride),
3826
- context: contextPolicy.contextForAgent(params.agent!),
3827
- skills: skillOverride === false ? [] : skillOverride,
3828
- output: effectiveOutput,
3829
- outputMode: effectiveOutputMode,
3830
- ...(readsOverride !== undefined ? { reads: readsOverride } : {}),
3831
- outputBaseDir: resolveSingleRunOutputBaseDir(deps, artifactsDir, id),
3832
- modelOverride,
3833
- thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride),
3834
- maxSubagentDepth,
3835
- waitToolEnabled: deps.waitToolEnabled,
3836
- worktreeSetupHook: deps.config.worktreeSetupHook,
3837
- worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
3838
- worktreeBaseDir: deps.config.worktreeBaseDir,
3839
- controlConfig,
3840
- controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
3841
- childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
3842
- nestedRoute: data.nestedRoute,
3843
- agentContract: params.agentContract,
3844
- structuredOutputSchema: params.outputSchema,
3845
- acceptance: params.acceptance,
3846
- timeoutMs: data.timeoutMs,
3847
- turnBudget: data.turnBudget,
3848
- toolBudget: effectiveToolBudget.toolBudget,
3849
- usageBudget: data.usageBudget,
3850
- allowZeroToolBudget: data.allowZeroToolBudget && effectiveToolBudget.toolBudget === data.toolBudget,
3851
- }));
4072
+ try {
4073
+ return executeAsyncSingle(id, compactOptional<Parameters<typeof executeAsyncSingle>[1]>({
4074
+ agent: params.agent!,
4075
+ task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(task) : task,
4076
+ goal: task,
4077
+ agentConfig,
4078
+ ctx: asyncCtx,
4079
+ availableModels,
4080
+ cwd: effectiveCwd,
4081
+ maxOutput: params.maxOutput,
4082
+ artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
4083
+ artifactConfig,
4084
+ shareEnabled,
4085
+ activeAsyncCapacity: capacity.activeAsyncCapacity,
4086
+ sessionRoot,
4087
+ sessionFile: sessionFileForTask(params.agent!, 0, modelOverride),
4088
+ context: contextPolicy.contextForAgent(params.agent!),
4089
+ skills: skillOverride === false ? [] : skillOverride,
4090
+ output: effectiveOutput,
4091
+ outputMode: effectiveOutputMode,
4092
+ ...(readsOverride !== undefined ? { reads: readsOverride } : {}),
4093
+ outputBaseDir: resolveSingleRunOutputBaseDir(deps, artifactsDir, id),
4094
+ modelOverride,
4095
+ thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride),
4096
+ maxSubagentDepth,
4097
+ waitToolEnabled: deps.waitToolEnabled,
4098
+ worktreeSetupHook: deps.config.worktreeSetupHook,
4099
+ worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
4100
+ worktreeBaseDir: deps.config.worktreeBaseDir,
4101
+ controlConfig,
4102
+ controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
4103
+ childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
4104
+ nestedRoute: data.nestedRoute,
4105
+ agentContract: params.agentContract,
4106
+ structuredOutputSchema: params.outputSchema,
4107
+ acceptance: params.acceptance,
4108
+ timeoutMs: data.timeoutMs,
4109
+ turnBudget: data.turnBudget,
4110
+ toolBudget: effectiveToolBudget.toolBudget,
4111
+ usageBudget: data.usageBudget,
4112
+ allowZeroToolBudget: data.allowZeroToolBudget && effectiveToolBudget.toolBudget === data.toolBudget,
4113
+ runFanoutBudget: data.runFanoutBudget,
4114
+ }));
4115
+ } finally {
4116
+ finalizeClarifiedBackgroundCapacity(data, deps, capacity.activeAsyncCapacity);
4117
+ }
3852
4118
  }
3853
4119
  }
3854
4120
 
4121
+ const authoredTask = task;
3855
4122
  if (shouldForkAgent(contextPolicy, params.agent!)) {
3856
4123
  task = wrapForkTask(task);
3857
4124
  }
@@ -3888,6 +4155,11 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3888
4155
  beginForegroundChild(foregroundControl, omitUndefinedProperties({
3889
4156
  index: 0,
3890
4157
  agent: params.agent!,
4158
+ authoredTask,
4159
+ effectivePrompt: task,
4160
+ cwd: effectiveCwd,
4161
+ outputPath,
4162
+ rerun: { params: { ...params, task: authoredTask, async: params.async ?? false } },
3891
4163
  description: foregroundControl.description,
3892
4164
  ...(modelOverride ? { model: modelOverride } : {}),
3893
4165
  ...(thinking ? { thinking } : {}),
@@ -3913,8 +4185,10 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3913
4185
  r = await runSync(ctx.cwd, agents, params.agent!, task, compactOptional<Parameters<typeof runSync>[4]>({
3914
4186
  permissions: deps.config.permissions,
3915
4187
  parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
4188
+ llmIntentArbiter: createTaskMutationArbiter(ctx),
3916
4189
  ...workflowForegroundSteeringLaunchOptions(foregroundControl, 0),
3917
4190
  context: data.contextPolicy.contextForAgent(params.agent!),
4191
+ runFanoutBudget: params.runFanoutAdmitted ? data.runFanoutBudget : { ...data.runFanoutBudget, parentPath: `${data.runFanoutBudget.parentPath ? `${data.runFanoutBudget.parentPath}/` : ""}single` },
3918
4192
  cwd: effectiveCwd,
3919
4193
  signal,
3920
4194
  interruptSignal: interruptController.signal,
@@ -3948,6 +4222,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3948
4222
  agentContract: params.agentContract,
3949
4223
  acceptance: params.acceptance,
3950
4224
  acceptanceContext: { mode: "single" },
4225
+ onEffectivePrompt: foregroundControl ? (prompt) => updateLiveEffectivePrompt(foregroundControl, 0, prompt) : undefined,
3951
4226
  onDetachReady: (detach) => {
3952
4227
  detachForeground = detach;
3953
4228
  },
@@ -3996,12 +4271,14 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3996
4271
  if (r.artifactPaths) allArtifactPaths.push(r.artifactPaths);
3997
4272
 
3998
4273
  const fullOutput = getSingleResultOutput(r);
4274
+ const preserveRejectedSavedOutput = r.acceptance?.explicit && r.acceptance.status === "rejected" && r.savedOutputPath !== undefined;
3999
4275
  const finalizedOutput = finalizeSingleOutput(omitUndefinedProperties({
4000
4276
  fullOutput,
4001
4277
  truncatedOutput: r.truncation?.text,
4002
4278
  outputPath,
4003
4279
  outputMode: r.outputMode,
4004
4280
  exitCode: r.exitCode,
4281
+ preserveSavedOutput: preserveRejectedSavedOutput,
4005
4282
  savedPath: r.savedOutputPath,
4006
4283
  outputReference: r.outputReference,
4007
4284
  saveError: r.outputSaveError,
@@ -4092,7 +4369,7 @@ function duplicateSubagentCallResult(params: SubagentParamsLike): AgentToolResul
4092
4369
  };
4093
4370
  }
4094
4371
 
4095
- const workflowLaunchObservers = new WeakMap<object, (launch: { agent: string; sessionFile?: string }) => void>();
4372
+ const workflowLaunchObservers = new WeakMap<object, (launch: { agent: string; sessionFile?: string; async: boolean; runId?: string }) => void>();
4096
4373
 
4097
4374
  function recordMissionWorkflowChild(
4098
4375
  binding: MissionLaunchBinding | undefined,
@@ -4101,8 +4378,9 @@ function recordMissionWorkflowChild(
4101
4378
  update: Omit<MissionWorkflowChildUpdate, "workflowRunId" | "key">,
4102
4379
  ): void {
4103
4380
  if (!binding) return;
4381
+ const { task: _task, ...durableUpdate } = update;
4104
4382
  try {
4105
- updateMission(binding.location, binding.missionId, { upsertWorkflowChildren: [{ workflowRunId, key, ...update }] });
4383
+ updateMission(binding.location, binding.missionId, { upsertWorkflowChildren: [{ workflowRunId, key, ...durableUpdate }] });
4106
4384
  } catch (error) {
4107
4385
  console.warn(`[pi-subagents] Failed to record mission workflow child '${key}': ${error instanceof Error ? error.message : String(error)}`);
4108
4386
  }
@@ -4184,7 +4462,7 @@ export function prepareWorkflowLaunchParams(
4184
4462
  childParams: Record<string, unknown>,
4185
4463
  parentWorkflowRunId: string,
4186
4464
  workflowKey: string,
4187
- options: { missionDetached?: boolean; suppressRoutineResultIntercom?: boolean } = {},
4465
+ options: { missionDetached?: boolean; suppressRoutineResultIntercom?: boolean; runFanoutBudget?: RunFanoutBudgetDescriptor } = {},
4188
4466
  ): SubagentParamsLike {
4189
4467
  if (typeof childParams.resume === "string") {
4190
4468
  if (childParams.gate !== undefined || workflowDefaults.gate !== undefined) {
@@ -4212,6 +4490,7 @@ export function prepareWorkflowLaunchParams(
4212
4490
  ...(options.missionDetached ? { mission: false } : {}),
4213
4491
  workflowParentRunId: parentWorkflowRunId,
4214
4492
  workflowKey,
4493
+ ...(options.runFanoutBudget ? { runFanoutBudget: { ...options.runFanoutBudget, parentPath: `${options.runFanoutBudget.parentPath ? `${options.runFanoutBudget.parentPath}/` : ""}workflow[${workflowKey}]` } } : {}),
4215
4494
  ...(options.suppressRoutineResultIntercom ? { suppressRoutineResultIntercom: true } : {}),
4216
4495
  } as SubagentParamsLike;
4217
4496
  const normalizedGate = normalizeGateParams(launchParams);
@@ -4359,6 +4638,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4359
4638
  } {
4360
4639
  const delegatedThinkingOverrides = new WeakMap<object, AgentConfig["thinking"]>();
4361
4640
  const delegatedZeroToolBudgets = new WeakSet<object>();
4641
+ const delegatedExecutions = new WeakSet<object>();
4362
4642
  const warnedArtifactPackageDirs = new Set<string>();
4363
4643
  const scheduledOwnerExecutors = new Map<string, ReturnType<typeof createSubagentExecutor>>();
4364
4644
  const execute = async (
@@ -4372,6 +4652,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4372
4652
  const workflowLaunchObserver = workflowLaunchObservers.get(params);
4373
4653
  const delegatedThinkingOverride = delegatedThinkingOverrides.get(params);
4374
4654
  const allowZeroToolBudget = delegatedZeroToolBudgets.has(params);
4655
+ const delegatedExecution = delegatedExecutions.has(params);
4375
4656
  if (!preserveActiveSession) deps.state.baseCwd = ctx.cwd;
4376
4657
  deps.state.foregroundRuns ??= new Map();
4377
4658
  deps.state.foregroundControls ??= new Map();
@@ -4382,7 +4663,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4382
4663
  const normalizedAction = typeof requestParams.action === "string" ? requestParams.action.trim() : requestParams.action;
4383
4664
  if (requestParams.workflowScript !== undefined && normalizedAction === undefined) {
4384
4665
  const parentCwd = ctx.cwd;
4385
- const timeout = requestParams.timeoutMs ?? requestParams.maxRuntimeMs ?? (requestParams.async === false ? DEFAULT_FOREGROUND_TIMEOUT_MS : undefined);
4666
+ const timeout = requestParams.timeoutMs ?? requestParams.maxRuntimeMs ?? (requestParams.async === false ? resolveConfigDefaultTimeoutMs(deps.config.timeoutMs) ?? DEFAULT_FOREGROUND_TIMEOUT_MS : undefined);
4386
4667
  const workflowUsageBudget = validateUsageBudgetConfig(requestParams.usageBudget ?? deps.config.usageBudget, requestParams.usageBudget ? "usageBudget" : "config.usageBudget");
4387
4668
  if (workflowUsageBudget.error) return buildRequestedModeError(requestParams, workflowUsageBudget.error);
4388
4669
  const workflowCwd = resolveRequestedCwd(parentCwd, requestParams.cwd);
@@ -4393,11 +4674,45 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4393
4674
  const explicitMission = requestParams.missionId !== undefined || requestParams.mission !== undefined;
4394
4675
  const autoMission = !explicitMission;
4395
4676
  const workflowPreview = autoMission ? previewSimpleWorkflowRun(requestParams.workflowScript) : undefined;
4396
- const previewTask = workflowPreview?.task?.trim() || undefined;
4397
4677
  const previewAgent = workflowPreview?.agent?.trim() || undefined;
4398
4678
  const scriptFirstLine = requestParams.workflowScript.split(/\r?\n/).map((line) => line.trim()).find(Boolean) || "Workflow";
4399
4679
  const boundedScriptPreview = scriptFirstLine.length > 100 ? `${scriptFirstLine.slice(0, 97)}...` : scriptFirstLine;
4400
- const derivedObjective = previewTask || (previewAgent ? `Workflow: ${previewAgent}` : boundedScriptPreview);
4680
+ const derivedObjective = previewAgent ? `Workflow: ${previewAgent}` : boundedScriptPreview;
4681
+ const workflowDepth = checkSubagentDepth(deps.config.maxSubagentDepth).depth;
4682
+ const asyncWorkflow = requestParams.async !== false;
4683
+ const topLevelAsyncWorkflow = asyncWorkflow
4684
+ && workflowDepth === 0
4685
+ && !resolveInheritedNestedRouteFromEnv()
4686
+ && !requestParams.workflowParentRunId;
4687
+ const workflowRunId = asyncWorkflow ? randomUUID() : undefined;
4688
+ let workflowCapacity: ActiveAsyncCapacityHandle | undefined;
4689
+ if (workflowRunId && topLevelAsyncWorkflow) {
4690
+ const currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
4691
+ try {
4692
+ workflowCapacity = acquireActiveAsyncCapacity({
4693
+ sessionId: currentSessionId,
4694
+ limit: resolveMaxActiveAsyncRunsPerSession(deps.config.maxActiveAsyncRunsPerSession),
4695
+ runId: workflowRunId,
4696
+ kind: "workflow",
4697
+ asyncDir: path.join(DIRS.async, workflowRunId),
4698
+ }, { liveWorkflowRunIds: new Set(deps.state.workflowControllers?.keys() ?? []) });
4699
+ } catch (error) {
4700
+ if (error instanceof ActiveAsyncCapacityError) {
4701
+ deps.state.activeAsyncCapacity = error.snapshot;
4702
+ return { content: [{ type: "text", text: error.message }], isError: true, details: { mode: "workflow", results: [], activeAsyncCapacity: error.snapshot } };
4703
+ }
4704
+ throw error;
4705
+ }
4706
+ }
4707
+ let workflowFanoutBudget: RunFanoutBudgetDescriptor;
4708
+ try {
4709
+ workflowFanoutBudget = requestParams.runFanoutBudget
4710
+ ?? decodeRunFanoutBudgetDescriptor(process.env[RUN_FANOUT_BUDGET_ENV])
4711
+ ?? createRunFanoutBudget(_id, resolveMaxSubagentSpawnsPerRun(deps.config.maxSubagentSpawnsPerRun));
4712
+ } catch (error) {
4713
+ workflowCapacity?.rollback();
4714
+ return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, details: { mode: "workflow", results: [] } };
4715
+ }
4401
4716
  let missionBinding: MissionLaunchBinding | undefined;
4402
4717
  let missionWarning: string | undefined;
4403
4718
  try {
@@ -4408,22 +4723,12 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4408
4723
  ownerSessionId: resolveCurrentSessionId(ctx.sessionManager),
4409
4724
  });
4410
4725
  } catch (error) {
4411
- if (explicitMission) return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, details: { mode: "workflow", results: [] } };
4726
+ if (explicitMission) {
4727
+ workflowCapacity?.rollback();
4728
+ return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, details: { mode: "workflow", results: [] } };
4729
+ }
4412
4730
  missionWarning = `Mission tracking unavailable: ${error instanceof Error ? error.message : String(error)}`;
4413
4731
  }
4414
- let shouldPatchMissionObjective = autoMission && previewTask === undefined && missionBinding !== undefined;
4415
- const patchMissionObjective = (task: unknown): void => {
4416
- if (!shouldPatchMissionObjective || !missionBinding || typeof task !== "string" || !task.trim()) return;
4417
- shouldPatchMissionObjective = false;
4418
- const objective = task.trim();
4419
- const firstLine = objective.split(/\r?\n/, 1)[0]?.trim() || objective;
4420
- const title = firstLine.length > 100 ? `${firstLine.slice(0, 97)}...` : firstLine;
4421
- try {
4422
- updateMission(missionBinding.location, missionBinding.missionId, { title, objective });
4423
- } catch (error) {
4424
- console.warn(`[pi-subagents] Failed to update automatic mission objective: ${error instanceof Error ? error.message : String(error)}`);
4425
- }
4426
- };
4427
4732
  const detachWorkflowChildMissions = autoMission || missionBinding !== undefined || requestParams.mission === false;
4428
4733
  const workflowState = missionBinding ? createMissionWorkflowState(missionBinding.location, missionBinding.missionId) : undefined;
4429
4734
  const attachWorkflowMission = (result: AgentToolResult<Details>): AgentToolResult<Details> => {
@@ -4437,20 +4742,27 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4437
4742
  : { ...result, details: { ...result.details, missionWarning: warning } };
4438
4743
  }
4439
4744
  };
4440
- if (requestParams.async !== false) {
4745
+ if (workflowRunId) {
4441
4746
  const toolCallId = _id;
4442
- const workflowRunId = randomUUID();
4443
4747
  const asyncDir = path.join(DIRS.async, workflowRunId);
4444
4748
  const resultPath = path.join(DIRS.results, `${workflowRunId}.json`);
4445
4749
  const statusPath = path.join(asyncDir, "status.json");
4446
4750
  const eventsPath = path.join(asyncDir, "events.jsonl");
4447
4751
  const startedAt = Date.now();
4448
4752
  const currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
4449
- fs.mkdirSync(asyncDir, { recursive: true });
4450
- fs.mkdirSync(DIRS.results, { recursive: true });
4753
+ try {
4754
+ fs.mkdirSync(asyncDir, { recursive: true });
4755
+ writeRunFanoutBudgetDescriptor(asyncDir, workflowFanoutBudget);
4756
+ fs.mkdirSync(DIRS.results, { recursive: true });
4757
+ } catch (error) {
4758
+ workflowCapacity?.rollback();
4759
+ return { content: [{ type: "text", text: `Failed to create async workflow storage: ${error instanceof Error ? error.message : String(error)}` }], isError: true, details: { mode: "workflow", results: [] } };
4760
+ }
4451
4761
  const controller = new AbortController();
4452
4762
  deps.state.workflowControllers ??= new Map();
4453
4763
  deps.state.workflowControllers.set(workflowRunId, controller);
4764
+ workflowCapacity?.markWorkflowStarted();
4765
+ if (workflowCapacity) deps.state.activeAsyncCapacity = getActiveAsyncCapacitySnapshot(currentSessionId, resolveMaxActiveAsyncRunsPerSession(deps.config.maxActiveAsyncRunsPerSession), { liveWorkflowRunIds: new Set(deps.state.workflowControllers.keys()) });
4454
4766
  let status: AsyncStatus = {
4455
4767
  runId: workflowRunId,
4456
4768
  toolCallId,
@@ -4464,6 +4776,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4464
4776
  pid: process.pid,
4465
4777
  steps: [],
4466
4778
  workflow: { trace: [], emits: [], console: [] },
4779
+ runFanoutBudget: getRunFanoutBudgetSnapshot(workflowFanoutBudget),
4467
4780
  };
4468
4781
  const appendWorkflowEvent = (event: Record<string, unknown>) => fs.appendFileSync(eventsPath, `${JSON.stringify({ ts: Date.now(), runId: workflowRunId, ...event })}\n`, "utf-8");
4469
4782
  let indexedState: AsyncStatus["state"] | undefined;
@@ -4550,12 +4863,23 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4550
4863
  if (existing) {
4551
4864
  existing.status = mapped;
4552
4865
  if (entry.agent) existing.agent = entry.agent;
4866
+ if (entry.runId) existing.runId = entry.runId;
4867
+ if (entry.state === "failed" && !entry.runId && existing.async === undefined) existing.async = false;
4553
4868
  if (entry.error === undefined) delete existing.error;
4554
4869
  else existing.error = entry.error;
4555
4870
  if (entry.durationMs === undefined) delete existing.durationMs;
4556
4871
  else existing.durationMs = entry.durationMs;
4557
4872
  } else {
4558
- const step: NonNullable<AsyncStatus["steps"]>[number] = { agent: entry.agent ?? entry.key, label: entry.key, workflowKey: entry.key, parentWorkflowRunId: workflowRunId, status: mapped, startedAt: Date.now() };
4873
+ const step: NonNullable<AsyncStatus["steps"]>[number] = {
4874
+ agent: entry.agent ?? entry.key,
4875
+ label: entry.key,
4876
+ workflowKey: entry.key,
4877
+ parentWorkflowRunId: workflowRunId,
4878
+ status: mapped,
4879
+ startedAt: Date.now(),
4880
+ ...(entry.runId ? { runId: entry.runId } : {}),
4881
+ ...(entry.state === "failed" && !entry.runId ? { async: false } : {}),
4882
+ };
4559
4883
  status.steps?.push(step);
4560
4884
  workflowSteps.set(entry.key, step);
4561
4885
  }
@@ -4567,37 +4891,48 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4567
4891
  appendWorkflowEvent({ type: "subagent.workflow.trace", trace });
4568
4892
  };
4569
4893
  try {
4570
- const workflow = await runWorkflowScript({
4571
- script: workflowScript,
4894
+ const autoRelaunchCap = resolveMaxWorkflowAutoRelaunches(deps.config.maxWorkflowAutoRelaunches);
4895
+ let relaunched = 0;
4896
+ let capReached = false;
4897
+ let workflow: Awaited<ReturnType<typeof runWorkflowScript>> | undefined;
4898
+ while (true) {
4899
+ if (relaunched > 0) {
4900
+ workflowFanoutBudget = createRunFanoutBudget(`${workflowRunId}#${relaunched + 1}`, workflowFanoutBudget.limit);
4901
+ status = { ...status, workflow: { trace: [], emits: [], console: [] } };
4902
+ }
4903
+ workflow = await runWorkflowScript({
4904
+ script: workflowScript,
4572
4905
  timeoutMs: timeout,
4573
4906
  signal: controller.signal,
4574
4907
  prompts: workflowPrompts,
4575
4908
  ...(workflowState ? { state: workflowState } : {}),
4576
4909
  onTrace: updateTrace,
4910
+ admit: (calls) => {
4911
+ status.runFanoutBudget = claimRunFanoutBatch(workflowFanoutBudget, calls.map(({ key }) => `workflow[${key}]`));
4912
+ persist();
4913
+ },
4577
4914
  onEmit: (emits) => {
4578
4915
  // Each emit is validated at the host boundary in runWorkflowScript before onEmit fires.
4579
4916
  status.workflow = { ...(status.workflow ?? { trace: [], console: [] }), emits };
4580
4917
  persist();
4581
4918
  appendWorkflowEvent({ type: "subagent.workflow.emit", value: emits.at(-1) });
4582
4919
  },
4583
- launch: async (key, childParams, workflowSignal) => {
4920
+ launch: async (key, childParams, workflowSignal, admission) => {
4584
4921
  if (workflowUsageBudget.budget && childParams.async === true) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, "workflow usageBudget does not support async runs.run launches."));
4585
4922
  const budgetState = usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults));
4586
4923
  if (budgetState?.exhausted) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, usageBudgetExceededMessage(budgetState)));
4587
- patchMissionObjective(childParams.task);
4588
4924
  const childPhase = typeof childParams.phase === "string" && childParams.phase.trim() ? childParams.phase.trim() : undefined;
4589
4925
  const childLabel = typeof childParams.label === "string" && childParams.label.trim() ? childParams.label.trim() : undefined;
4590
4926
  recordMissionWorkflowChild(missionBinding, workflowRunId, key, {
4591
4927
  status: "running",
4592
4928
  ...(typeof childParams.agent === "string" && childParams.agent.trim() ? { agent: childParams.agent.trim() } : {}),
4593
- ...(typeof childParams.task === "string" && childParams.task.trim() ? { task: childParams.task.trim() } : {}),
4594
4929
  ...(childLabel ? { label: childLabel } : {}),
4595
4930
  ...(childPhase ? { phase: childPhase } : {}),
4596
4931
  heartbeat: { status: "running", ...(childPhase ? { phase: childPhase } : {}) },
4597
4932
  });
4598
4933
  const result = await runMissionWorkflowChild(missionBinding, workflowRunId, key, childPhase, () => {
4599
4934
  const childRequest = bindMissionWorkflowChildAsyncLaunch(
4600
- prepareWorkflowLaunchParams(workflowChildDefaults, childParams, workflowRunId, key, { missionDetached: detachWorkflowChildMissions }),
4935
+ { ...prepareWorkflowLaunchParams(workflowChildDefaults, childParams, workflowRunId, key, { missionDetached: detachWorkflowChildMissions, runFanoutBudget: workflowFanoutBudget }), runFanoutAdmitted: admission.admitted },
4601
4936
  missionBinding,
4602
4937
  deps.asyncByDefault,
4603
4938
  );
@@ -4606,6 +4941,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4606
4941
  if (step) {
4607
4942
  step.agent = launch.agent;
4608
4943
  step.sessionFile = launch.sessionFile;
4944
+ step.async = launch.async;
4945
+ if (launch.runId) step.runId = launch.runId;
4609
4946
  persist();
4610
4947
  }
4611
4948
  recordMissionWorkflowChild(missionBinding, workflowRunId, key, { status: "running", agent: launch.agent, ...(launch.sessionFile ? { sessionPath: launch.sessionFile } : {}) });
@@ -4637,8 +4974,13 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4637
4974
  }, ctx, preserveActiveSession);
4638
4975
  });
4639
4976
  workflowResults.push(...result.details.results);
4640
- if (result.details.asyncDir && missionBinding) writeMissionAsyncBinding(result.details.asyncDir, missionBinding);
4641
4977
  const child = workflowChildResult(key, result);
4978
+ const step = status.steps?.find((candidate) => candidate.workflowKey === key);
4979
+ if (step) {
4980
+ step.async = Boolean(result.details.asyncId || result.details.asyncDir);
4981
+ if (child.runId) step.runId = child.runId;
4982
+ }
4983
+ if (result.details.asyncDir && missionBinding) writeMissionAsyncBinding(result.details.asyncDir, missionBinding);
4642
4984
  const childStatus = missionWorkflowChildStatus(result);
4643
4985
  recordMissionWorkflowChild(missionBinding, workflowRunId, key, {
4644
4986
  status: childStatus,
@@ -4657,14 +4999,33 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4657
4999
  },
4658
5000
  status: async (keyOrRunId, workflowSignal) => workflowChildResult(keyOrRunId, await execute(randomUUID(), { action: "status", id: keyOrRunId }, workflowSignal, undefined, ctx, preserveActiveSession)),
4659
5001
  });
5002
+ const decision = resolveAutoRelaunchDecision(workflow?.value, relaunched, autoRelaunchCap);
5003
+ if (!decision.relaunch) {
5004
+ capReached = decision.capped;
5005
+ break;
5006
+ }
5007
+ relaunched += 1;
5008
+ status = { ...status, workflowRelaunchCount: relaunched };
5009
+ persist();
5010
+ appendWorkflowEvent({ type: "subagent.workflow.relaunched", relaunched });
5011
+ }
5012
+ if (!workflow) throw new Error("Workflow script produced no result.");
4660
5013
  const returnPreview = formatWorkflowValue(workflow.value).slice(0, 1_000);
4661
5014
  const emitPreview = workflow.emits.length > 0 ? ` Emitted: ${workflow.emits.map(formatWorkflowValue).join(", ").slice(0, 1_000)}` : "";
4662
- const summary = `Workflow completed with ${workflow.children.length} child run(s). Return: ${returnPreview}${emitPreview} Trace: ${workflow.trace.length} event(s).`;
5015
+ const relaunchNote = relaunched > 0 ? `Auto-relaunched ${relaunched} time(s) with a fresh fan-out budget. ` : "";
5016
+ const summary = `${relaunchNote}Workflow completed with ${workflow.children.length} child run(s). Return: ${returnPreview}${emitPreview} Trace: ${workflow.trace.length} event(s).`;
4663
5017
  const workflowUsage = sumResultsUsage(workflowResults);
4664
- status = { ...status, state: "complete", endedAt: Date.now(), workflow: { value: workflow.value, trace: workflow.trace, emits: workflow.emits, console: workflow.console }, totalTokens: { input: workflowUsage.input, output: workflowUsage.output, total: workflowUsage.input + workflowUsage.output }, totalCost: sumResultsCost(workflowResults) };
4665
- writeAtomicJson(resultPath, { id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: true, state: "complete", summary, output: summary, results: workflow.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: workflowCwd, sessionId: currentSessionId, timestamp: Date.now(), durationMs: Date.now() - startedAt });
4666
- persist();
4667
- appendWorkflowEvent({ type: "subagent.workflow.completed", state: "complete" });
5018
+ if (capReached) {
5019
+ status = compactOptional<AsyncStatus>({ ...status, state: "failed", error: `Workflow auto-relaunch cap reached after ${relaunched} relaunch(es); the goal is not clean yet. The progress file is current; raise config.maxWorkflowAutoRelaunches or relaunch manually.`, endedAt: Date.now(), workflow: { value: workflow.value, trace: workflow.trace, emits: workflow.emits, console: workflow.console } });
5020
+ writeAtomicJson(resultPath, { id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: false, state: "failed", summary: status.error, error: status.error, results: workflow.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: workflowCwd, sessionId: currentSessionId, timestamp: Date.now(), durationMs: Date.now() - startedAt });
5021
+ persist();
5022
+ appendWorkflowEvent({ type: "subagent.workflow.completed", state: "failed", error: status.error });
5023
+ } else {
5024
+ status = { ...status, state: "complete", endedAt: Date.now(), workflow: { value: workflow.value, trace: workflow.trace, emits: workflow.emits, console: workflow.console }, totalTokens: { input: workflowUsage.input, output: workflowUsage.output, total: workflowUsage.input + workflowUsage.output }, totalCost: sumResultsCost(workflowResults) };
5025
+ writeAtomicJson(resultPath, { id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: true, state: "complete", summary, output: summary, results: workflow.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: workflowCwd, sessionId: currentSessionId, timestamp: Date.now(), durationMs: Date.now() - startedAt });
5026
+ persist();
5027
+ appendWorkflowEvent({ type: "subagent.workflow.completed", state: "complete" });
5028
+ }
4668
5029
  } catch (error) {
4669
5030
  const partial = error instanceof WorkflowScriptError ? error.partial : { trace: [], emits: [], console: [], children: [] };
4670
5031
  const stopped = controller.signal.aborted;
@@ -4674,12 +5035,14 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4674
5035
  appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, error: status.error });
4675
5036
  } finally {
4676
5037
  deps.state.workflowControllers?.delete(workflowRunId);
5038
+ deps.state.activeAsyncCapacity = workflowCapacity?.reconcile(new Set(deps.state.workflowControllers?.keys() ?? []))
5039
+ ?? deps.state.activeAsyncCapacity;
4677
5040
  }
4678
5041
  });
4679
- return attachWorkflowMission({
5042
+ return attachWorkflowMission(withRunFanoutBudget({
4680
5043
  content: [{ type: "text", text: formatAsyncStartedMessage(`Async workflow [${workflowRunId}]`, ctx.hasUI === true) }],
4681
- details: { mode: "workflow", runId: workflowRunId, toolCallId, asyncId: workflowRunId, asyncDir, results: [], chatProgress },
4682
- });
5044
+ details: { mode: "workflow", runId: workflowRunId, toolCallId, asyncId: workflowRunId, asyncDir, results: [], chatProgress, ...(deps.state.activeAsyncCapacity ? { activeAsyncCapacity: deps.state.activeAsyncCapacity } : {}) },
5045
+ }, workflowFanoutBudget));
4683
5046
  }
4684
5047
  const { workflowScript: _workflowScript, action: _action, agent: _agent, task: _task, resume: _resume, tasks: _tasks, chain: _chain, concurrency: _concurrency, async: _async, foregroundOnly: _foregroundOnly, clarify: _clarify, timeoutMs: _timeoutMs, maxRuntimeMs: _maxRuntimeMs, usageBudget: _usageBudget, chatProgress: _chatProgress, missionId: _missionId, mission: _mission, ...workflowChildDefaults } = requestParams;
4685
5048
  const workflowResults: SingleResult[] = [];
@@ -4699,28 +5062,29 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4699
5062
  liveWorkflow = { ...liveWorkflow, trace };
4700
5063
  sendWorkflowProgress();
4701
5064
  },
5065
+ admit: (calls) => {
5066
+ claimRunFanoutBatch(workflowFanoutBudget, calls.map(({ key }) => `workflow[${key}]`));
5067
+ },
4702
5068
  onEmit: (emits) => {
4703
5069
  liveWorkflow = { ...liveWorkflow, emits };
4704
5070
  sendWorkflowProgress();
4705
5071
  },
4706
- launch: async (key, childParams, workflowSignal) => {
5072
+ launch: async (key, childParams, workflowSignal, admission) => {
4707
5073
  if (workflowUsageBudget.budget && childParams.async === true) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, "workflow usageBudget does not support async runs.run launches."));
4708
5074
  const budgetState = usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults));
4709
5075
  if (budgetState?.exhausted) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, usageBudgetExceededMessage(budgetState)));
4710
- patchMissionObjective(childParams.task);
4711
5076
  const childPhase = typeof childParams.phase === "string" && childParams.phase.trim() ? childParams.phase.trim() : undefined;
4712
5077
  const childLabel = typeof childParams.label === "string" && childParams.label.trim() ? childParams.label.trim() : undefined;
4713
5078
  recordMissionWorkflowChild(missionBinding, _id, key, {
4714
5079
  status: "running",
4715
5080
  ...(typeof childParams.agent === "string" && childParams.agent.trim() ? { agent: childParams.agent.trim() } : {}),
4716
- ...(typeof childParams.task === "string" && childParams.task.trim() ? { task: childParams.task.trim() } : {}),
4717
5081
  ...(childLabel ? { label: childLabel } : {}),
4718
5082
  ...(childPhase ? { phase: childPhase } : {}),
4719
5083
  heartbeat: { status: "running", ...(childPhase ? { phase: childPhase } : {}) },
4720
5084
  });
4721
5085
  const result = await runMissionWorkflowChild(missionBinding, _id, key, childPhase, () => {
4722
5086
  const childRequest = bindMissionWorkflowChildAsyncLaunch(
4723
- prepareWorkflowLaunchParams(workflowChildDefaults, childParams, _id, key, { missionDetached: detachWorkflowChildMissions, suppressRoutineResultIntercom: chatProgress.mode === "live-card" }),
5087
+ { ...prepareWorkflowLaunchParams(workflowChildDefaults, childParams, _id, key, { missionDetached: detachWorkflowChildMissions, suppressRoutineResultIntercom: chatProgress.mode === "live-card", runFanoutBudget: workflowFanoutBudget }), runFanoutAdmitted: admission.admitted },
4724
5088
  missionBinding,
4725
5089
  deps.asyncByDefault,
4726
5090
  );
@@ -4761,10 +5125,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4761
5125
  if (workflow.emits.length > 0) sections.push(`Emitted:\n${workflow.emits.map(formatWorkflowValue).join("\n")}`);
4762
5126
  if (workflow.console.length > 0) sections.push(`Console:\n${workflow.console.map((entry) => `[${entry.level}] ${entry.text}`).join("\n")}`);
4763
5127
  if (traceLines.length > 0) sections.push(`Call trace:\n${traceLines.join("\n")}`);
4764
- return attachWorkflowMission({
5128
+ return attachWorkflowMission(withRunFanoutBudget({
4765
5129
  content: [{ type: "text", text: sections.join("\n\n") }],
4766
5130
  details: compactOptional<Details>({ mode: "workflow", runId: _id, results: workflow.children.flatMap((child) => (child.results ?? []) as SingleResult[]), totalChildUsage: sumResultsUsage(workflowResults), totalCost: sumResultsCost(workflowResults), usageBudget: usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults)), workflow: { value: workflow.value, trace: workflow.trace, emits: workflow.emits, console: workflow.console }, chatProgress }),
4767
- });
5131
+ }, workflowFanoutBudget));
4768
5132
  } catch (error) {
4769
5133
  const partial = error instanceof WorkflowScriptError ? error.partial : { trace: [], emits: [], console: [], children: [] };
4770
5134
  const text = error instanceof Error ? error.message : String(error);
@@ -4773,11 +5137,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4773
5137
  if (partial.emits.length > 0) sections.push(`Emitted:\n${partial.emits.map(formatWorkflowValue).join("\n")}`);
4774
5138
  if (partial.console.length > 0) sections.push(`Console:\n${partial.console.map((entry) => `[${entry.level}] ${entry.text}`).join("\n")}`);
4775
5139
  if (traceLines.length > 0) sections.push(`Call trace:\n${traceLines.join("\n")}`);
4776
- return attachWorkflowMission({
5140
+ return attachWorkflowMission(withRunFanoutBudget({
4777
5141
  content: [{ type: "text", text: sections.join("\n\n") }],
4778
5142
  isError: true,
4779
5143
  details: compactOptional<Details>({ mode: "workflow", runId: _id, results: partial.children.flatMap((child) => (child.results ?? []) as SingleResult[]), totalChildUsage: sumResultsUsage(workflowResults), totalCost: sumResultsCost(workflowResults), usageBudget: usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults)), workflow: { trace: partial.trace, emits: partial.emits, console: partial.console }, chatProgress }),
4780
- });
5144
+ }, workflowFanoutBudget));
4781
5145
  }
4782
5146
  }
4783
5147
  const directParams = prepareWorkflowChildParams(requestParams);
@@ -4992,7 +5356,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4992
5356
  }
4993
5357
  if (action === "children.list") {
4994
5358
  deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
4995
- const children = listRetainedChildren(DIRS.async, deps.state.currentSessionId);
5359
+ const children = listRetainedChildren(DIRS.async, deps.state.sessionLineage?.length ? deps.state.sessionLineage : [deps.state.currentSessionId]);
4996
5360
  return {
4997
5361
  content: [{ type: "text", text: formatRetainedChildren(children) }],
4998
5362
  details: { mode: "management", results: [] },
@@ -5015,6 +5379,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5015
5379
  if (!sessionError) sessionError = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
5016
5380
  }
5017
5381
  const spawnBudget = getSpawnBudgetSnapshot(deps.state, deps.config, currentSessionId);
5382
+ const activeAsyncCapacity = currentSessionId
5383
+ ? getActiveAsyncCapacitySnapshot(currentSessionId, resolveMaxActiveAsyncRunsPerSession(deps.config.maxActiveAsyncRunsPerSession), { liveWorkflowRunIds: new Set(deps.state.workflowControllers?.keys() ?? []) })
5384
+ : { used: 0, limit: resolveMaxActiveAsyncRunsPerSession(deps.config.maxActiveAsyncRunsPerSession) ?? 0 };
5385
+ deps.state.activeAsyncCapacity = activeAsyncCapacity;
5018
5386
  return {
5019
5387
  content: [{
5020
5388
  type: "text",
@@ -5031,7 +5399,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5031
5399
  expandTilde: deps.expandTilde,
5032
5400
  })),
5033
5401
  }],
5034
- details: { mode: "management", results: [], spawnBudget },
5402
+ details: { mode: "management", results: [], spawnBudget, activeAsyncCapacity },
5035
5403
  };
5036
5404
  }
5037
5405
  if (action === "status") {
@@ -5448,7 +5816,6 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5448
5816
  const runId = randomUUID().slice(0, 8);
5449
5817
  const inheritedNestedRoute = resolveInheritedNestedRouteFromEnv();
5450
5818
  const nestedParentAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
5451
- const nestedRoute = inheritedNestedRoute ?? createNestedRoute(runId);
5452
5819
  const shareEnabled = effectiveParams.share === true;
5453
5820
  const hasChain = (effectiveParams.chain?.length ?? 0) > 0;
5454
5821
  const hasTasks = (effectiveParams.tasks?.length ?? 0) > 0;
@@ -5532,9 +5899,50 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5532
5899
  const foregroundTimeout = resolveSingleAgentLaunchTimeout(
5533
5900
  effectiveParams,
5534
5901
  effectiveAsync,
5902
+ resolveConfigDefaultTimeoutMs(deps.config.timeoutMs),
5535
5903
  );
5536
5904
  if (foregroundTimeout.error) return buildRequestedModeError(effectiveParams, foregroundTimeout.error);
5537
5905
  const controlConfig = resolveControlConfig(deps.config.control, effectiveParams.control);
5906
+ const requestedWorkflowChildAsyncId = typeof effectiveParams.workflowChildAsyncId === "string" ? effectiveParams.workflowChildAsyncId.trim() : "";
5907
+ const asyncRunId = requestedWorkflowChildAsyncId && path.basename(requestedWorkflowChildAsyncId) === requestedWorkflowChildAsyncId
5908
+ ? requestedWorkflowChildAsyncId
5909
+ : randomUUID();
5910
+ const topLevelAsyncCapacityEligible = depth === 0 && !inheritedNestedRoute && !effectiveParams.workflowParentRunId;
5911
+ const topLevelAsync = effectiveAsync && topLevelAsyncCapacityEligible;
5912
+ let activeAsyncCapacity: ActiveAsyncCapacityHandle | undefined;
5913
+ if (topLevelAsync) {
5914
+ try {
5915
+ const activeLimit = resolveMaxActiveAsyncRunsPerSession(deps.config.maxActiveAsyncRunsPerSession);
5916
+ activeAsyncCapacity = acquireActiveAsyncCapacity({
5917
+ sessionId: requestSessionId,
5918
+ limit: activeLimit,
5919
+ runId: asyncRunId,
5920
+ kind: "runner",
5921
+ asyncDir: path.join(DIRS.async, asyncRunId),
5922
+ }, { liveWorkflowRunIds: new Set(deps.state.workflowControllers?.keys() ?? []) });
5923
+ deps.state.activeAsyncCapacity = getActiveAsyncCapacitySnapshot(requestSessionId, activeLimit, { liveWorkflowRunIds: new Set(deps.state.workflowControllers?.keys() ?? []) });
5924
+ } catch (error) {
5925
+ if (error instanceof ActiveAsyncCapacityError) {
5926
+ deps.state.activeAsyncCapacity = error.snapshot;
5927
+ return { content: [{ type: "text", text: error.message }], isError: true, details: { mode: foregroundMode, results: [], activeAsyncCapacity: error.snapshot } };
5928
+ }
5929
+ throw error;
5930
+ }
5931
+ }
5932
+
5933
+ let runFanoutBudget: RunFanoutBudgetDescriptor;
5934
+ try {
5935
+ const inheritedRunFanoutBudget = effectiveParams.runFanoutBudget ? undefined : decodeRunFanoutBudgetDescriptor(process.env[RUN_FANOUT_BUDGET_ENV]);
5936
+ runFanoutBudget = effectiveParams.runFanoutBudget
5937
+ ?? (inheritedRunFanoutBudget ? { ...inheritedRunFanoutBudget, parentPath: `${inheritedRunFanoutBudget.parentPath ? `${inheritedRunFanoutBudget.parentPath}/` : ""}${runId}` } : undefined)
5938
+ ?? createRunFanoutBudget(runId, resolveMaxSubagentSpawnsPerRun(deps.config.maxSubagentSpawnsPerRun));
5939
+ if (!effectiveParams.runFanoutAdmitted) claimRunFanoutBatch(runFanoutBudget, staticRunFanoutPaths(effectiveParams));
5940
+ } catch (error) {
5941
+ activeAsyncCapacity?.rollback();
5942
+ if (error instanceof RunFanoutLimitError) return runFanoutErrorResult(error, foregroundMode);
5943
+ return buildRequestedModeError(effectiveParams, error instanceof Error ? error.message : String(error));
5944
+ }
5945
+ const nestedRoute = inheritedNestedRoute ?? createNestedRoute(runId);
5538
5946
 
5539
5947
  const artifactConfig: ArtifactConfig = omitUndefinedProperties({
5540
5948
  ...DEFAULT_ARTIFACT_CONFIG,
@@ -5560,6 +5968,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5560
5968
  try {
5561
5969
  fs.mkdirSync(sessionRoot, { recursive: true });
5562
5970
  } catch (error) {
5971
+ activeAsyncCapacity?.rollback();
5563
5972
  const message = error instanceof Error ? error.message : String(error);
5564
5973
  return toExecutionErrorResult(
5565
5974
  effectiveParams,
@@ -5590,10 +5999,14 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5590
5999
  preflightForkSessionsForStaticTasks(effectiveParams, contextPolicy, forkSessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems);
5591
6000
  }
5592
6001
  } catch (error) {
6002
+ activeAsyncCapacity?.rollback();
5593
6003
  return toExecutionErrorResult(effectiveParams, error, contextPolicy.contextSummary);
5594
6004
  }
5595
6005
  const chainBindingsError = validateExecutionChainBindings(effectiveParams, deps.config.chain?.dynamicFanout?.maxItems);
5596
- if (chainBindingsError) return withResolvedContext(chainBindingsError, contextPolicy.contextSummary);
6006
+ if (chainBindingsError) {
6007
+ activeAsyncCapacity?.rollback();
6008
+ return withResolvedContext(chainBindingsError, contextPolicy.contextSummary);
6009
+ }
5597
6010
 
5598
6011
  const onUpdateWithContext = onUpdate
5599
6012
  ? (r: AgentToolResult<Details>) => onUpdate(withResolvedContext({
@@ -5613,14 +6026,17 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5613
6026
  ownerSessionId: requestSessionId,
5614
6027
  });
5615
6028
  } catch (error) {
5616
- if (explicitMission) return toExecutionErrorResult(effectiveParams, error, contextPolicy.contextSummary);
6029
+ if (explicitMission) {
6030
+ activeAsyncCapacity?.rollback();
6031
+ return toExecutionErrorResult(effectiveParams, error, contextPolicy.contextSummary);
6032
+ }
5617
6033
  missionWarning = `Mission tracking unavailable: ${error instanceof Error ? error.message : String(error)}`;
5618
6034
  }
5619
6035
 
5620
6036
  const attachMission = (result: AgentToolResult<Details>): AgentToolResult<Details> => {
5621
6037
  if (!missionBinding) return missionWarning ? { ...result, details: { ...result.details, missionWarning } } : result;
5622
6038
  try {
5623
- return attachMissionToLaunchResult({ binding: missionBinding, result });
6039
+ return attachMissionToLaunchResult({ binding: delegatedExecution ? { ...missionBinding, announceInContent: false } : missionBinding, result });
5624
6040
  } catch (error) {
5625
6041
  const warning = `Mission tracking unavailable after launch: ${error instanceof Error ? error.message : String(error)}`;
5626
6042
  if (explicitMission) {
@@ -5641,7 +6057,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5641
6057
  requestSessionId,
5642
6058
  requestedSpawns,
5643
6059
  );
5644
- if (reservation.error) return attachMission(spawnBudgetErrorResult(reservation.error, foregroundMode));
6060
+ if (reservation.error) {
6061
+ activeAsyncCapacity?.rollback();
6062
+ return attachMission(spawnBudgetErrorResult(reservation.error, foregroundMode));
6063
+ }
5645
6064
 
5646
6065
  const execData: ExecutionContextData = omitUndefinedProperties({
5647
6066
  params: effectiveParams,
@@ -5661,6 +6080,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5661
6080
  artifactsDir,
5662
6081
  backgroundRequestedWhileClarifying,
5663
6082
  effectiveAsync,
6083
+ asyncRunId,
5664
6084
  controlConfig,
5665
6085
  intercomBridge,
5666
6086
  nestedRoute,
@@ -5676,11 +6096,14 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5676
6096
  parentSessionId: requestSessionId,
5677
6097
  parentPiSessionId: requestPiSessionId,
5678
6098
  capabilityCeiling: resolveCurrentSubagentCapabilityCeiling(requestSessionId),
6099
+ runFanoutBudget,
6100
+ topLevelAsyncCapacityEligible,
6101
+ activeAsyncCapacity,
5679
6102
  });
5680
6103
 
5681
- const foregroundDescription = effectiveParams.task?.trim()
5682
- || effectiveParams.tasks?.[0]?.task?.trim()
5683
- || (effectiveParams.chain ? firstRawChainTask(effectiveParams.chain)?.trim() : undefined);
6104
+ const foregroundDescription = selectedAgentNames.length === 1
6105
+ ? `${selectedAgentNames[0]} child`
6106
+ : `${selectedAgentNames.length} live children`;
5684
6107
  const parentWorkflowStatus = effectiveParams.workflowParentRunId
5685
6108
  ? readStatus(path.join(DIRS.async, effectiveParams.workflowParentRunId))
5686
6109
  : null;
@@ -5715,6 +6138,37 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5715
6138
  interrupt: undefined,
5716
6139
  });
5717
6140
  if (foregroundControl) {
6141
+ foregroundControl.promptAuditRedo = async (index, guidance) => {
6142
+ const audit = getLivePromptAudit(foregroundControl, index);
6143
+ if (!audit?.rerun) return { text: "Redo is not safe for this prompt in this slice.", isError: true };
6144
+ const rewrittenTask = await rewritePromptWithGuidance({
6145
+ ctx,
6146
+ authoredTask: audit.authoredTask,
6147
+ runtimeAdditions: audit.runtimeAdditions,
6148
+ finalEffectivePrompt: audit.finalEffectivePrompt,
6149
+ guidance,
6150
+ signal,
6151
+ });
6152
+ const redoParams = promptAuditRedoParams(audit.rerun.params, rewrittenTask);
6153
+ const previousForegroundId = deps.state.lastForegroundControlId;
6154
+ const launch = execute(randomUUID(), redoParams, signal, undefined, ctx, true);
6155
+ const newRunId = deps.state.lastForegroundControlId && deps.state.lastForegroundControlId !== previousForegroundId
6156
+ ? deps.state.lastForegroundControlId
6157
+ : undefined;
6158
+ if (!newRunId) {
6159
+ const result = await launch;
6160
+ return { text: result.content.find((item) => item.type === "text")?.text ?? "Prompt redo could not start.", isError: true };
6161
+ }
6162
+ foregroundControl.supersededByRunId = newRunId;
6163
+ const replacement = deps.state.foregroundControls.get(newRunId);
6164
+ if (replacement) replacement.sourceRunId = foregroundControl.runId;
6165
+ void launch.then((result) => {
6166
+ if (result.isError) console.warn(`[pi-subagents] Prompt redo ${newRunId} failed: ${result.content.find((item) => item.type === "text")?.text ?? "unknown error"}`);
6167
+ }).catch((error) => {
6168
+ console.warn(`[pi-subagents] Prompt redo ${newRunId} failed: ${error instanceof Error ? error.message : String(error)}`);
6169
+ });
6170
+ return { text: `Prompt redo started ${newRunId}.` };
6171
+ };
5718
6172
  deps.state.foregroundControls.set(runId, foregroundControl);
5719
6173
  deps.state.lastForegroundControlId = runId;
5720
6174
  }
@@ -5809,9 +6263,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5809
6263
  if (workflowLaunchObserver) {
5810
6264
  const singleTask = hasTasks && effectiveParams.tasks?.length === 1 ? effectiveParams.tasks[0] : undefined;
5811
6265
  const launch = hasSingle
5812
- ? { agent: effectiveParams.agent!, sessionFile: childSessionFileForTask(effectiveParams.agent!, 0, effectiveParams.model) }
6266
+ ? { agent: effectiveParams.agent!, sessionFile: childSessionFileForTask(effectiveParams.agent!, 0, effectiveParams.model), async: effectiveAsync, ...(effectiveAsync ? { runId: asyncRunId } : {}) }
5813
6267
  : singleTask
5814
- ? { agent: singleTask.agent, sessionFile: childSessionFileForTask(singleTask.agent, 0, singleTask.model) }
6268
+ ? { agent: singleTask.agent, sessionFile: childSessionFileForTask(singleTask.agent, 0, singleTask.model), async: effectiveAsync, ...(effectiveAsync ? { runId: asyncRunId } : {}) }
5815
6269
  : undefined;
5816
6270
  if (launch) {
5817
6271
  workflowLaunchObservers.delete(params);
@@ -5819,31 +6273,33 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5819
6273
  }
5820
6274
  }
5821
6275
  const asyncResult = runAsyncPath(execData, deps);
5822
- if (asyncResult) return attachMission(withResolvedContext(withForkThinkingNotes(asyncResult, forkThinkingDowngrades), contextPolicy.contextSummary));
6276
+ if (asyncResult) return attachMission(withRunFanoutBudget(withResolvedContext(withForkThinkingNotes(asyncResult, forkThinkingDowngrades), contextPolicy.contextSummary), runFanoutBudget));
5823
6277
  if (foregroundControl) {
5824
6278
  writeNestedForegroundEvent("subagent.nested.started");
5825
6279
  nestedForegroundStarted = true;
5826
6280
  }
6281
+ const runFanoutAnnotateContent = !delegatedExecution;
5827
6282
  if (hasChain && effectiveParams.chain) {
5828
6283
  const result = await runChainPath(execData, deps);
5829
6284
  writeNestedForegroundEvent("subagent.nested.completed", result);
5830
- return attachMission(withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary));
6285
+ return attachMission(withRunFanoutBudget(withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary), runFanoutBudget, { annotateContent: runFanoutAnnotateContent }));
5831
6286
  }
5832
6287
  if (hasTasks && effectiveParams.tasks) {
5833
6288
  const result = await runParallelPath(execData, deps);
5834
6289
  writeNestedForegroundEvent("subagent.nested.completed", result);
5835
- return attachMission(withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary));
6290
+ return attachMission(withRunFanoutBudget(withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary), runFanoutBudget, { annotateContent: runFanoutAnnotateContent }));
5836
6291
  }
5837
6292
  if (hasSingle) {
5838
6293
  const result = await runSinglePath(execData, deps);
5839
6294
  writeNestedForegroundEvent("subagent.nested.completed", result);
5840
- return attachMission(withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary));
6295
+ return attachMission(withRunFanoutBudget(withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary), runFanoutBudget, { annotateContent: runFanoutAnnotateContent }));
5841
6296
  }
5842
6297
  } catch (error) {
5843
6298
  const errorResult = withForkThinkingNotes(toExecutionErrorResult(effectiveParams, error, contextPolicy.contextSummary), forkThinkingDowngrades);
5844
6299
  if (nestedForegroundStarted) writeNestedForegroundEvent("subagent.nested.completed", errorResult);
5845
6300
  return attachMission(errorResult);
5846
6301
  } finally {
6302
+ if (activeAsyncCapacity && !activeAsyncCapacity.owner.runnerStartedAt) activeAsyncCapacity.rollback();
5847
6303
  if (foregroundControl) {
5848
6304
  settleForegroundSchedulingOwner(foregroundControl);
5849
6305
  removeForegroundControlIfIdle(deps.state, runId);
@@ -5912,6 +6368,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5912
6368
  delete privateParams.delegatedAllowZeroToolBudget;
5913
6369
  if (thinkingOverride !== undefined) delegatedThinkingOverrides.set(delegatedParams, thinkingOverride);
5914
6370
  if (allowZeroToolBudget) delegatedZeroToolBudgets.add(delegatedParams);
6371
+ delegatedExecutions.add(delegatedParams);
5915
6372
  return execute(id, delegatedParams, signal, onUpdate, ctx);
5916
6373
  };
5917
6374