pi-subagents 0.31.0 → 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 (42) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +170 -8
  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 +69 -4
  12. package/src/extension/schemas.ts +2 -2
  13. package/src/intercom/intercom-bridge.ts +25 -1
  14. package/src/profiles/profiles.ts +637 -0
  15. package/src/runs/background/async-execution.ts +138 -33
  16. package/src/runs/background/async-job-tracker.ts +77 -1
  17. package/src/runs/background/async-resume.ts +11 -13
  18. package/src/runs/background/async-status.ts +41 -9
  19. package/src/runs/background/chain-root-attachment.ts +34 -4
  20. package/src/runs/background/control-channel.ts +227 -0
  21. package/src/runs/background/run-status.ts +1 -0
  22. package/src/runs/background/stale-run-reconciler.ts +28 -1
  23. package/src/runs/background/subagent-runner.ts +459 -113
  24. package/src/runs/foreground/chain-execution.ts +29 -7
  25. package/src/runs/foreground/execution.ts +24 -6
  26. package/src/runs/foreground/subagent-executor.ts +240 -44
  27. package/src/runs/shared/acceptance.ts +45 -22
  28. package/src/runs/shared/dynamic-fanout.ts +1 -1
  29. package/src/runs/shared/model-fallback.ts +4 -0
  30. package/src/runs/shared/nested-events.ts +58 -0
  31. package/src/runs/shared/parallel-utils.ts +49 -1
  32. package/src/runs/shared/pi-args.ts +5 -3
  33. package/src/runs/shared/pi-spawn.ts +52 -20
  34. package/src/runs/shared/single-output.ts +2 -0
  35. package/src/runs/shared/subagent-prompt-runtime.ts +3 -2
  36. package/src/runs/shared/worktree.ts +28 -5
  37. package/src/shared/artifacts.ts +15 -1
  38. package/src/shared/fork-context.ts +133 -22
  39. package/src/shared/types.ts +82 -3
  40. package/src/shared/utils.ts +99 -14
  41. package/src/slash/slash-commands.ts +726 -40
  42. 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,
@@ -51,8 +52,10 @@ import {
51
52
  stripDetailsOutputsForIntercomReceipt,
52
53
  } from "../../intercom/result-intercom.ts";
53
54
  import { buildRevivedAsyncTask, interruptLiveAsyncResumeTarget, resolveAsyncResumeTarget } from "../background/async-resume.ts";
55
+ import { deliverInterruptRequest } from "../background/control-channel.ts";
56
+ import { reconcileAsyncRun } from "../background/stale-run-reconciler.ts";
54
57
  import { resolveAsyncRootResultPath } from "../background/chain-root-attachment.ts";
55
- 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";
56
59
  import { resolveSubagentRunId, type ResolvedSubagentRunId } from "../background/run-id-resolver.ts";
57
60
  import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
58
61
  import { inspectSubagentStatus } from "../background/run-status.ts";
@@ -90,6 +93,7 @@ import {
90
93
  SUBAGENT_CONTROL_EVENT,
91
94
  SUBAGENT_CONTROL_INTERCOM_EVENT,
92
95
  checkSubagentDepth,
96
+ resolveMaxSubagentSpawnsPerSession,
93
97
  resolveTopLevelParallelConcurrency,
94
98
  resolveTopLevelParallelMaxTasks,
95
99
  resolveChildMaxSubagentDepth,
@@ -97,7 +101,6 @@ import {
97
101
  wrapForkTask,
98
102
  } from "../../shared/types.ts";
99
103
 
100
- const ASYNC_INTERRUPT_SIGNAL: NodeJS.Signals = process.platform === "win32" ? "SIGBREAK" : "SIGUSR2";
101
104
  const MUTATING_MANAGEMENT_ACTIONS = new Set(["create", "update", "delete"]);
102
105
 
103
106
  interface TaskParam {
@@ -153,6 +156,7 @@ interface ExecutorDeps {
153
156
  state: SubagentState;
154
157
  config: ExtensionConfig;
155
158
  asyncByDefault: boolean;
159
+ companionSuggestionLines?: (input: { surface: "list" | "doctor"; cwd: string; context?: "fresh" | "fork"; orchestratorTarget?: string }) => string[];
156
160
  tempArtifactsDir: string;
157
161
  getSubagentSessionRoot: (parentSessionFile: string | null) => string;
158
162
  expandTilde: (p: string) => string;
@@ -174,6 +178,7 @@ interface ExecutionContextData {
174
178
  sessionDirForIndex: (idx?: number) => string;
175
179
  sessionFileForIndex: (idx?: number) => string | undefined;
176
180
  sessionFileForTask: (agentName: string, idx?: number) => string | undefined;
181
+ thinkingOverrideForTask: (agentName: string, idx?: number) => AgentConfig["thinking"] | undefined;
177
182
  artifactConfig: ArtifactConfig;
178
183
  artifactsDir: string;
179
184
  backgroundRequestedWhileClarifying: boolean;
@@ -232,6 +237,35 @@ function nestedResolutionScopeForExecutor(deps: ExecutorDeps): NestedRunResoluti
232
237
  };
233
238
  }
234
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
+
235
269
  function foregroundStatusResult(control: SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never): AgentToolResult<Details> {
236
270
  let nestedWarning: string | undefined;
237
271
  try {
@@ -372,7 +406,17 @@ function resolveResumeTarget(params: SubagentParamsLike, state: SubagentState, o
372
406
  throw new Error("Run not found. Provide id or runId.");
373
407
  }
374
408
 
375
- function getAsyncInterruptTarget(state: SubagentState, runId: string | undefined): { asyncId: string; asyncDir: string } | undefined {
409
+ function getAsyncInterruptTarget(
410
+ state: SubagentState,
411
+ runId: string | undefined,
412
+ location?: { asyncDir: string | null; resolvedId?: string },
413
+ ): { asyncId: string; asyncDir: string } | undefined {
414
+ if (location?.asyncDir) {
415
+ return {
416
+ asyncId: location.resolvedId ?? runId ?? path.basename(location.asyncDir),
417
+ asyncDir: location.asyncDir,
418
+ };
419
+ }
376
420
  if (runId) {
377
421
  const direct = state.asyncJobs.get(runId);
378
422
  if (direct) return { asyncId: direct.asyncId, asyncDir: direct.asyncDir };
@@ -415,10 +459,15 @@ function emitControlNotification(input: {
415
459
  }
416
460
  }
417
461
 
418
- function interruptAsyncRun(state: SubagentState, runId: string | undefined, kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean): AgentToolResult<Details> | null {
419
- const target = getAsyncInterruptTarget(state, runId);
462
+ function interruptAsyncRun(
463
+ state: SubagentState,
464
+ runId: string | undefined,
465
+ kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean,
466
+ location?: { asyncDir: string | null; resolvedId?: string },
467
+ ): AgentToolResult<Details> | null {
468
+ const target = getAsyncInterruptTarget(state, runId, location);
420
469
  if (!target) return null;
421
- const status = readStatus(target.asyncDir);
470
+ const status = reconcileAsyncRun(target.asyncDir, { kill }).status;
422
471
  if (!status || status.state !== "running" || typeof status.pid !== "number") {
423
472
  return {
424
473
  content: [{ type: "text", text: `No running async run with an interrupt-capable pid was found for '${runId ?? "current"}'.` }],
@@ -427,7 +476,7 @@ function interruptAsyncRun(state: SubagentState, runId: string | undefined, kill
427
476
  };
428
477
  }
429
478
  try {
430
- (kill ?? process.kill)(status.pid, ASYNC_INTERRUPT_SIGNAL);
479
+ deliverInterruptRequest({ asyncDir: target.asyncDir, pid: status.pid, kill, source: "interrupt-action" });
431
480
  const tracked = state.asyncJobs.get(target.asyncId);
432
481
  if (tracked) {
433
482
  tracked.activityState = undefined;
@@ -719,11 +768,11 @@ function directNestedAsyncInterrupt(target: ResolvedSubagentRunId & { kind: "nes
719
768
  const run = target.match.run;
720
769
  const asyncDir = resolveNestedAsyncDir(target.match.rootRunId, run);
721
770
  if (!asyncDir) return undefined;
722
- const status = readStatus(asyncDir);
771
+ const status = reconcileAsyncRun(asyncDir, { resultsDir: path.join(RESULTS_DIR, "nested", target.match.rootRunId) }).status;
723
772
  const pid = typeof status?.pid === "number" && status.pid > 0 ? status.pid : run.pid;
724
773
  if (!status || status.state !== "running" || typeof pid !== "number" || pid <= 0) return undefined;
725
774
  try {
726
- process.kill(pid, ASYNC_INTERRUPT_SIGNAL);
775
+ deliverInterruptRequest({ asyncDir, pid, source: "nested-interrupt" });
727
776
  return { content: [{ type: "text", text: `Interrupt requested for nested async run ${run.id}.` }], details: { mode: "management", results: [] } };
728
777
  } catch (error) {
729
778
  const message = error instanceof Error ? error.message : String(error);
@@ -800,6 +849,7 @@ async function resumeAsyncRun(input: {
800
849
  target,
801
850
  state: input.deps.state,
802
851
  kill: input.deps.kill,
852
+ resultsDir: RESULTS_DIR,
803
853
  });
804
854
  if (!interrupt.ok) {
805
855
  return {
@@ -904,7 +954,7 @@ async function resumeAsyncRun(input: {
904
954
  availableModels,
905
955
  cwd: effectiveCwd,
906
956
  maxOutput: input.params.maxOutput,
907
- artifactsDir: input.deps.tempArtifactsDir,
957
+ artifactsDir: getArtifactsDir(parentSessionFile, effectiveCwd),
908
958
  artifactConfig,
909
959
  shareEnabled: input.params.share === true,
910
960
  sessionRoot: input.deps.getSubagentSessionRoot(parentSessionFile),
@@ -913,9 +963,11 @@ async function resumeAsyncRun(input: {
913
963
  maxSubagentDepth: resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth),
914
964
  worktreeSetupHook: input.deps.config.worktreeSetupHook,
915
965
  worktreeSetupHookTimeoutMs: input.deps.config.worktreeSetupHookTimeoutMs,
966
+ worktreeBaseDir: input.deps.config.worktreeBaseDir,
916
967
  controlConfig: resolveControlConfig(input.deps.config.control, input.params.control),
917
968
  controlIntercomTarget: intercomBridge.active ? intercomBridge.orchestratorTarget : undefined,
918
969
  childIntercomTarget: intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
970
+ globalConcurrencyLimit: input.deps.config.globalConcurrencyLimit,
919
971
  });
920
972
  if (result.isError) return result;
921
973
  const attachedId = result.details.asyncId ?? runId;
@@ -931,6 +983,7 @@ async function resumeAsyncRun(input: {
931
983
 
932
984
  const runId = randomUUID().slice(0, 8);
933
985
  const artifactConfig: ArtifactConfig = { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false };
986
+ const artifactsDir = getArtifactsDir(parentSessionFile, effectiveCwd);
934
987
  const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo);
935
988
  const result = executeAsyncSingle(runId, {
936
989
  agent: target.agent,
@@ -946,14 +999,16 @@ async function resumeAsyncRun(input: {
946
999
  },
947
1000
  cwd: effectiveCwd,
948
1001
  maxOutput: input.params.maxOutput,
949
- artifactsDir: input.deps.tempArtifactsDir,
1002
+ artifactsDir,
950
1003
  artifactConfig,
951
1004
  shareEnabled: input.params.share === true,
952
1005
  sessionRoot: input.deps.getSubagentSessionRoot(parentSessionFile),
953
1006
  sessionFile: target.sessionFile,
1007
+ outputBaseDir: resolveSingleRunOutputBaseDir(input.deps, artifactsDir, runId),
954
1008
  maxSubagentDepth: resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth),
955
1009
  worktreeSetupHook: input.deps.config.worktreeSetupHook,
956
1010
  worktreeSetupHookTimeoutMs: input.deps.config.worktreeSetupHookTimeoutMs,
1011
+ worktreeBaseDir: input.deps.config.worktreeBaseDir,
957
1012
  controlConfig: resolveControlConfig(input.deps.config.control, input.params.control),
958
1013
  controlIntercomTarget: intercomBridge.active ? intercomBridge.orchestratorTarget : undefined,
959
1014
  childIntercomTarget: intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
@@ -1374,6 +1429,7 @@ function toExecutionErrorResult(params: SubagentParamsLike, error: unknown): Age
1374
1429
  function collectChainSessionFiles(
1375
1430
  chain: ChainStep[],
1376
1431
  sessionFileForTask: (agentName: string, idx?: number) => string | undefined,
1432
+ dynamicFanoutMaxItems?: number,
1377
1433
  ): (string | undefined)[] {
1378
1434
  const sessionFiles: (string | undefined)[] = [];
1379
1435
  let flatIndex = 0;
@@ -1386,7 +1442,11 @@ function collectChainSessionFiles(
1386
1442
  continue;
1387
1443
  }
1388
1444
  if (isDynamicParallelStep(step)) {
1389
- 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
+ }
1390
1450
  continue;
1391
1451
  }
1392
1452
  sessionFiles.push(sessionFileForTask((step as SequentialStep).agent, flatIndex));
@@ -1395,6 +1455,35 @@ function collectChainSessionFiles(
1395
1455
  return sessionFiles;
1396
1456
  }
1397
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
+
1398
1487
  function wrapChainTasksForFork(chain: ChainStep[], contextPolicy: AgentDefaultContextPolicy): ChainStep[] {
1399
1488
  return chain.map((step, stepIndex) => {
1400
1489
  if (isParallelStep(step)) {
@@ -1433,6 +1522,7 @@ function preflightForkSessionsForStaticTasks(
1433
1522
  params: SubagentParamsLike,
1434
1523
  contextPolicy: AgentDefaultContextPolicy,
1435
1524
  sessionFileForTask: (agentName: string, idx?: number) => string | undefined,
1525
+ dynamicFanoutMaxItems?: number,
1436
1526
  ): void {
1437
1527
  if (!contextPolicy.usesFork) return;
1438
1528
  if (params.agent) {
@@ -1456,8 +1546,11 @@ function preflightForkSessionsForStaticTasks(
1456
1546
  continue;
1457
1547
  }
1458
1548
  if (isDynamicParallelStep(step)) {
1459
- if (shouldForkAgent(contextPolicy, step.parallel.agent)) sessionFileForTask(step.parallel.agent, flatIndex);
1460
- 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;
1461
1554
  continue;
1462
1555
  }
1463
1556
  const sequential = step as SequentialStep;
@@ -1476,6 +1569,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1476
1569
  sessionRoot,
1477
1570
  sessionFileForIndex,
1478
1571
  sessionFileForTask,
1572
+ thinkingOverrideForTask,
1479
1573
  artifactConfig,
1480
1574
  artifactsDir,
1481
1575
  effectiveAsync,
@@ -1569,13 +1663,17 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1569
1663
  sessionRoot,
1570
1664
  chainSkills: [],
1571
1665
  sessionFilesByFlatIndex: params.tasks.map((task, index) => sessionFileForTask(task.agent, index)),
1666
+ thinkingOverridesByFlatIndex: params.tasks.map((task, index) => thinkingOverrideForTask(task.agent, index)),
1572
1667
  maxSubagentDepth: currentMaxSubagentDepth,
1573
1668
  worktreeSetupHook: deps.config.worktreeSetupHook,
1574
1669
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
1670
+ worktreeBaseDir: deps.config.worktreeBaseDir,
1575
1671
  controlConfig,
1576
1672
  controlIntercomTarget,
1577
1673
  childIntercomTarget,
1578
1674
  nestedRoute,
1675
+ timeoutMs: data.timeoutMs,
1676
+ globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
1579
1677
  });
1580
1678
  }
1581
1679
 
@@ -1596,15 +1694,19 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1596
1694
  shareEnabled,
1597
1695
  sessionRoot,
1598
1696
  chainSkills,
1599
- sessionFilesByFlatIndex: collectChainSessionFiles(chain, sessionFileForTask),
1697
+ sessionFilesByFlatIndex: collectChainSessionFiles(chain, sessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems),
1698
+ thinkingOverridesByFlatIndex: collectChainThinkingOverrides(chain, thinkingOverrideForTask, deps.config.chain?.dynamicFanout?.maxItems),
1600
1699
  dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
1601
1700
  maxSubagentDepth: currentMaxSubagentDepth,
1602
1701
  worktreeSetupHook: deps.config.worktreeSetupHook,
1603
1702
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
1703
+ worktreeBaseDir: deps.config.worktreeBaseDir,
1604
1704
  controlConfig,
1605
1705
  controlIntercomTarget,
1606
1706
  childIntercomTarget,
1607
1707
  nestedRoute,
1708
+ timeoutMs: data.timeoutMs,
1709
+ globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
1608
1710
  });
1609
1711
  }
1610
1712
 
@@ -1640,15 +1742,19 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1640
1742
  skills,
1641
1743
  output: effectiveOutput,
1642
1744
  outputMode: effectiveOutputMode,
1745
+ outputBaseDir: resolveSingleRunOutputBaseDir(deps, artifactsDir, id),
1643
1746
  modelOverride,
1747
+ thinkingOverride: thinkingOverrideForTask(params.agent!, 0),
1644
1748
  maxSubagentDepth,
1645
1749
  worktreeSetupHook: deps.config.worktreeSetupHook,
1646
1750
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
1751
+ worktreeBaseDir: deps.config.worktreeBaseDir,
1647
1752
  controlConfig,
1648
1753
  controlIntercomTarget,
1649
1754
  childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(agent, index) : undefined,
1650
1755
  nestedRoute,
1651
1756
  acceptance: params.acceptance,
1757
+ timeoutMs: data.timeoutMs,
1652
1758
  });
1653
1759
  }
1654
1760
 
@@ -1667,6 +1773,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1667
1773
  sessionDirForIndex,
1668
1774
  sessionFileForIndex,
1669
1775
  sessionFileForTask,
1776
+ thinkingOverrideForTask,
1670
1777
  artifactsDir,
1671
1778
  artifactConfig,
1672
1779
  onUpdate,
@@ -1694,6 +1801,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1694
1801
  sessionDirForIndex,
1695
1802
  sessionFileForIndex,
1696
1803
  sessionFileForTask,
1804
+ thinkingOverrideForTask,
1697
1805
  artifactsDir,
1698
1806
  artifactConfig,
1699
1807
  includeProgress: params.includeProgress,
@@ -1706,19 +1814,18 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1706
1814
  foregroundControl,
1707
1815
  nestedRoute: foregroundControl?.nestedRoute,
1708
1816
  chainSkills,
1709
- chainDir: params.chainDir,
1817
+ chainDir: params.chainDir ?? getProjectChainRunsDir(effectiveCwd),
1710
1818
  dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
1711
1819
  maxSubagentDepth: currentMaxSubagentDepth,
1712
1820
  worktreeSetupHook: deps.config.worktreeSetupHook,
1713
1821
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
1822
+ worktreeBaseDir: deps.config.worktreeBaseDir,
1714
1823
  timeoutMs: data.timeoutMs,
1715
1824
  deadlineAt: data.deadlineAt,
1825
+ globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
1716
1826
  });
1717
1827
 
1718
1828
  if (chainResult.requestedAsync) {
1719
- if (data.timeoutMs !== undefined) {
1720
- return buildRequestedModeError(params, "timeoutMs/maxRuntimeMs are only supported for foreground runs; background launch from clarify cannot preserve the timeout.");
1721
- }
1722
1829
  if (!isAsyncAvailable()) {
1723
1830
  return {
1724
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." }],
@@ -1749,20 +1856,29 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1749
1856
  shareEnabled,
1750
1857
  sessionRoot,
1751
1858
  chainSkills: chainResult.requestedAsync.chainSkills,
1752
- sessionFilesByFlatIndex: collectChainSessionFiles(asyncChain, sessionFileForTask),
1859
+ sessionFilesByFlatIndex: collectChainSessionFiles(asyncChain, sessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems),
1860
+ thinkingOverridesByFlatIndex: collectChainThinkingOverrides(asyncChain, thinkingOverrideForTask, deps.config.chain?.dynamicFanout?.maxItems),
1753
1861
  dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
1754
1862
  maxSubagentDepth: currentMaxSubagentDepth,
1755
1863
  worktreeSetupHook: deps.config.worktreeSetupHook,
1756
1864
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
1865
+ worktreeBaseDir: deps.config.worktreeBaseDir,
1757
1866
  controlConfig,
1758
1867
  controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
1759
1868
  childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
1760
1869
  nestedRoute: data.nestedRoute,
1870
+ timeoutMs: data.timeoutMs,
1871
+ globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
1761
1872
  });
1762
1873
  }
1763
1874
 
1764
- const chainDetails = chainResult.details ? compactForegroundDetails({ ...chainResult.details, runId }) : undefined;
1765
- 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;
1766
1882
  if (chainDetails) rememberForegroundRun(deps.state, { runId, mode: "chain", cwd: effectiveCwd, results: chainDetails.results });
1767
1883
  const intercomReceipt = chainDetails && !chainDetails.results.some((result) => result.interrupted || result.detached)
1768
1884
  ? await maybeBuildForegroundIntercomReceipt({
@@ -1796,9 +1912,11 @@ interface ForegroundParallelRunInput {
1796
1912
  sessionDirForIndex: (idx?: number) => string | undefined;
1797
1913
  sessionFileForIndex: (idx?: number) => string | undefined;
1798
1914
  sessionFileForTask: (agentName: string, idx?: number) => string | undefined;
1915
+ thinkingOverrideForTask: (agentName: string, idx?: number) => AgentConfig["thinking"] | undefined;
1799
1916
  shareEnabled: boolean;
1800
1917
  artifactConfig: ArtifactConfig;
1801
1918
  artifactsDir: string;
1919
+ outputBaseDir: string;
1802
1920
  maxOutput?: MaxOutputConfig;
1803
1921
  paramsCwd: string;
1804
1922
  progressDir: string;
@@ -1813,6 +1931,7 @@ interface ForegroundParallelRunInput {
1813
1931
  orchestratorIntercomTarget?: string;
1814
1932
  foregroundControl?: SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never;
1815
1933
  concurrencyLimit: number;
1934
+ globalSemaphore?: Semaphore;
1816
1935
  liveResults: (SingleResult | undefined)[];
1817
1936
  liveProgress: (AgentProgress | undefined)[];
1818
1937
  onUpdate?: (r: AgentToolResult<Details>) => void;
@@ -1836,6 +1955,7 @@ function createParallelWorktreeSetup(
1836
1955
  tasks: TaskParam[],
1837
1956
  setupHook: ExtensionConfig["worktreeSetupHook"],
1838
1957
  setupHookTimeoutMs: ExtensionConfig["worktreeSetupHookTimeoutMs"],
1958
+ baseDir: ExtensionConfig["worktreeBaseDir"],
1839
1959
  ): { setup?: WorktreeSetup; errorResult?: AgentToolResult<Details> } {
1840
1960
  if (!enabled) return {};
1841
1961
  try {
@@ -1845,6 +1965,7 @@ function createParallelWorktreeSetup(
1845
1965
  setupHook: setupHook
1846
1966
  ? { hookPath: setupHook, timeoutMs: setupHookTimeoutMs }
1847
1967
  : undefined,
1968
+ baseDir,
1848
1969
  }),
1849
1970
  };
1850
1971
  } catch (error) {
@@ -1862,6 +1983,12 @@ function buildParallelWorktreeTaskCwdError(
1862
1983
  return formatWorktreeTaskCwdConflict(conflict, sharedCwd);
1863
1984
  }
1864
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
+
1865
1992
  function buildChainWorktreeTaskCwdError(chain: ChainStep[], sharedCwd: string): string | undefined {
1866
1993
  for (let stepIndex = 0; stepIndex < chain.length; stepIndex++) {
1867
1994
  const step = chain[stepIndex]!;
@@ -1901,6 +2028,7 @@ function findDuplicateParallelOutputPath(input: {
1901
2028
  behaviors: ResolvedStepBehavior[];
1902
2029
  paramsCwd: string;
1903
2030
  ctxCwd: string;
2031
+ outputBaseDir: string;
1904
2032
  worktreeSetup?: WorktreeSetup;
1905
2033
  }): string | undefined {
1906
2034
  const seen = new Map<string, { index: number; agent: string }>();
@@ -1909,7 +2037,7 @@ function findDuplicateParallelOutputPath(input: {
1909
2037
  if (!behavior?.output) continue;
1910
2038
  const task = input.tasks[index]!;
1911
2039
  const taskCwd = resolveParallelTaskCwd(task, input.paramsCwd, input.worktreeSetup, index);
1912
- const outputPath = resolveSingleOutputPath(behavior.output, input.ctxCwd, taskCwd);
2040
+ const outputPath = resolveSingleOutputPath(behavior.output, input.ctxCwd, taskCwd, input.outputBaseDir);
1913
2041
  if (!outputPath) continue;
1914
2042
  const previous = seen.get(outputPath);
1915
2043
  if (previous) {
@@ -1921,6 +2049,12 @@ function findDuplicateParallelOutputPath(input: {
1921
2049
  }
1922
2050
 
1923
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
+ }
1924
2058
  return mapConcurrent(input.tasks, input.concurrencyLimit, async (task, index) => {
1925
2059
  const behavior = input.behaviors[index];
1926
2060
  const effectiveSkills = behavior?.skills;
@@ -1931,7 +2065,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
1931
2065
  const progressInstructions = behavior
1932
2066
  ? buildChainInstructions({ ...behavior, output: false, reads: false }, input.progressDir, index === input.firstProgressIndex)
1933
2067
  : { prefix: "", suffix: "" };
1934
- const outputPath = resolveSingleOutputPath(behavior?.output, input.ctx.cwd, taskCwd);
2068
+ const outputPath = resolveSingleOutputPath(behavior?.output, input.ctx.cwd, taskCwd, input.outputBaseDir);
1935
2069
  const taskText = injectSingleOutputInstruction(
1936
2070
  `${readInstructions.prefix}${input.taskTexts[index]!}${progressInstructions.suffix}`,
1937
2071
  outputPath,
@@ -1975,6 +2109,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
1975
2109
  orchestratorIntercomTarget: input.orchestratorIntercomTarget,
1976
2110
  nestedRoute: input.foregroundControl?.nestedRoute,
1977
2111
  modelOverride: input.modelOverrides[index],
2112
+ thinkingOverride: input.thinkingOverrideForTask(task.agent, index),
1978
2113
  availableModels: input.availableModels,
1979
2114
  preferredModelProvider: input.ctx.model?.provider,
1980
2115
  skills: effectiveSkills === false ? [] : effectiveSkills,
@@ -2022,7 +2157,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
2022
2157
  input.foregroundControl.updatedAt = Date.now();
2023
2158
  }
2024
2159
  });
2025
- });
2160
+ }, input.globalSemaphore);
2026
2161
  }
2027
2162
 
2028
2163
  async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps): Promise<AgentToolResult<Details>> {
@@ -2036,6 +2171,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2036
2171
  sessionDirForIndex,
2037
2172
  sessionFileForIndex,
2038
2173
  sessionFileForTask,
2174
+ thinkingOverrideForTask,
2039
2175
  shareEnabled,
2040
2176
  artifactConfig,
2041
2177
  artifactsDir,
@@ -2146,9 +2282,6 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2146
2282
  }
2147
2283
 
2148
2284
  if (result.runInBackground) {
2149
- if (data.timeoutMs !== undefined) {
2150
- return buildRequestedModeError(params, "timeoutMs/maxRuntimeMs are only supported for foreground runs; background launch from clarify cannot preserve the timeout.");
2151
- }
2152
2285
  if (!isAsyncAvailable()) {
2153
2286
  return {
2154
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." }],
@@ -2195,12 +2328,16 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2195
2328
  sessionRoot,
2196
2329
  chainSkills: [],
2197
2330
  sessionFilesByFlatIndex: tasks.map((task, index) => sessionFileForTask(task.agent, index)),
2331
+ thinkingOverridesByFlatIndex: tasks.map((task, index) => thinkingOverrideForTask(task.agent, index)),
2198
2332
  maxSubagentDepth: currentMaxSubagentDepth,
2199
2333
  worktreeSetupHook: deps.config.worktreeSetupHook,
2200
2334
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
2335
+ worktreeBaseDir: deps.config.worktreeBaseDir,
2201
2336
  controlConfig,
2202
2337
  controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
2203
2338
  childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
2339
+ timeoutMs: data.timeoutMs,
2340
+ globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2204
2341
  });
2205
2342
  }
2206
2343
  }
@@ -2217,21 +2354,24 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2217
2354
  tasks,
2218
2355
  deps.config.worktreeSetupHook,
2219
2356
  deps.config.worktreeSetupHookTimeoutMs,
2357
+ deps.config.worktreeBaseDir,
2220
2358
  );
2221
2359
  if (errorResult) return errorResult;
2222
2360
 
2223
2361
  try {
2362
+ const outputBaseDir = path.join(artifactsDir, "outputs", runId);
2224
2363
  const duplicateOutputError = findDuplicateParallelOutputPath({
2225
2364
  tasks,
2226
2365
  behaviors,
2227
2366
  paramsCwd: effectiveCwd,
2228
2367
  ctxCwd: ctx.cwd,
2368
+ outputBaseDir,
2229
2369
  worktreeSetup,
2230
2370
  });
2231
2371
  if (duplicateOutputError) return buildParallelModeError(duplicateOutputError);
2232
2372
  for (let index = 0; index < tasks.length; index++) {
2233
2373
  const taskCwd = resolveParallelTaskCwd(tasks[index]!, effectiveCwd, worktreeSetup, index);
2234
- const outputPath = resolveSingleOutputPath(behaviors[index]?.output, ctx.cwd, taskCwd);
2374
+ const outputPath = resolveSingleOutputPath(behaviors[index]?.output, ctx.cwd, taskCwd, outputBaseDir);
2235
2375
  const validationError = validateFileOnlyOutputMode(behaviors[index]?.outputMode, outputPath, `Parallel task ${index + 1} (${tasks[index]!.agent})`);
2236
2376
  if (validationError) return buildParallelModeError(validationError);
2237
2377
  }
@@ -2256,9 +2396,11 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2256
2396
  sessionDirForIndex,
2257
2397
  sessionFileForIndex,
2258
2398
  sessionFileForTask,
2399
+ thinkingOverrideForTask,
2259
2400
  shareEnabled,
2260
2401
  artifactConfig,
2261
2402
  artifactsDir,
2403
+ outputBaseDir,
2262
2404
  maxOutput: params.maxOutput,
2263
2405
  paramsCwd: effectiveCwd,
2264
2406
  progressDir: parallelProgressDir,
@@ -2272,6 +2414,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2272
2414
  orchestratorIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
2273
2415
  foregroundControl,
2274
2416
  concurrencyLimit: parallelConcurrency,
2417
+ globalSemaphore: new Semaphore(deps.config.globalConcurrencyLimit ?? DEFAULT_GLOBAL_CONCURRENCY_LIMIT),
2275
2418
  maxSubagentDepths,
2276
2419
  liveResults,
2277
2420
  liveProgress,
@@ -2290,6 +2433,10 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2290
2433
  if (result.artifactPaths) allArtifactPaths.push(result.artifactPaths);
2291
2434
  }
2292
2435
 
2436
+ if (foregroundControl) {
2437
+ updateForegroundNestedProjection(foregroundControl);
2438
+ attachRootChildrenToSteps(runId, results, foregroundControl.nestedChildren);
2439
+ }
2293
2440
  const interrupted = results.find((result) => result.interrupted);
2294
2441
  const details = compactForegroundDetails({
2295
2442
  mode: "parallel",
@@ -2297,6 +2444,8 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2297
2444
  results,
2298
2445
  progress: params.includeProgress ? allProgress : undefined,
2299
2446
  artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
2447
+ totalChildUsage: sumResultsUsage(results),
2448
+ totalCost: sumResultsCost(results),
2300
2449
  });
2301
2450
  rememberForegroundRun(deps.state, { runId, mode: "parallel", cwd: effectiveCwd, results: details.results });
2302
2451
  if (interrupted) {
@@ -2368,6 +2517,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2368
2517
  runId,
2369
2518
  sessionDirForIndex,
2370
2519
  sessionFileForTask,
2520
+ thinkingOverrideForTask,
2371
2521
  shareEnabled,
2372
2522
  artifactConfig,
2373
2523
  artifactsDir,
@@ -2438,9 +2588,6 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2438
2588
  if (override?.skills !== undefined) skillOverride = override.skills;
2439
2589
 
2440
2590
  if (result.runInBackground) {
2441
- if (data.timeoutMs !== undefined) {
2442
- return buildRequestedModeError(params, "timeoutMs/maxRuntimeMs are only supported for foreground runs; background launch from clarify cannot preserve the timeout.");
2443
- }
2444
2591
  if (!isAsyncAvailable()) {
2445
2592
  return {
2446
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." }],
@@ -2473,13 +2620,17 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2473
2620
  skills: skillOverride === false ? [] : skillOverride,
2474
2621
  output: effectiveOutput,
2475
2622
  outputMode: effectiveOutputMode,
2623
+ outputBaseDir: resolveSingleRunOutputBaseDir(deps, artifactsDir, id),
2476
2624
  modelOverride,
2625
+ thinkingOverride: thinkingOverrideForTask(params.agent!, 0),
2477
2626
  maxSubagentDepth,
2478
2627
  worktreeSetupHook: deps.config.worktreeSetupHook,
2479
2628
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
2629
+ worktreeBaseDir: deps.config.worktreeBaseDir,
2480
2630
  controlConfig,
2481
2631
  controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
2482
2632
  childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
2633
+ timeoutMs: data.timeoutMs,
2483
2634
  });
2484
2635
  }
2485
2636
  }
@@ -2488,7 +2639,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2488
2639
  task = wrapForkTask(task);
2489
2640
  }
2490
2641
  const cleanTask = task;
2491
- const outputPath = resolveSingleOutputPath(effectiveOutput, ctx.cwd, effectiveCwd);
2642
+ const outputPath = resolveSingleOutputPath(effectiveOutput, ctx.cwd, effectiveCwd, resolveSingleRunOutputBaseDir(deps, artifactsDir, runId));
2492
2643
  const validationError = validateFileOnlyOutputMode(effectiveOutputMode, outputPath, `Single run (${params.agent})`);
2493
2644
  if (validationError) {
2494
2645
  return { content: [{ type: "text", text: validationError }], isError: true, details: { mode: "single", results: [] } };
@@ -2563,6 +2714,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2563
2714
  nestedRoute: foregroundControl?.nestedRoute,
2564
2715
  index: 0,
2565
2716
  modelOverride,
2717
+ thinkingOverride: thinkingOverrideForTask(params.agent!, 0),
2566
2718
  availableModels,
2567
2719
  preferredModelProvider: currentProvider,
2568
2720
  skills: effectiveSkills,
@@ -2599,6 +2751,10 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2599
2751
  outputReference: r.outputReference,
2600
2752
  saveError: r.outputSaveError,
2601
2753
  });
2754
+ if (foregroundControl) {
2755
+ updateForegroundNestedProjection(foregroundControl);
2756
+ attachRootChildrenToSteps(runId, [r], foregroundControl.nestedChildren);
2757
+ }
2602
2758
  const details = compactForegroundDetails({
2603
2759
  mode: "single",
2604
2760
  runId,
@@ -2606,6 +2762,8 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2606
2762
  progress: params.includeProgress ? allProgress : undefined,
2607
2763
  artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
2608
2764
  truncation: r.truncation,
2765
+ totalChildUsage: sumResultsUsage([r]),
2766
+ totalCost: sumResultsCost([r]),
2609
2767
  });
2610
2768
  rememberForegroundRun(deps.state, { runId, mode: "single", cwd: effectiveCwd, results: details.results });
2611
2769
 
@@ -2724,6 +2882,12 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2724
2882
  orchestratorTarget,
2725
2883
  sessionError,
2726
2884
  expandTilde: deps.expandTilde,
2885
+ companionPackageLines: deps.companionSuggestionLines?.({
2886
+ surface: "doctor",
2887
+ cwd: requestCwd,
2888
+ context: paramsWithResolvedCwd.context,
2889
+ orchestratorTarget,
2890
+ }),
2727
2891
  }),
2728
2892
  }],
2729
2893
  details: { mode: "management", results: [] },
@@ -2784,7 +2948,12 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2784
2948
  details: { mode: "management", results: [] },
2785
2949
  };
2786
2950
  }
2787
- const asyncInterruptResult = interruptAsyncRun(deps.state, resolved?.kind === "async" ? resolved.id : targetRunId, deps.kill);
2951
+ const asyncInterruptResult = interruptAsyncRun(
2952
+ deps.state,
2953
+ resolved?.kind === "async" ? resolved.id : targetRunId,
2954
+ deps.kill,
2955
+ resolved?.kind === "async" ? resolved.location : undefined,
2956
+ );
2788
2957
  if (asyncInterruptResult) return asyncInterruptResult;
2789
2958
  return {
2790
2959
  content: [{ type: "text", text: "No interrupt-capable run found in this session." }],
@@ -2806,7 +2975,21 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2806
2975
  details: { mode: "management" as const, results: [] },
2807
2976
  };
2808
2977
  }
2809
- 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
+ });
2810
2993
  }
2811
2994
 
2812
2995
  const { blocked, depth, maxDepth } = checkSubagentDepth(deps.config.maxSubagentDepth);
@@ -2879,24 +3062,24 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2879
3062
  if (validationError) return validationError;
2880
3063
 
2881
3064
  let forkSessionFileForIndex: (idx?: number) => string | undefined = () => undefined;
3065
+ let forkThinkingOverrideForIndex: (idx?: number) => AgentConfig["thinking"] | undefined = () => undefined;
2882
3066
  try {
2883
- 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;
2884
3070
  } catch (error) {
2885
3071
  return toExecutionErrorResult(effectiveParams, error);
2886
3072
  }
2887
3073
  const requestedAsync = effectiveParams.async ?? deps.asyncByDefault;
2888
3074
  const backgroundRequestedWhileClarifying = (hasChain || hasTasks) && requestedAsync && effectiveParams.clarify === true;
2889
3075
  const effectiveAsync = requestedAsync && effectiveParams.clarify !== true;
2890
- if (foregroundTimeout.timeoutMs !== undefined && effectiveAsync) {
2891
- return buildRequestedModeError(effectiveParams, "timeoutMs/maxRuntimeMs are only supported for foreground runs; set async: false or omit the timeout for background runs.");
2892
- }
2893
3076
  const controlConfig = resolveControlConfig(deps.config.control, effectiveParams.control);
2894
3077
 
2895
3078
  const artifactConfig: ArtifactConfig = {
2896
3079
  ...DEFAULT_ARTIFACT_CONFIG,
2897
3080
  enabled: effectiveParams.artifacts !== false,
2898
3081
  };
2899
- const artifactsDir = effectiveAsync ? deps.tempArtifactsDir : getArtifactsDir(parentSessionFile);
3082
+ const artifactsDir = getArtifactsDir(parentSessionFile, effectiveCwd);
2900
3083
 
2901
3084
  let sessionRoot: string;
2902
3085
  if (effectiveParams.sessionDir) {
@@ -2920,12 +3103,14 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2920
3103
  path.join(sessionRoot, `run-${idx ?? 0}`);
2921
3104
  const forkSessionFileForTask = (agentName: string, idx?: number) =>
2922
3105
  shouldForkAgent(contextPolicy, agentName) ? forkSessionFileForIndex(idx) : undefined;
3106
+ const forkThinkingOverrideForTask = (agentName: string, idx?: number) =>
3107
+ shouldForkAgent(contextPolicy, agentName) ? forkThinkingOverrideForIndex(idx) : undefined;
2923
3108
  const childSessionFileForTask = (agentName: string, idx?: number) =>
2924
3109
  forkSessionFileForTask(agentName, idx) ?? path.join(sessionDirForIndex(idx), "session.jsonl");
2925
3110
  const childSessionFileForIndex = (idx?: number) =>
2926
3111
  path.join(sessionDirForIndex(idx), "session.jsonl");
2927
3112
  try {
2928
- preflightForkSessionsForStaticTasks(effectiveParams, contextPolicy, forkSessionFileForTask);
3113
+ preflightForkSessionsForStaticTasks(effectiveParams, contextPolicy, forkSessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems);
2929
3114
  } catch (error) {
2930
3115
  return toExecutionErrorResult(effectiveParams, error);
2931
3116
  }
@@ -2936,6 +3121,16 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2936
3121
  ? (r: AgentToolResult<Details>) => onUpdate(withForkContext(r, effectiveParams.context))
2937
3122
  : undefined;
2938
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
+
2939
3134
  const execData: ExecutionContextData = {
2940
3135
  params: effectiveParams,
2941
3136
  effectiveCwd,
@@ -2949,6 +3144,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2949
3144
  sessionDirForIndex,
2950
3145
  sessionFileForIndex: childSessionFileForIndex,
2951
3146
  sessionFileForTask: childSessionFileForTask,
3147
+ thinkingOverrideForTask: forkThinkingOverrideForTask,
2952
3148
  artifactConfig,
2953
3149
  artifactsDir,
2954
3150
  backgroundRequestedWhileClarifying,
@@ -2960,7 +3156,6 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2960
3156
  contextPolicy,
2961
3157
  };
2962
3158
 
2963
- const foregroundMode: "single" | "parallel" | "chain" = hasChain ? "chain" : hasTasks ? "parallel" : "single";
2964
3159
  const foregroundControl = effectiveAsync
2965
3160
  ? undefined
2966
3161
  : {
@@ -3024,6 +3219,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3024
3219
  startedAt: foregroundControl?.startedAt ?? now,
3025
3220
  ...(state !== "running" ? { endedAt: now } : {}),
3026
3221
  lastUpdate: now,
3222
+ ...(details?.totalCost ? { totalCost: details.totalCost } : {}),
3027
3223
  ...(errorText ? { error: errorText } : {}),
3028
3224
  ...(details?.results.length ? { steps: details.results.map((child) => ({
3029
3225
  agent: child.agent,