pi-subagents 0.31.1 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +40 -4
  2. package/README.md +97 -7
  3. package/package.json +1 -1
  4. package/skills/pi-subagents/SKILL.md +6 -1
  5. package/src/agents/agent-management.ts +6 -1
  6. package/src/agents/agents.ts +55 -11
  7. package/src/extension/companion-suggestions.ts +359 -0
  8. package/src/extension/config.ts +27 -4
  9. package/src/extension/doctor.ts +2 -0
  10. package/src/extension/fanout-child.ts +1 -0
  11. package/src/extension/index.ts +58 -4
  12. package/src/extension/schemas.ts +2 -2
  13. package/src/runs/background/async-execution.ts +138 -33
  14. package/src/runs/background/async-job-tracker.ts +77 -1
  15. package/src/runs/background/async-status.ts +41 -9
  16. package/src/runs/background/chain-root-attachment.ts +34 -4
  17. package/src/runs/background/control-channel.ts +50 -0
  18. package/src/runs/background/run-status.ts +1 -0
  19. package/src/runs/background/stale-run-reconciler.ts +28 -1
  20. package/src/runs/background/subagent-runner.ts +454 -115
  21. package/src/runs/foreground/chain-execution.ts +29 -7
  22. package/src/runs/foreground/execution.ts +24 -6
  23. package/src/runs/foreground/subagent-executor.ts +209 -35
  24. package/src/runs/shared/acceptance.ts +45 -22
  25. package/src/runs/shared/dynamic-fanout.ts +1 -1
  26. package/src/runs/shared/model-fallback.ts +4 -0
  27. package/src/runs/shared/nested-events.ts +58 -0
  28. package/src/runs/shared/parallel-utils.ts +49 -1
  29. package/src/runs/shared/pi-args.ts +5 -3
  30. package/src/runs/shared/pi-spawn.ts +52 -20
  31. package/src/runs/shared/single-output.ts +2 -0
  32. package/src/runs/shared/subagent-prompt-runtime.ts +2 -2
  33. package/src/runs/shared/worktree.ts +28 -5
  34. package/src/shared/artifacts.ts +15 -1
  35. package/src/shared/fork-context.ts +133 -22
  36. package/src/shared/types.ts +81 -3
  37. package/src/shared/utils.ts +99 -14
  38. package/src/slash/slash-commands.ts +117 -0
  39. package/src/tui/render.ts +16 -4
@@ -4,7 +4,7 @@ import * as path from "node:path";
4
4
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
5
5
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
6
6
  import { type AgentConfig, type AgentScope } from "../../agents/agents.ts";
7
- import { getArtifactsDir } from "../../shared/artifacts.ts";
7
+ import { getArtifactsDir, getProjectChainRunsDir } from "../../shared/artifacts.ts";
8
8
  import { ChainClarifyComponent, type ChainClarifyResult } from "./chain-clarify.ts";
9
9
  import { toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
10
10
  import { executeChain } from "./chain-execution.ts";
@@ -40,7 +40,8 @@ import { resolveCurrentSessionId } from "../../shared/session-identity.ts";
40
40
  import { applyIntercomBridgeToAgent, INTERCOM_BRIDGE_MARKER, resolveIntercomBridge, resolveIntercomSessionTarget, resolveSubagentIntercomTarget, type IntercomBridgeState } from "../../intercom/intercom-bridge.ts";
41
41
  import { formatControlIntercomMessage, formatControlNoticeMessage, resolveControlConfig, shouldNotifyControlEvent } from "../shared/subagent-control.ts";
42
42
  import { finalizeSingleOutput, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
43
- import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, readStatus, resolveChildCwd } from "../../shared/utils.ts";
43
+ import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, readStatus, resolveChildCwd, sumResultsCost, sumResultsUsage } from "../../shared/utils.ts";
44
+ import { DEFAULT_GLOBAL_CONCURRENCY_LIMIT, Semaphore } from "../shared/parallel-utils.ts";
44
45
  import {
45
46
  attachNestedChildrenToResultChildren,
46
47
  buildSubagentResultIntercomPayload,
@@ -54,7 +55,7 @@ import { buildRevivedAsyncTask, interruptLiveAsyncResumeTarget, resolveAsyncResu
54
55
  import { deliverInterruptRequest } from "../background/control-channel.ts";
55
56
  import { reconcileAsyncRun } from "../background/stale-run-reconciler.ts";
56
57
  import { resolveAsyncRootResultPath } from "../background/chain-root-attachment.ts";
57
- import { createNestedRoute, readNestedControlResults, resolveInheritedNestedRouteFromEnv, resolveNestedAsyncDir, resolveNestedParentAddressFromEnv, updateForegroundNestedProjection, writeNestedControlRequest, writeNestedEvent, type NestedRunResolutionScope } from "../shared/nested-events.ts";
58
+ import { attachRootChildrenToSteps, createNestedRoute, readNestedControlResults, resolveInheritedNestedRouteFromEnv, resolveNestedAsyncDir, resolveNestedParentAddressFromEnv, updateForegroundNestedProjection, writeNestedControlRequest, writeNestedEvent, type NestedRunResolutionScope } from "../shared/nested-events.ts";
58
59
  import { resolveSubagentRunId, type ResolvedSubagentRunId } from "../background/run-id-resolver.ts";
59
60
  import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
60
61
  import { inspectSubagentStatus } from "../background/run-status.ts";
@@ -92,6 +93,7 @@ import {
92
93
  SUBAGENT_CONTROL_EVENT,
93
94
  SUBAGENT_CONTROL_INTERCOM_EVENT,
94
95
  checkSubagentDepth,
96
+ resolveMaxSubagentSpawnsPerSession,
95
97
  resolveTopLevelParallelConcurrency,
96
98
  resolveTopLevelParallelMaxTasks,
97
99
  resolveChildMaxSubagentDepth,
@@ -154,6 +156,7 @@ interface ExecutorDeps {
154
156
  state: SubagentState;
155
157
  config: ExtensionConfig;
156
158
  asyncByDefault: boolean;
159
+ companionSuggestionLines?: (input: { surface: "list" | "doctor"; cwd: string; context?: "fresh" | "fork"; orchestratorTarget?: string }) => string[];
157
160
  tempArtifactsDir: string;
158
161
  getSubagentSessionRoot: (parentSessionFile: string | null) => string;
159
162
  expandTilde: (p: string) => string;
@@ -175,6 +178,7 @@ interface ExecutionContextData {
175
178
  sessionDirForIndex: (idx?: number) => string;
176
179
  sessionFileForIndex: (idx?: number) => string | undefined;
177
180
  sessionFileForTask: (agentName: string, idx?: number) => string | undefined;
181
+ thinkingOverrideForTask: (agentName: string, idx?: number) => AgentConfig["thinking"] | undefined;
178
182
  artifactConfig: ArtifactConfig;
179
183
  artifactsDir: string;
180
184
  backgroundRequestedWhileClarifying: boolean;
@@ -233,6 +237,35 @@ function nestedResolutionScopeForExecutor(deps: ExecutorDeps): NestedRunResoluti
233
237
  };
234
238
  }
235
239
 
240
+ function reserveSubagentSpawns(input: { state: SubagentState; config: ExtensionConfig; sessionId: string | null; requested: number; mode: "single" | "parallel" | "chain" }): AgentToolResult<Details> | undefined {
241
+ if (input.requested <= 0) return undefined;
242
+ if (input.state.subagentSpawns?.sessionId !== input.sessionId) {
243
+ input.state.subagentSpawns = { sessionId: input.sessionId, count: 0 };
244
+ }
245
+ const maxSpawns = resolveMaxSubagentSpawnsPerSession(input.config.maxSubagentSpawnsPerSession);
246
+ const used = input.state.subagentSpawns.count;
247
+ if (used + input.requested > maxSpawns) {
248
+ return {
249
+ content: [{ type: "text", text: `Subagent spawn limit reached for this session (${used}/${maxSpawns} used, ${input.requested} requested). Complete the work directly or start a new session.` }],
250
+ isError: true,
251
+ details: { mode: input.mode, results: [] },
252
+ };
253
+ }
254
+ input.state.subagentSpawns.count = used + input.requested;
255
+ return undefined;
256
+ }
257
+
258
+ function countRequestedSubagentSpawns(params: SubagentParamsLike, config: ExtensionConfig): number {
259
+ if (params.tasks) return params.tasks.length;
260
+ if (params.chain) {
261
+ return params.chain.reduce((total, step) => {
262
+ if (isDynamicParallelStep(step)) return total + (step.expand.maxItems ?? config.chain?.dynamicFanout?.maxItems ?? 0);
263
+ return total + getStepAgents(step).length;
264
+ }, 0);
265
+ }
266
+ return params.agent ? 1 : 0;
267
+ }
268
+
236
269
  function foregroundStatusResult(control: SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never): AgentToolResult<Details> {
237
270
  let nestedWarning: string | undefined;
238
271
  try {
@@ -921,7 +954,7 @@ async function resumeAsyncRun(input: {
921
954
  availableModels,
922
955
  cwd: effectiveCwd,
923
956
  maxOutput: input.params.maxOutput,
924
- artifactsDir: input.deps.tempArtifactsDir,
957
+ artifactsDir: getArtifactsDir(parentSessionFile, effectiveCwd),
925
958
  artifactConfig,
926
959
  shareEnabled: input.params.share === true,
927
960
  sessionRoot: input.deps.getSubagentSessionRoot(parentSessionFile),
@@ -930,9 +963,11 @@ async function resumeAsyncRun(input: {
930
963
  maxSubagentDepth: resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth),
931
964
  worktreeSetupHook: input.deps.config.worktreeSetupHook,
932
965
  worktreeSetupHookTimeoutMs: input.deps.config.worktreeSetupHookTimeoutMs,
966
+ worktreeBaseDir: input.deps.config.worktreeBaseDir,
933
967
  controlConfig: resolveControlConfig(input.deps.config.control, input.params.control),
934
968
  controlIntercomTarget: intercomBridge.active ? intercomBridge.orchestratorTarget : undefined,
935
969
  childIntercomTarget: intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
970
+ globalConcurrencyLimit: input.deps.config.globalConcurrencyLimit,
936
971
  });
937
972
  if (result.isError) return result;
938
973
  const attachedId = result.details.asyncId ?? runId;
@@ -948,6 +983,7 @@ async function resumeAsyncRun(input: {
948
983
 
949
984
  const runId = randomUUID().slice(0, 8);
950
985
  const artifactConfig: ArtifactConfig = { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false };
986
+ const artifactsDir = getArtifactsDir(parentSessionFile, effectiveCwd);
951
987
  const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo);
952
988
  const result = executeAsyncSingle(runId, {
953
989
  agent: target.agent,
@@ -963,14 +999,16 @@ async function resumeAsyncRun(input: {
963
999
  },
964
1000
  cwd: effectiveCwd,
965
1001
  maxOutput: input.params.maxOutput,
966
- artifactsDir: input.deps.tempArtifactsDir,
1002
+ artifactsDir,
967
1003
  artifactConfig,
968
1004
  shareEnabled: input.params.share === true,
969
1005
  sessionRoot: input.deps.getSubagentSessionRoot(parentSessionFile),
970
1006
  sessionFile: target.sessionFile,
1007
+ outputBaseDir: resolveSingleRunOutputBaseDir(input.deps, artifactsDir, runId),
971
1008
  maxSubagentDepth: resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth),
972
1009
  worktreeSetupHook: input.deps.config.worktreeSetupHook,
973
1010
  worktreeSetupHookTimeoutMs: input.deps.config.worktreeSetupHookTimeoutMs,
1011
+ worktreeBaseDir: input.deps.config.worktreeBaseDir,
974
1012
  controlConfig: resolveControlConfig(input.deps.config.control, input.params.control),
975
1013
  controlIntercomTarget: intercomBridge.active ? intercomBridge.orchestratorTarget : undefined,
976
1014
  childIntercomTarget: intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
@@ -1391,6 +1429,7 @@ function toExecutionErrorResult(params: SubagentParamsLike, error: unknown): Age
1391
1429
  function collectChainSessionFiles(
1392
1430
  chain: ChainStep[],
1393
1431
  sessionFileForTask: (agentName: string, idx?: number) => string | undefined,
1432
+ dynamicFanoutMaxItems?: number,
1394
1433
  ): (string | undefined)[] {
1395
1434
  const sessionFiles: (string | undefined)[] = [];
1396
1435
  let flatIndex = 0;
@@ -1403,7 +1442,11 @@ function collectChainSessionFiles(
1403
1442
  continue;
1404
1443
  }
1405
1444
  if (isDynamicParallelStep(step)) {
1406
- sessionFiles.push(undefined);
1445
+ const maxItems = step.expand.maxItems ?? dynamicFanoutMaxItems ?? 0;
1446
+ for (let itemIndex = 0; itemIndex < maxItems; itemIndex++) {
1447
+ sessionFiles.push(sessionFileForTask(step.parallel.agent, flatIndex));
1448
+ flatIndex++;
1449
+ }
1407
1450
  continue;
1408
1451
  }
1409
1452
  sessionFiles.push(sessionFileForTask((step as SequentialStep).agent, flatIndex));
@@ -1412,6 +1455,35 @@ function collectChainSessionFiles(
1412
1455
  return sessionFiles;
1413
1456
  }
1414
1457
 
1458
+ function collectChainThinkingOverrides(
1459
+ chain: ChainStep[],
1460
+ thinkingOverrideForTask: (agentName: string, idx?: number) => AgentConfig["thinking"] | undefined,
1461
+ dynamicFanoutMaxItems?: number,
1462
+ ): (AgentConfig["thinking"] | undefined)[] {
1463
+ const thinkingOverrides: (AgentConfig["thinking"] | undefined)[] = [];
1464
+ let flatIndex = 0;
1465
+ for (const step of chain) {
1466
+ if (isParallelStep(step)) {
1467
+ for (const task of step.parallel) {
1468
+ thinkingOverrides.push(thinkingOverrideForTask(task.agent, flatIndex));
1469
+ flatIndex++;
1470
+ }
1471
+ continue;
1472
+ }
1473
+ if (isDynamicParallelStep(step)) {
1474
+ const maxItems = step.expand.maxItems ?? dynamicFanoutMaxItems ?? 0;
1475
+ for (let itemIndex = 0; itemIndex < maxItems; itemIndex++) {
1476
+ thinkingOverrides.push(thinkingOverrideForTask(step.parallel.agent, flatIndex));
1477
+ flatIndex++;
1478
+ }
1479
+ continue;
1480
+ }
1481
+ thinkingOverrides.push(thinkingOverrideForTask((step as SequentialStep).agent, flatIndex));
1482
+ flatIndex++;
1483
+ }
1484
+ return thinkingOverrides;
1485
+ }
1486
+
1415
1487
  function wrapChainTasksForFork(chain: ChainStep[], contextPolicy: AgentDefaultContextPolicy): ChainStep[] {
1416
1488
  return chain.map((step, stepIndex) => {
1417
1489
  if (isParallelStep(step)) {
@@ -1450,6 +1522,7 @@ function preflightForkSessionsForStaticTasks(
1450
1522
  params: SubagentParamsLike,
1451
1523
  contextPolicy: AgentDefaultContextPolicy,
1452
1524
  sessionFileForTask: (agentName: string, idx?: number) => string | undefined,
1525
+ dynamicFanoutMaxItems?: number,
1453
1526
  ): void {
1454
1527
  if (!contextPolicy.usesFork) return;
1455
1528
  if (params.agent) {
@@ -1473,8 +1546,11 @@ function preflightForkSessionsForStaticTasks(
1473
1546
  continue;
1474
1547
  }
1475
1548
  if (isDynamicParallelStep(step)) {
1476
- if (shouldForkAgent(contextPolicy, step.parallel.agent)) sessionFileForTask(step.parallel.agent, flatIndex);
1477
- flatIndex++;
1549
+ const maxItems = step.expand.maxItems ?? dynamicFanoutMaxItems ?? 0;
1550
+ if (shouldForkAgent(contextPolicy, step.parallel.agent)) {
1551
+ for (let itemIndex = 0; itemIndex < maxItems; itemIndex++) sessionFileForTask(step.parallel.agent, flatIndex + itemIndex);
1552
+ }
1553
+ flatIndex += maxItems;
1478
1554
  continue;
1479
1555
  }
1480
1556
  const sequential = step as SequentialStep;
@@ -1493,6 +1569,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1493
1569
  sessionRoot,
1494
1570
  sessionFileForIndex,
1495
1571
  sessionFileForTask,
1572
+ thinkingOverrideForTask,
1496
1573
  artifactConfig,
1497
1574
  artifactsDir,
1498
1575
  effectiveAsync,
@@ -1586,13 +1663,17 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1586
1663
  sessionRoot,
1587
1664
  chainSkills: [],
1588
1665
  sessionFilesByFlatIndex: params.tasks.map((task, index) => sessionFileForTask(task.agent, index)),
1666
+ thinkingOverridesByFlatIndex: params.tasks.map((task, index) => thinkingOverrideForTask(task.agent, index)),
1589
1667
  maxSubagentDepth: currentMaxSubagentDepth,
1590
1668
  worktreeSetupHook: deps.config.worktreeSetupHook,
1591
1669
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
1670
+ worktreeBaseDir: deps.config.worktreeBaseDir,
1592
1671
  controlConfig,
1593
1672
  controlIntercomTarget,
1594
1673
  childIntercomTarget,
1595
1674
  nestedRoute,
1675
+ timeoutMs: data.timeoutMs,
1676
+ globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
1596
1677
  });
1597
1678
  }
1598
1679
 
@@ -1613,15 +1694,19 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1613
1694
  shareEnabled,
1614
1695
  sessionRoot,
1615
1696
  chainSkills,
1616
- sessionFilesByFlatIndex: collectChainSessionFiles(chain, sessionFileForTask),
1697
+ sessionFilesByFlatIndex: collectChainSessionFiles(chain, sessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems),
1698
+ thinkingOverridesByFlatIndex: collectChainThinkingOverrides(chain, thinkingOverrideForTask, deps.config.chain?.dynamicFanout?.maxItems),
1617
1699
  dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
1618
1700
  maxSubagentDepth: currentMaxSubagentDepth,
1619
1701
  worktreeSetupHook: deps.config.worktreeSetupHook,
1620
1702
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
1703
+ worktreeBaseDir: deps.config.worktreeBaseDir,
1621
1704
  controlConfig,
1622
1705
  controlIntercomTarget,
1623
1706
  childIntercomTarget,
1624
1707
  nestedRoute,
1708
+ timeoutMs: data.timeoutMs,
1709
+ globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
1625
1710
  });
1626
1711
  }
1627
1712
 
@@ -1657,15 +1742,19 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1657
1742
  skills,
1658
1743
  output: effectiveOutput,
1659
1744
  outputMode: effectiveOutputMode,
1745
+ outputBaseDir: resolveSingleRunOutputBaseDir(deps, artifactsDir, id),
1660
1746
  modelOverride,
1747
+ thinkingOverride: thinkingOverrideForTask(params.agent!, 0),
1661
1748
  maxSubagentDepth,
1662
1749
  worktreeSetupHook: deps.config.worktreeSetupHook,
1663
1750
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
1751
+ worktreeBaseDir: deps.config.worktreeBaseDir,
1664
1752
  controlConfig,
1665
1753
  controlIntercomTarget,
1666
1754
  childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(agent, index) : undefined,
1667
1755
  nestedRoute,
1668
1756
  acceptance: params.acceptance,
1757
+ timeoutMs: data.timeoutMs,
1669
1758
  });
1670
1759
  }
1671
1760
 
@@ -1684,6 +1773,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1684
1773
  sessionDirForIndex,
1685
1774
  sessionFileForIndex,
1686
1775
  sessionFileForTask,
1776
+ thinkingOverrideForTask,
1687
1777
  artifactsDir,
1688
1778
  artifactConfig,
1689
1779
  onUpdate,
@@ -1711,6 +1801,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1711
1801
  sessionDirForIndex,
1712
1802
  sessionFileForIndex,
1713
1803
  sessionFileForTask,
1804
+ thinkingOverrideForTask,
1714
1805
  artifactsDir,
1715
1806
  artifactConfig,
1716
1807
  includeProgress: params.includeProgress,
@@ -1723,19 +1814,18 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1723
1814
  foregroundControl,
1724
1815
  nestedRoute: foregroundControl?.nestedRoute,
1725
1816
  chainSkills,
1726
- chainDir: params.chainDir,
1817
+ chainDir: params.chainDir ?? getProjectChainRunsDir(effectiveCwd),
1727
1818
  dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
1728
1819
  maxSubagentDepth: currentMaxSubagentDepth,
1729
1820
  worktreeSetupHook: deps.config.worktreeSetupHook,
1730
1821
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
1822
+ worktreeBaseDir: deps.config.worktreeBaseDir,
1731
1823
  timeoutMs: data.timeoutMs,
1732
1824
  deadlineAt: data.deadlineAt,
1825
+ globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
1733
1826
  });
1734
1827
 
1735
1828
  if (chainResult.requestedAsync) {
1736
- if (data.timeoutMs !== undefined) {
1737
- return buildRequestedModeError(params, "timeoutMs/maxRuntimeMs are only supported for foreground runs; background launch from clarify cannot preserve the timeout.");
1738
- }
1739
1829
  if (!isAsyncAvailable()) {
1740
1830
  return {
1741
1831
  content: [{ type: "text", text: "Background mode requires upstream jiti for TypeScript execution but it could not be found. Ensure the pi-subagents package dependencies are installed." }],
@@ -1766,20 +1856,29 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1766
1856
  shareEnabled,
1767
1857
  sessionRoot,
1768
1858
  chainSkills: chainResult.requestedAsync.chainSkills,
1769
- sessionFilesByFlatIndex: collectChainSessionFiles(asyncChain, sessionFileForTask),
1859
+ sessionFilesByFlatIndex: collectChainSessionFiles(asyncChain, sessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems),
1860
+ thinkingOverridesByFlatIndex: collectChainThinkingOverrides(asyncChain, thinkingOverrideForTask, deps.config.chain?.dynamicFanout?.maxItems),
1770
1861
  dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
1771
1862
  maxSubagentDepth: currentMaxSubagentDepth,
1772
1863
  worktreeSetupHook: deps.config.worktreeSetupHook,
1773
1864
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
1865
+ worktreeBaseDir: deps.config.worktreeBaseDir,
1774
1866
  controlConfig,
1775
1867
  controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
1776
1868
  childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
1777
1869
  nestedRoute: data.nestedRoute,
1870
+ timeoutMs: data.timeoutMs,
1871
+ globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
1778
1872
  });
1779
1873
  }
1780
1874
 
1781
- const chainDetails = chainResult.details ? compactForegroundDetails({ ...chainResult.details, runId }) : undefined;
1782
- if (foregroundControl) updateForegroundNestedProjection(foregroundControl);
1875
+ const rawChainDetails = chainResult.details ? { ...chainResult.details, runId } : undefined;
1876
+ if (foregroundControl && rawChainDetails) {
1877
+ updateForegroundNestedProjection(foregroundControl);
1878
+ attachRootChildrenToSteps(runId, rawChainDetails.results, foregroundControl.nestedChildren);
1879
+ rawChainDetails.totalCost = sumResultsCost(rawChainDetails.results);
1880
+ }
1881
+ const chainDetails = rawChainDetails ? compactForegroundDetails(rawChainDetails) : undefined;
1783
1882
  if (chainDetails) rememberForegroundRun(deps.state, { runId, mode: "chain", cwd: effectiveCwd, results: chainDetails.results });
1784
1883
  const intercomReceipt = chainDetails && !chainDetails.results.some((result) => result.interrupted || result.detached)
1785
1884
  ? await maybeBuildForegroundIntercomReceipt({
@@ -1813,9 +1912,11 @@ interface ForegroundParallelRunInput {
1813
1912
  sessionDirForIndex: (idx?: number) => string | undefined;
1814
1913
  sessionFileForIndex: (idx?: number) => string | undefined;
1815
1914
  sessionFileForTask: (agentName: string, idx?: number) => string | undefined;
1915
+ thinkingOverrideForTask: (agentName: string, idx?: number) => AgentConfig["thinking"] | undefined;
1816
1916
  shareEnabled: boolean;
1817
1917
  artifactConfig: ArtifactConfig;
1818
1918
  artifactsDir: string;
1919
+ outputBaseDir: string;
1819
1920
  maxOutput?: MaxOutputConfig;
1820
1921
  paramsCwd: string;
1821
1922
  progressDir: string;
@@ -1830,6 +1931,7 @@ interface ForegroundParallelRunInput {
1830
1931
  orchestratorIntercomTarget?: string;
1831
1932
  foregroundControl?: SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never;
1832
1933
  concurrencyLimit: number;
1934
+ globalSemaphore?: Semaphore;
1833
1935
  liveResults: (SingleResult | undefined)[];
1834
1936
  liveProgress: (AgentProgress | undefined)[];
1835
1937
  onUpdate?: (r: AgentToolResult<Details>) => void;
@@ -1853,6 +1955,7 @@ function createParallelWorktreeSetup(
1853
1955
  tasks: TaskParam[],
1854
1956
  setupHook: ExtensionConfig["worktreeSetupHook"],
1855
1957
  setupHookTimeoutMs: ExtensionConfig["worktreeSetupHookTimeoutMs"],
1958
+ baseDir: ExtensionConfig["worktreeBaseDir"],
1856
1959
  ): { setup?: WorktreeSetup; errorResult?: AgentToolResult<Details> } {
1857
1960
  if (!enabled) return {};
1858
1961
  try {
@@ -1862,6 +1965,7 @@ function createParallelWorktreeSetup(
1862
1965
  setupHook: setupHook
1863
1966
  ? { hookPath: setupHook, timeoutMs: setupHookTimeoutMs }
1864
1967
  : undefined,
1968
+ baseDir,
1865
1969
  }),
1866
1970
  };
1867
1971
  } catch (error) {
@@ -1879,6 +1983,12 @@ function buildParallelWorktreeTaskCwdError(
1879
1983
  return formatWorktreeTaskCwdConflict(conflict, sharedCwd);
1880
1984
  }
1881
1985
 
1986
+ function resolveSingleRunOutputBaseDir(deps: ExecutorDeps, artifactsDir: string, runId: string): string {
1987
+ return deps.config.singleRunOutputBaseDir
1988
+ ? path.resolve(deps.expandTilde(deps.config.singleRunOutputBaseDir))
1989
+ : path.join(artifactsDir, "outputs", runId);
1990
+ }
1991
+
1882
1992
  function buildChainWorktreeTaskCwdError(chain: ChainStep[], sharedCwd: string): string | undefined {
1883
1993
  for (let stepIndex = 0; stepIndex < chain.length; stepIndex++) {
1884
1994
  const step = chain[stepIndex]!;
@@ -1918,6 +2028,7 @@ function findDuplicateParallelOutputPath(input: {
1918
2028
  behaviors: ResolvedStepBehavior[];
1919
2029
  paramsCwd: string;
1920
2030
  ctxCwd: string;
2031
+ outputBaseDir: string;
1921
2032
  worktreeSetup?: WorktreeSetup;
1922
2033
  }): string | undefined {
1923
2034
  const seen = new Map<string, { index: number; agent: string }>();
@@ -1926,7 +2037,7 @@ function findDuplicateParallelOutputPath(input: {
1926
2037
  if (!behavior?.output) continue;
1927
2038
  const task = input.tasks[index]!;
1928
2039
  const taskCwd = resolveParallelTaskCwd(task, input.paramsCwd, input.worktreeSetup, index);
1929
- const outputPath = resolveSingleOutputPath(behavior.output, input.ctxCwd, taskCwd);
2040
+ const outputPath = resolveSingleOutputPath(behavior.output, input.ctxCwd, taskCwd, input.outputBaseDir);
1930
2041
  if (!outputPath) continue;
1931
2042
  const previous = seen.get(outputPath);
1932
2043
  if (previous) {
@@ -1938,6 +2049,12 @@ function findDuplicateParallelOutputPath(input: {
1938
2049
  }
1939
2050
 
1940
2051
  async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Promise<SingleResult[]> {
2052
+ // Pre-warm fork session files sequentially before concurrent dispatch to avoid
2053
+ // races where multiple workers simultaneously try to branch the same parent session.
2054
+ // sessionFileForIndex caches results, so these calls return instantly inside mapConcurrent.
2055
+ for (let i = 0; i < input.tasks.length; i++) {
2056
+ input.sessionFileForIndex(i);
2057
+ }
1941
2058
  return mapConcurrent(input.tasks, input.concurrencyLimit, async (task, index) => {
1942
2059
  const behavior = input.behaviors[index];
1943
2060
  const effectiveSkills = behavior?.skills;
@@ -1948,7 +2065,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
1948
2065
  const progressInstructions = behavior
1949
2066
  ? buildChainInstructions({ ...behavior, output: false, reads: false }, input.progressDir, index === input.firstProgressIndex)
1950
2067
  : { prefix: "", suffix: "" };
1951
- const outputPath = resolveSingleOutputPath(behavior?.output, input.ctx.cwd, taskCwd);
2068
+ const outputPath = resolveSingleOutputPath(behavior?.output, input.ctx.cwd, taskCwd, input.outputBaseDir);
1952
2069
  const taskText = injectSingleOutputInstruction(
1953
2070
  `${readInstructions.prefix}${input.taskTexts[index]!}${progressInstructions.suffix}`,
1954
2071
  outputPath,
@@ -1992,6 +2109,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
1992
2109
  orchestratorIntercomTarget: input.orchestratorIntercomTarget,
1993
2110
  nestedRoute: input.foregroundControl?.nestedRoute,
1994
2111
  modelOverride: input.modelOverrides[index],
2112
+ thinkingOverride: input.thinkingOverrideForTask(task.agent, index),
1995
2113
  availableModels: input.availableModels,
1996
2114
  preferredModelProvider: input.ctx.model?.provider,
1997
2115
  skills: effectiveSkills === false ? [] : effectiveSkills,
@@ -2039,7 +2157,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
2039
2157
  input.foregroundControl.updatedAt = Date.now();
2040
2158
  }
2041
2159
  });
2042
- });
2160
+ }, input.globalSemaphore);
2043
2161
  }
2044
2162
 
2045
2163
  async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps): Promise<AgentToolResult<Details>> {
@@ -2053,6 +2171,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2053
2171
  sessionDirForIndex,
2054
2172
  sessionFileForIndex,
2055
2173
  sessionFileForTask,
2174
+ thinkingOverrideForTask,
2056
2175
  shareEnabled,
2057
2176
  artifactConfig,
2058
2177
  artifactsDir,
@@ -2163,9 +2282,6 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2163
2282
  }
2164
2283
 
2165
2284
  if (result.runInBackground) {
2166
- if (data.timeoutMs !== undefined) {
2167
- return buildRequestedModeError(params, "timeoutMs/maxRuntimeMs are only supported for foreground runs; background launch from clarify cannot preserve the timeout.");
2168
- }
2169
2285
  if (!isAsyncAvailable()) {
2170
2286
  return {
2171
2287
  content: [{ type: "text", text: "Background mode requires upstream jiti for TypeScript execution but it could not be found. Ensure the pi-subagents package dependencies are installed." }],
@@ -2212,12 +2328,16 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2212
2328
  sessionRoot,
2213
2329
  chainSkills: [],
2214
2330
  sessionFilesByFlatIndex: tasks.map((task, index) => sessionFileForTask(task.agent, index)),
2331
+ thinkingOverridesByFlatIndex: tasks.map((task, index) => thinkingOverrideForTask(task.agent, index)),
2215
2332
  maxSubagentDepth: currentMaxSubagentDepth,
2216
2333
  worktreeSetupHook: deps.config.worktreeSetupHook,
2217
2334
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
2335
+ worktreeBaseDir: deps.config.worktreeBaseDir,
2218
2336
  controlConfig,
2219
2337
  controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
2220
2338
  childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
2339
+ timeoutMs: data.timeoutMs,
2340
+ globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2221
2341
  });
2222
2342
  }
2223
2343
  }
@@ -2234,21 +2354,24 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2234
2354
  tasks,
2235
2355
  deps.config.worktreeSetupHook,
2236
2356
  deps.config.worktreeSetupHookTimeoutMs,
2357
+ deps.config.worktreeBaseDir,
2237
2358
  );
2238
2359
  if (errorResult) return errorResult;
2239
2360
 
2240
2361
  try {
2362
+ const outputBaseDir = path.join(artifactsDir, "outputs", runId);
2241
2363
  const duplicateOutputError = findDuplicateParallelOutputPath({
2242
2364
  tasks,
2243
2365
  behaviors,
2244
2366
  paramsCwd: effectiveCwd,
2245
2367
  ctxCwd: ctx.cwd,
2368
+ outputBaseDir,
2246
2369
  worktreeSetup,
2247
2370
  });
2248
2371
  if (duplicateOutputError) return buildParallelModeError(duplicateOutputError);
2249
2372
  for (let index = 0; index < tasks.length; index++) {
2250
2373
  const taskCwd = resolveParallelTaskCwd(tasks[index]!, effectiveCwd, worktreeSetup, index);
2251
- const outputPath = resolveSingleOutputPath(behaviors[index]?.output, ctx.cwd, taskCwd);
2374
+ const outputPath = resolveSingleOutputPath(behaviors[index]?.output, ctx.cwd, taskCwd, outputBaseDir);
2252
2375
  const validationError = validateFileOnlyOutputMode(behaviors[index]?.outputMode, outputPath, `Parallel task ${index + 1} (${tasks[index]!.agent})`);
2253
2376
  if (validationError) return buildParallelModeError(validationError);
2254
2377
  }
@@ -2273,9 +2396,11 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2273
2396
  sessionDirForIndex,
2274
2397
  sessionFileForIndex,
2275
2398
  sessionFileForTask,
2399
+ thinkingOverrideForTask,
2276
2400
  shareEnabled,
2277
2401
  artifactConfig,
2278
2402
  artifactsDir,
2403
+ outputBaseDir,
2279
2404
  maxOutput: params.maxOutput,
2280
2405
  paramsCwd: effectiveCwd,
2281
2406
  progressDir: parallelProgressDir,
@@ -2289,6 +2414,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2289
2414
  orchestratorIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
2290
2415
  foregroundControl,
2291
2416
  concurrencyLimit: parallelConcurrency,
2417
+ globalSemaphore: new Semaphore(deps.config.globalConcurrencyLimit ?? DEFAULT_GLOBAL_CONCURRENCY_LIMIT),
2292
2418
  maxSubagentDepths,
2293
2419
  liveResults,
2294
2420
  liveProgress,
@@ -2307,6 +2433,10 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2307
2433
  if (result.artifactPaths) allArtifactPaths.push(result.artifactPaths);
2308
2434
  }
2309
2435
 
2436
+ if (foregroundControl) {
2437
+ updateForegroundNestedProjection(foregroundControl);
2438
+ attachRootChildrenToSteps(runId, results, foregroundControl.nestedChildren);
2439
+ }
2310
2440
  const interrupted = results.find((result) => result.interrupted);
2311
2441
  const details = compactForegroundDetails({
2312
2442
  mode: "parallel",
@@ -2314,6 +2444,8 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2314
2444
  results,
2315
2445
  progress: params.includeProgress ? allProgress : undefined,
2316
2446
  artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
2447
+ totalChildUsage: sumResultsUsage(results),
2448
+ totalCost: sumResultsCost(results),
2317
2449
  });
2318
2450
  rememberForegroundRun(deps.state, { runId, mode: "parallel", cwd: effectiveCwd, results: details.results });
2319
2451
  if (interrupted) {
@@ -2385,6 +2517,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2385
2517
  runId,
2386
2518
  sessionDirForIndex,
2387
2519
  sessionFileForTask,
2520
+ thinkingOverrideForTask,
2388
2521
  shareEnabled,
2389
2522
  artifactConfig,
2390
2523
  artifactsDir,
@@ -2455,9 +2588,6 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2455
2588
  if (override?.skills !== undefined) skillOverride = override.skills;
2456
2589
 
2457
2590
  if (result.runInBackground) {
2458
- if (data.timeoutMs !== undefined) {
2459
- return buildRequestedModeError(params, "timeoutMs/maxRuntimeMs are only supported for foreground runs; background launch from clarify cannot preserve the timeout.");
2460
- }
2461
2591
  if (!isAsyncAvailable()) {
2462
2592
  return {
2463
2593
  content: [{ type: "text", text: "Background mode requires upstream jiti for TypeScript execution but it could not be found. Ensure the pi-subagents package dependencies are installed." }],
@@ -2490,13 +2620,17 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2490
2620
  skills: skillOverride === false ? [] : skillOverride,
2491
2621
  output: effectiveOutput,
2492
2622
  outputMode: effectiveOutputMode,
2623
+ outputBaseDir: resolveSingleRunOutputBaseDir(deps, artifactsDir, id),
2493
2624
  modelOverride,
2625
+ thinkingOverride: thinkingOverrideForTask(params.agent!, 0),
2494
2626
  maxSubagentDepth,
2495
2627
  worktreeSetupHook: deps.config.worktreeSetupHook,
2496
2628
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
2629
+ worktreeBaseDir: deps.config.worktreeBaseDir,
2497
2630
  controlConfig,
2498
2631
  controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
2499
2632
  childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
2633
+ timeoutMs: data.timeoutMs,
2500
2634
  });
2501
2635
  }
2502
2636
  }
@@ -2505,7 +2639,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2505
2639
  task = wrapForkTask(task);
2506
2640
  }
2507
2641
  const cleanTask = task;
2508
- const outputPath = resolveSingleOutputPath(effectiveOutput, ctx.cwd, effectiveCwd);
2642
+ const outputPath = resolveSingleOutputPath(effectiveOutput, ctx.cwd, effectiveCwd, resolveSingleRunOutputBaseDir(deps, artifactsDir, runId));
2509
2643
  const validationError = validateFileOnlyOutputMode(effectiveOutputMode, outputPath, `Single run (${params.agent})`);
2510
2644
  if (validationError) {
2511
2645
  return { content: [{ type: "text", text: validationError }], isError: true, details: { mode: "single", results: [] } };
@@ -2580,6 +2714,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2580
2714
  nestedRoute: foregroundControl?.nestedRoute,
2581
2715
  index: 0,
2582
2716
  modelOverride,
2717
+ thinkingOverride: thinkingOverrideForTask(params.agent!, 0),
2583
2718
  availableModels,
2584
2719
  preferredModelProvider: currentProvider,
2585
2720
  skills: effectiveSkills,
@@ -2616,6 +2751,10 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2616
2751
  outputReference: r.outputReference,
2617
2752
  saveError: r.outputSaveError,
2618
2753
  });
2754
+ if (foregroundControl) {
2755
+ updateForegroundNestedProjection(foregroundControl);
2756
+ attachRootChildrenToSteps(runId, [r], foregroundControl.nestedChildren);
2757
+ }
2619
2758
  const details = compactForegroundDetails({
2620
2759
  mode: "single",
2621
2760
  runId,
@@ -2623,6 +2762,8 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2623
2762
  progress: params.includeProgress ? allProgress : undefined,
2624
2763
  artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
2625
2764
  truncation: r.truncation,
2765
+ totalChildUsage: sumResultsUsage([r]),
2766
+ totalCost: sumResultsCost([r]),
2626
2767
  });
2627
2768
  rememberForegroundRun(deps.state, { runId, mode: "single", cwd: effectiveCwd, results: details.results });
2628
2769
 
@@ -2741,6 +2882,12 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2741
2882
  orchestratorTarget,
2742
2883
  sessionError,
2743
2884
  expandTilde: deps.expandTilde,
2885
+ companionPackageLines: deps.companionSuggestionLines?.({
2886
+ surface: "doctor",
2887
+ cwd: requestCwd,
2888
+ context: paramsWithResolvedCwd.context,
2889
+ orchestratorTarget,
2890
+ }),
2744
2891
  }),
2745
2892
  }],
2746
2893
  details: { mode: "management", results: [] },
@@ -2828,7 +2975,21 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2828
2975
  details: { mode: "management" as const, results: [] },
2829
2976
  };
2830
2977
  }
2831
- return handleManagementAction(params.action, paramsWithResolvedCwd, { ...ctx, cwd: requestCwd, config: deps.config });
2978
+ return handleManagementAction(params.action, paramsWithResolvedCwd, {
2979
+ ...ctx,
2980
+ cwd: requestCwd,
2981
+ config: deps.config,
2982
+ companionSuggestionLines: () => {
2983
+ if (params.action !== "list" || deps.state.companionSuggestionListShown) return [];
2984
+ const lines = deps.companionSuggestionLines?.({
2985
+ surface: "list",
2986
+ cwd: requestCwd,
2987
+ context: paramsWithResolvedCwd.context,
2988
+ }) ?? [];
2989
+ if (lines.length > 0) deps.state.companionSuggestionListShown = true;
2990
+ return lines;
2991
+ },
2992
+ });
2832
2993
  }
2833
2994
 
2834
2995
  const { blocked, depth, maxDepth } = checkSubagentDepth(deps.config.maxSubagentDepth);
@@ -2901,24 +3062,24 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2901
3062
  if (validationError) return validationError;
2902
3063
 
2903
3064
  let forkSessionFileForIndex: (idx?: number) => string | undefined = () => undefined;
3065
+ let forkThinkingOverrideForIndex: (idx?: number) => AgentConfig["thinking"] | undefined = () => undefined;
2904
3066
  try {
2905
- forkSessionFileForIndex = createForkContextResolver(ctx.sessionManager, contextPolicy.usesFork ? "fork" : undefined).sessionFileForIndex;
3067
+ const forkContextResolver = createForkContextResolver(ctx.sessionManager, contextPolicy.usesFork ? "fork" : undefined);
3068
+ forkSessionFileForIndex = forkContextResolver.sessionFileForIndex;
3069
+ forkThinkingOverrideForIndex = forkContextResolver.thinkingOverrideForIndex;
2906
3070
  } catch (error) {
2907
3071
  return toExecutionErrorResult(effectiveParams, error);
2908
3072
  }
2909
3073
  const requestedAsync = effectiveParams.async ?? deps.asyncByDefault;
2910
3074
  const backgroundRequestedWhileClarifying = (hasChain || hasTasks) && requestedAsync && effectiveParams.clarify === true;
2911
3075
  const effectiveAsync = requestedAsync && effectiveParams.clarify !== true;
2912
- if (foregroundTimeout.timeoutMs !== undefined && effectiveAsync) {
2913
- return buildRequestedModeError(effectiveParams, "timeoutMs/maxRuntimeMs are only supported for foreground runs; set async: false or omit the timeout for background runs.");
2914
- }
2915
3076
  const controlConfig = resolveControlConfig(deps.config.control, effectiveParams.control);
2916
3077
 
2917
3078
  const artifactConfig: ArtifactConfig = {
2918
3079
  ...DEFAULT_ARTIFACT_CONFIG,
2919
3080
  enabled: effectiveParams.artifacts !== false,
2920
3081
  };
2921
- const artifactsDir = effectiveAsync ? deps.tempArtifactsDir : getArtifactsDir(parentSessionFile);
3082
+ const artifactsDir = getArtifactsDir(parentSessionFile, effectiveCwd);
2922
3083
 
2923
3084
  let sessionRoot: string;
2924
3085
  if (effectiveParams.sessionDir) {
@@ -2942,12 +3103,14 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2942
3103
  path.join(sessionRoot, `run-${idx ?? 0}`);
2943
3104
  const forkSessionFileForTask = (agentName: string, idx?: number) =>
2944
3105
  shouldForkAgent(contextPolicy, agentName) ? forkSessionFileForIndex(idx) : undefined;
3106
+ const forkThinkingOverrideForTask = (agentName: string, idx?: number) =>
3107
+ shouldForkAgent(contextPolicy, agentName) ? forkThinkingOverrideForIndex(idx) : undefined;
2945
3108
  const childSessionFileForTask = (agentName: string, idx?: number) =>
2946
3109
  forkSessionFileForTask(agentName, idx) ?? path.join(sessionDirForIndex(idx), "session.jsonl");
2947
3110
  const childSessionFileForIndex = (idx?: number) =>
2948
3111
  path.join(sessionDirForIndex(idx), "session.jsonl");
2949
3112
  try {
2950
- preflightForkSessionsForStaticTasks(effectiveParams, contextPolicy, forkSessionFileForTask);
3113
+ preflightForkSessionsForStaticTasks(effectiveParams, contextPolicy, forkSessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems);
2951
3114
  } catch (error) {
2952
3115
  return toExecutionErrorResult(effectiveParams, error);
2953
3116
  }
@@ -2958,6 +3121,16 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2958
3121
  ? (r: AgentToolResult<Details>) => onUpdate(withForkContext(r, effectiveParams.context))
2959
3122
  : undefined;
2960
3123
 
3124
+ const foregroundMode: "single" | "parallel" | "chain" = hasChain ? "chain" : hasTasks ? "parallel" : "single";
3125
+ const spawnLimitError = reserveSubagentSpawns({
3126
+ state: deps.state,
3127
+ config: deps.config,
3128
+ sessionId: deps.state.currentSessionId,
3129
+ requested: countRequestedSubagentSpawns(effectiveParams, deps.config),
3130
+ mode: foregroundMode,
3131
+ });
3132
+ if (spawnLimitError) return spawnLimitError;
3133
+
2961
3134
  const execData: ExecutionContextData = {
2962
3135
  params: effectiveParams,
2963
3136
  effectiveCwd,
@@ -2971,6 +3144,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2971
3144
  sessionDirForIndex,
2972
3145
  sessionFileForIndex: childSessionFileForIndex,
2973
3146
  sessionFileForTask: childSessionFileForTask,
3147
+ thinkingOverrideForTask: forkThinkingOverrideForTask,
2974
3148
  artifactConfig,
2975
3149
  artifactsDir,
2976
3150
  backgroundRequestedWhileClarifying,
@@ -2982,7 +3156,6 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2982
3156
  contextPolicy,
2983
3157
  };
2984
3158
 
2985
- const foregroundMode: "single" | "parallel" | "chain" = hasChain ? "chain" : hasTasks ? "parallel" : "single";
2986
3159
  const foregroundControl = effectiveAsync
2987
3160
  ? undefined
2988
3161
  : {
@@ -3046,6 +3219,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3046
3219
  startedAt: foregroundControl?.startedAt ?? now,
3047
3220
  ...(state !== "running" ? { endedAt: now } : {}),
3048
3221
  lastUpdate: now,
3222
+ ...(details?.totalCost ? { totalCost: details.totalCost } : {}),
3049
3223
  ...(errorText ? { error: errorText } : {}),
3050
3224
  ...(details?.results.length ? { steps: details.results.map((child) => ({
3051
3225
  agent: child.agent,