pi-subagents 0.30.0 → 0.31.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 +26 -0
  2. package/README.md +116 -17
  3. package/agents/context-builder.md +3 -3
  4. package/agents/planner.md +1 -1
  5. package/agents/researcher.md +1 -1
  6. package/agents/scout.md +1 -1
  7. package/package.json +7 -7
  8. package/skills/pi-subagents/SKILL.md +5 -0
  9. package/src/agents/agent-management.ts +170 -6
  10. package/src/agents/agent-serializer.ts +31 -13
  11. package/src/agents/agents.ts +207 -23
  12. package/src/agents/frontmatter.ts +66 -2
  13. package/src/agents/skills.ts +117 -20
  14. package/src/extension/doctor.ts +20 -0
  15. package/src/extension/fanout-child.ts +1 -0
  16. package/src/extension/index.ts +47 -4
  17. package/src/extension/schemas.ts +10 -76
  18. package/src/intercom/intercom-bridge.ts +2 -3
  19. package/src/runs/background/async-execution.ts +14 -4
  20. package/src/runs/background/async-job-tracker.ts +56 -11
  21. package/src/runs/background/result-watcher.ts +11 -2
  22. package/src/runs/background/stale-run-reconciler.ts +9 -4
  23. package/src/runs/background/subagent-runner.ts +79 -3
  24. package/src/runs/foreground/chain-execution.ts +26 -2
  25. package/src/runs/foreground/execution.ts +113 -8
  26. package/src/runs/foreground/subagent-executor.ts +325 -77
  27. package/src/runs/shared/acceptance.ts +285 -34
  28. package/src/runs/shared/completion-guard.ts +1 -1
  29. package/src/runs/shared/dynamic-fanout.ts +4 -2
  30. package/src/runs/shared/mcp-direct-tool-allowlist.ts +2 -2
  31. package/src/runs/shared/parallel-utils.ts +6 -1
  32. package/src/runs/shared/pi-args.ts +9 -1
  33. package/src/runs/shared/single-output.ts +15 -1
  34. package/src/shared/settings.ts +1 -0
  35. package/src/shared/types.ts +8 -2
  36. package/src/shared/utils.ts +19 -1
  37. package/src/slash/prompt-template-bridge.ts +26 -3
  38. package/src/slash/slash-commands.ts +33 -3
  39. package/src/tui/render.ts +265 -13
@@ -34,6 +34,7 @@ import { discoverAvailableSkills, normalizeSkillInput } from "../../agents/skill
34
34
  import { buildAsyncRunnerSteps, executeAsyncChain, executeAsyncSingle, formatAsyncStartedMessage, isAsyncAvailable } from "../background/async-execution.ts";
35
35
  import { enqueueChainAppendRequest, readPendingChainAppendRequests, runnerStepOutputNames } from "../background/chain-append.ts";
36
36
  import { ChainOutputValidationError, validateChainOutputBindingsWithContext } from "../shared/chain-outputs.ts";
37
+ import { validateAcceptanceInput } from "../shared/acceptance.ts";
37
38
  import { createForkContextResolver } from "../../shared/fork-context.ts";
38
39
  import { resolveCurrentSessionId } from "../../shared/session-identity.ts";
39
40
  import { applyIntercomBridgeToAgent, INTERCOM_BRIDGE_MARKER, resolveIntercomBridge, resolveIntercomSessionTarget, resolveSubagentIntercomTarget, type IntercomBridgeState } from "../../intercom/intercom-bridge.ts";
@@ -128,6 +129,8 @@ export interface SubagentParamsLike {
128
129
  worktree?: boolean;
129
130
  context?: "fresh" | "fork";
130
131
  async?: boolean;
132
+ timeoutMs?: number;
133
+ maxRuntimeMs?: number;
131
134
  clarify?: boolean;
132
135
  share?: boolean;
133
136
  control?: ControlConfig;
@@ -170,6 +173,7 @@ interface ExecutionContextData {
170
173
  sessionRoot: string;
171
174
  sessionDirForIndex: (idx?: number) => string;
172
175
  sessionFileForIndex: (idx?: number) => string | undefined;
176
+ sessionFileForTask: (agentName: string, idx?: number) => string | undefined;
173
177
  artifactConfig: ArtifactConfig;
174
178
  artifactsDir: string;
175
179
  backgroundRequestedWhileClarifying: boolean;
@@ -177,6 +181,9 @@ interface ExecutionContextData {
177
181
  controlConfig: ResolvedControlConfig;
178
182
  intercomBridge: IntercomBridgeState;
179
183
  nestedRoute?: NestedRouteInfo;
184
+ timeoutMs?: number;
185
+ deadlineAt?: number;
186
+ contextPolicy: AgentDefaultContextPolicy;
180
187
  }
181
188
 
182
189
  function resolveRequestedCwd(runtimeCwd: string, requestedCwd: string | undefined): string {
@@ -471,6 +478,14 @@ function appendStepToAsyncChain(input: {
471
478
  details: { mode: "management", results: [] },
472
479
  };
473
480
  }
481
+ const acceptanceErrors = validateExecutionAcceptance(input.params);
482
+ if (acceptanceErrors.length > 0) {
483
+ return {
484
+ content: [{ type: "text", text: `Cannot append step: ${acceptanceErrors.join(" ")}` }],
485
+ isError: true,
486
+ details: { mode: "management", results: [] },
487
+ };
488
+ }
474
489
 
475
490
  let resolved: ResolvedSubagentRunId | undefined;
476
491
  try {
@@ -547,17 +562,19 @@ function appendStepToAsyncChain(input: {
547
562
 
548
563
  const scope: AgentScope = resolveExecutionAgentScope(input.params.agentScope);
549
564
  const agents = input.deps.discoverAgents(input.requestCwd, scope).agents;
565
+ const contextPolicy = resolveExplicitContextPolicy(input.params);
550
566
  const chainSkillInput = normalizeSkillInput(input.params.skill);
551
567
  const chainSkills = chainSkillInput === false ? [] : (chainSkillInput ?? []);
552
568
  const asyncCtx = {
553
569
  pi: input.deps.pi,
554
570
  cwd: input.ctx.cwd,
555
571
  currentSessionId: resolveCurrentSessionId(input.ctx.sessionManager),
572
+ parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
556
573
  currentModelProvider: input.ctx.model?.provider,
557
574
  currentModel: input.ctx.model,
558
575
  };
559
576
  const built = buildAsyncRunnerSteps(resolved.id, {
560
- chain: wrapChainTasksForFork(input.params.chain, input.params.context),
577
+ chain: wrapChainTasksForFork(input.params.chain, contextPolicy),
561
578
  task: input.params.task,
562
579
  resultMode: "chain",
563
580
  agents,
@@ -861,7 +878,8 @@ async function resumeAsyncRun(input: {
861
878
  const runId = randomUUID().slice(0, 8);
862
879
  const artifactConfig: ArtifactConfig = { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false };
863
880
  const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo);
864
- const chain = wrapChainTasksForFork(attachChain, input.params.context);
881
+ const contextPolicy = resolveExplicitContextPolicy(input.params);
882
+ const chain = wrapChainTasksForFork(attachChain, contextPolicy);
865
883
  const normalized = normalizeSkillInput(input.params.skill);
866
884
  const result = executeAsyncChain(runId, {
867
885
  chain,
@@ -879,6 +897,7 @@ async function resumeAsyncRun(input: {
879
897
  pi: input.deps.pi,
880
898
  cwd: input.requestCwd,
881
899
  currentSessionId: input.deps.state.currentSessionId,
900
+ parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
882
901
  currentModelProvider: input.ctx.model?.provider,
883
902
  currentModel: input.ctx.model,
884
903
  },
@@ -921,6 +940,7 @@ async function resumeAsyncRun(input: {
921
940
  pi: input.deps.pi,
922
941
  cwd: input.requestCwd,
923
942
  currentSessionId: input.deps.state.currentSessionId,
943
+ parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
924
944
  currentModelProvider: input.ctx.model?.provider,
925
945
  currentModel: input.ctx.model,
926
946
  },
@@ -1068,6 +1088,15 @@ function validateExecutionInput(
1068
1088
  };
1069
1089
  }
1070
1090
 
1091
+ const acceptanceErrors = validateExecutionAcceptance(params);
1092
+ if (acceptanceErrors.length > 0) {
1093
+ return {
1094
+ content: [{ type: "text", text: acceptanceErrors.join(" ") }],
1095
+ isError: true,
1096
+ details: { mode: getRequestedModeLabel(params), results: [] },
1097
+ };
1098
+ }
1099
+
1071
1100
  if (hasSingle && params.agent && !agents.find((agent) => agent.name === params.agent)) {
1072
1101
  return {
1073
1102
  content: [{ type: "text", text: `Unknown agent: ${params.agent}` }],
@@ -1145,6 +1174,42 @@ function validateExecutionInput(
1145
1174
  return null;
1146
1175
  }
1147
1176
 
1177
+ function validateExecutionChainBindings(params: SubagentParamsLike, dynamicFanoutMaxItems?: number): AgentToolResult<Details> | null {
1178
+ if ((params.chain?.length ?? 0) === 0) return null;
1179
+ try {
1180
+ validateChainOutputBindingsWithContext(params.chain as ChainStep[], { maxItems: dynamicFanoutMaxItems });
1181
+ } catch (error) {
1182
+ if (error instanceof ChainOutputValidationError) {
1183
+ return {
1184
+ content: [{ type: "text", text: error.message }],
1185
+ isError: true,
1186
+ details: { mode: "chain" as const, results: [] },
1187
+ };
1188
+ }
1189
+ throw error;
1190
+ }
1191
+ return null;
1192
+ }
1193
+
1194
+ function validateExecutionAcceptance(params: SubagentParamsLike): string[] {
1195
+ const errors: string[] = [];
1196
+ errors.push(...validateAcceptanceInput(params.acceptance, "acceptance"));
1197
+ for (const [index, task] of (params.tasks ?? []).entries()) {
1198
+ errors.push(...validateAcceptanceInput(task.acceptance, `tasks[${index}].acceptance`));
1199
+ }
1200
+ for (const [stepIndex, step] of (params.chain ?? []).entries()) {
1201
+ errors.push(...validateAcceptanceInput((step as { acceptance?: unknown }).acceptance, `chain[${stepIndex}].acceptance`));
1202
+ if (isParallelStep(step)) {
1203
+ for (const [taskIndex, task] of step.parallel.entries()) {
1204
+ errors.push(...validateAcceptanceInput(task.acceptance, `chain[${stepIndex}].parallel[${taskIndex}].acceptance`));
1205
+ }
1206
+ } else if (isDynamicParallelStep(step)) {
1207
+ errors.push(...validateAcceptanceInput(step.parallel.acceptance, `chain[${stepIndex}].parallel.acceptance`));
1208
+ }
1209
+ }
1210
+ return errors;
1211
+ }
1212
+
1148
1213
  function getRequestedModeLabel(params: SubagentParamsLike): Details["mode"] {
1149
1214
  if ((params.chain?.length ?? 0) > 0) return "chain";
1150
1215
  if ((params.tasks?.length ?? 0) > 0) return "parallel";
@@ -1152,16 +1217,46 @@ function getRequestedModeLabel(params: SubagentParamsLike): Details["mode"] {
1152
1217
  return "single";
1153
1218
  }
1154
1219
 
1155
- function applyAgentDefaultContext(params: SubagentParamsLike, agents: AgentConfig[]): SubagentParamsLike {
1156
- if (params.context !== undefined) return params;
1220
+ interface AgentDefaultContextPolicy {
1221
+ params: SubagentParamsLike;
1222
+ contextForAgent(agentName: string): "fresh" | "fork";
1223
+ usesFork: boolean;
1224
+ }
1225
+
1226
+ function resolveAgentDefaultContextPolicy(params: SubagentParamsLike, agents: AgentConfig[]): AgentDefaultContextPolicy {
1227
+ if (params.context !== undefined) {
1228
+ return resolveExplicitContextPolicy(params);
1229
+ }
1157
1230
  const byName = new Map(agents.map((agent) => [agent.name, agent]));
1231
+ const contextForAgent = (agentName: string): "fresh" | "fork" =>
1232
+ byName.get(agentName)?.defaultContext === "fork" ? "fork" : "fresh";
1233
+ const usesFork = collectRequestedAgentNames(params).some((name) => contextForAgent(name) === "fork");
1234
+ return {
1235
+ params: usesFork ? { ...params, context: "fork" } : params,
1236
+ contextForAgent,
1237
+ usesFork,
1238
+ };
1239
+ }
1240
+
1241
+ function resolveExplicitContextPolicy(params: SubagentParamsLike): AgentDefaultContextPolicy {
1242
+ const context = params.context === "fork" ? "fork" : "fresh";
1243
+ return {
1244
+ params,
1245
+ contextForAgent: () => context,
1246
+ usesFork: context === "fork",
1247
+ };
1248
+ }
1249
+
1250
+ function collectRequestedAgentNames(params: SubagentParamsLike): string[] {
1158
1251
  const names: string[] = [];
1159
1252
  if (params.agent) names.push(params.agent);
1160
1253
  for (const task of params.tasks ?? []) names.push(task.agent);
1161
1254
  for (const step of params.chain ?? []) names.push(...getStepAgents(step));
1162
- return names.some((name) => byName.get(name)?.defaultContext === "fork")
1163
- ? { ...params, context: "fork" }
1164
- : params;
1255
+ return names;
1256
+ }
1257
+
1258
+ function shouldForkAgent(contextPolicy: AgentDefaultContextPolicy, agentName: string): boolean {
1259
+ return contextPolicy.contextForAgent(agentName) === "fork";
1165
1260
  }
1166
1261
 
1167
1262
  function buildRequestedModeError(params: SubagentParamsLike, message: string): AgentToolResult<Details> {
@@ -1175,6 +1270,22 @@ function buildRequestedModeError(params: SubagentParamsLike, message: string): A
1175
1270
  );
1176
1271
  }
1177
1272
 
1273
+ function resolveForegroundTimeout(params: SubagentParamsLike): { timeoutMs?: number; error?: string } {
1274
+ const rawTimeout = params.timeoutMs;
1275
+ const rawMaxRuntime = params.maxRuntimeMs;
1276
+ if (rawTimeout === undefined && rawMaxRuntime === undefined) return {};
1277
+ for (const [name, value] of [["timeoutMs", rawTimeout], ["maxRuntimeMs", rawMaxRuntime]] as const) {
1278
+ if (value === undefined) continue;
1279
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
1280
+ return { error: `${name} must be a positive integer.` };
1281
+ }
1282
+ }
1283
+ if (rawTimeout !== undefined && rawMaxRuntime !== undefined && rawTimeout !== rawMaxRuntime) {
1284
+ return { error: "timeoutMs and maxRuntimeMs are aliases; provide only one value or use the same value for both." };
1285
+ }
1286
+ return { timeoutMs: rawTimeout ?? rawMaxRuntime };
1287
+ }
1288
+
1178
1289
  function expandTopLevelTaskCounts(tasks: TaskParam[]): { tasks?: TaskParam[]; error?: string } {
1179
1290
  const expanded: TaskParam[] = [];
1180
1291
  for (let taskIndex = 0; taskIndex < tasks.length; taskIndex++) {
@@ -1262,14 +1373,14 @@ function toExecutionErrorResult(params: SubagentParamsLike, error: unknown): Age
1262
1373
 
1263
1374
  function collectChainSessionFiles(
1264
1375
  chain: ChainStep[],
1265
- sessionFileForIndex: (idx?: number) => string | undefined,
1376
+ sessionFileForTask: (agentName: string, idx?: number) => string | undefined,
1266
1377
  ): (string | undefined)[] {
1267
1378
  const sessionFiles: (string | undefined)[] = [];
1268
1379
  let flatIndex = 0;
1269
1380
  for (const step of chain) {
1270
1381
  if (isParallelStep(step)) {
1271
- for (let i = 0; i < step.parallel.length; i++) {
1272
- sessionFiles.push(sessionFileForIndex(flatIndex));
1382
+ for (const task of step.parallel) {
1383
+ sessionFiles.push(sessionFileForTask(task.agent, flatIndex));
1273
1384
  flatIndex++;
1274
1385
  }
1275
1386
  continue;
@@ -1278,21 +1389,22 @@ function collectChainSessionFiles(
1278
1389
  sessionFiles.push(undefined);
1279
1390
  continue;
1280
1391
  }
1281
- sessionFiles.push(sessionFileForIndex(flatIndex));
1392
+ sessionFiles.push(sessionFileForTask((step as SequentialStep).agent, flatIndex));
1282
1393
  flatIndex++;
1283
1394
  }
1284
1395
  return sessionFiles;
1285
1396
  }
1286
1397
 
1287
- function wrapChainTasksForFork(chain: ChainStep[], context: SubagentParamsLike["context"]): ChainStep[] {
1288
- if (context !== "fork") return chain;
1398
+ function wrapChainTasksForFork(chain: ChainStep[], contextPolicy: AgentDefaultContextPolicy): ChainStep[] {
1289
1399
  return chain.map((step, stepIndex) => {
1290
1400
  if (isParallelStep(step)) {
1291
1401
  return {
1292
1402
  ...step,
1293
1403
  parallel: step.parallel.map((task) => ({
1294
1404
  ...task,
1295
- task: wrapForkTask(task.task ?? "{previous}"),
1405
+ task: shouldForkAgent(contextPolicy, task.agent)
1406
+ ? wrapForkTask(task.task ?? "{previous}")
1407
+ : task.task,
1296
1408
  })),
1297
1409
  };
1298
1410
  }
@@ -1301,18 +1413,59 @@ function wrapChainTasksForFork(chain: ChainStep[], context: SubagentParamsLike["
1301
1413
  ...step,
1302
1414
  parallel: {
1303
1415
  ...step.parallel,
1304
- task: wrapForkTask(step.parallel.task ?? "{previous}"),
1416
+ task: shouldForkAgent(contextPolicy, step.parallel.agent)
1417
+ ? wrapForkTask(step.parallel.task ?? "{previous}")
1418
+ : step.parallel.task,
1305
1419
  },
1306
1420
  };
1307
1421
  }
1308
1422
  const sequential = step as SequentialStep;
1309
1423
  return {
1310
1424
  ...sequential,
1311
- task: wrapForkTask(sequential.task ?? (stepIndex === 0 ? "{task}" : "{previous}")),
1425
+ task: shouldForkAgent(contextPolicy, sequential.agent)
1426
+ ? wrapForkTask(sequential.task ?? (stepIndex === 0 ? "{task}" : "{previous}"))
1427
+ : sequential.task,
1312
1428
  };
1313
1429
  });
1314
1430
  }
1315
1431
 
1432
+ function preflightForkSessionsForStaticTasks(
1433
+ params: SubagentParamsLike,
1434
+ contextPolicy: AgentDefaultContextPolicy,
1435
+ sessionFileForTask: (agentName: string, idx?: number) => string | undefined,
1436
+ ): void {
1437
+ if (!contextPolicy.usesFork) return;
1438
+ if (params.agent) {
1439
+ if (shouldForkAgent(contextPolicy, params.agent)) sessionFileForTask(params.agent, 0);
1440
+ return;
1441
+ }
1442
+ if (params.tasks) {
1443
+ params.tasks.forEach((task, index) => {
1444
+ if (shouldForkAgent(contextPolicy, task.agent)) sessionFileForTask(task.agent, index);
1445
+ });
1446
+ return;
1447
+ }
1448
+ if (!params.chain?.length) return;
1449
+ let flatIndex = 0;
1450
+ for (const step of params.chain) {
1451
+ if (isParallelStep(step)) {
1452
+ for (const task of step.parallel) {
1453
+ if (shouldForkAgent(contextPolicy, task.agent)) sessionFileForTask(task.agent, flatIndex);
1454
+ flatIndex++;
1455
+ }
1456
+ continue;
1457
+ }
1458
+ if (isDynamicParallelStep(step)) {
1459
+ if (shouldForkAgent(contextPolicy, step.parallel.agent)) sessionFileForTask(step.parallel.agent, flatIndex);
1460
+ flatIndex++;
1461
+ continue;
1462
+ }
1463
+ const sequential = step as SequentialStep;
1464
+ if (shouldForkAgent(contextPolicy, sequential.agent)) sessionFileForTask(sequential.agent, flatIndex);
1465
+ flatIndex++;
1466
+ }
1467
+ }
1468
+
1316
1469
  function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentToolResult<Details> | null {
1317
1470
  const {
1318
1471
  params,
@@ -1322,12 +1475,14 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1322
1475
  shareEnabled,
1323
1476
  sessionRoot,
1324
1477
  sessionFileForIndex,
1478
+ sessionFileForTask,
1325
1479
  artifactConfig,
1326
1480
  artifactsDir,
1327
1481
  effectiveAsync,
1328
1482
  controlConfig,
1329
1483
  intercomBridge,
1330
1484
  nestedRoute,
1485
+ contextPolicy,
1331
1486
  } = data;
1332
1487
  const hasChain = (params.chain?.length ?? 0) > 0;
1333
1488
  const hasTasks = (params.tasks?.length ?? 0) > 0;
@@ -1368,6 +1523,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1368
1523
  pi: deps.pi,
1369
1524
  cwd: ctx.cwd,
1370
1525
  currentSessionId: deps.state.currentSessionId!,
1526
+ parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
1371
1527
  currentModelProvider: ctx.model?.provider,
1372
1528
  currentModel: ctx.model,
1373
1529
  };
@@ -1385,7 +1541,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1385
1541
  const skillOverrides = params.tasks.map((task) => normalizeSkillInput(task.skill));
1386
1542
  const parallelTasks = params.tasks.map((task, index) => ({
1387
1543
  agent: task.agent,
1388
- task: params.context === "fork" ? wrapForkTask(task.task) : task.task,
1544
+ task: shouldForkAgent(contextPolicy, task.agent) ? wrapForkTask(task.task) : task.task,
1389
1545
  cwd: task.cwd,
1390
1546
  ...(modelOverrides[index] ? { model: modelOverrides[index] } : {}),
1391
1547
  ...(skillOverrides[index] !== undefined ? { skill: skillOverrides[index] } : {}),
@@ -1412,7 +1568,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1412
1568
  shareEnabled,
1413
1569
  sessionRoot,
1414
1570
  chainSkills: [],
1415
- sessionFilesByFlatIndex: params.tasks.map((_, index) => sessionFileForIndex(index)),
1571
+ sessionFilesByFlatIndex: params.tasks.map((task, index) => sessionFileForTask(task.agent, index)),
1416
1572
  maxSubagentDepth: currentMaxSubagentDepth,
1417
1573
  worktreeSetupHook: deps.config.worktreeSetupHook,
1418
1574
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
@@ -1426,7 +1582,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1426
1582
  if (hasChain && params.chain) {
1427
1583
  const normalized = normalizeSkillInput(params.skill);
1428
1584
  const chainSkills = normalized === false ? [] : (normalized ?? []);
1429
- const chain = wrapChainTasksForFork(params.chain as ChainStep[], params.context);
1585
+ const chain = wrapChainTasksForFork(params.chain as ChainStep[], contextPolicy);
1430
1586
  return executeAsyncChain(id, {
1431
1587
  chain,
1432
1588
  task: params.task,
@@ -1440,7 +1596,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1440
1596
  shareEnabled,
1441
1597
  sessionRoot,
1442
1598
  chainSkills,
1443
- sessionFilesByFlatIndex: collectChainSessionFiles(chain, sessionFileForIndex),
1599
+ sessionFilesByFlatIndex: collectChainSessionFiles(chain, sessionFileForTask),
1444
1600
  dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
1445
1601
  maxSubagentDepth: currentMaxSubagentDepth,
1446
1602
  worktreeSetupHook: deps.config.worktreeSetupHook,
@@ -1470,7 +1626,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1470
1626
  const modelOverride = resolveSubagentModelOverride((params.model as string | undefined) ?? a.model, ctx.model, availableModels, currentProvider);
1471
1627
  return executeAsyncSingle(id, {
1472
1628
  agent: params.agent!,
1473
- task: params.context === "fork" ? wrapForkTask(params.task ?? "") : (params.task ?? ""),
1629
+ task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(params.task ?? "") : (params.task ?? ""),
1474
1630
  agentConfig: a,
1475
1631
  ctx: asyncCtx,
1476
1632
  availableModels,
@@ -1480,7 +1636,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1480
1636
  artifactConfig,
1481
1637
  shareEnabled,
1482
1638
  sessionRoot,
1483
- sessionFile: sessionFileForIndex(0),
1639
+ sessionFile: sessionFileForTask(params.agent!, 0),
1484
1640
  skills,
1485
1641
  output: effectiveOutput,
1486
1642
  outputMode: effectiveOutputMode,
@@ -1510,18 +1666,20 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1510
1666
  shareEnabled,
1511
1667
  sessionDirForIndex,
1512
1668
  sessionFileForIndex,
1669
+ sessionFileForTask,
1513
1670
  artifactsDir,
1514
1671
  artifactConfig,
1515
1672
  onUpdate,
1516
1673
  sessionRoot,
1517
1674
  controlConfig,
1675
+ contextPolicy,
1518
1676
  } = data;
1519
1677
  const onControlEvent = createForegroundControlNotifier(data, deps);
1520
1678
  const childIntercomTarget = data.intercomBridge.active ? resolveSubagentIntercomTarget : undefined;
1521
1679
  const foregroundControl = deps.state.foregroundControls.get(runId);
1522
1680
  const normalized = normalizeSkillInput(params.skill);
1523
1681
  const chainSkills = normalized === false ? [] : (normalized ?? []);
1524
- const chain = wrapChainTasksForFork(params.chain as ChainStep[], params.context);
1682
+ const chain = wrapChainTasksForFork(params.chain as ChainStep[], contextPolicy);
1525
1683
  const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
1526
1684
  const chainResult = await executeChain({
1527
1685
  chain,
@@ -1535,6 +1693,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1535
1693
  shareEnabled,
1536
1694
  sessionDirForIndex,
1537
1695
  sessionFileForIndex,
1696
+ sessionFileForTask,
1538
1697
  artifactsDir,
1539
1698
  artifactConfig,
1540
1699
  includeProgress: params.includeProgress,
@@ -1552,9 +1711,14 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1552
1711
  maxSubagentDepth: currentMaxSubagentDepth,
1553
1712
  worktreeSetupHook: deps.config.worktreeSetupHook,
1554
1713
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
1714
+ timeoutMs: data.timeoutMs,
1715
+ deadlineAt: data.deadlineAt,
1555
1716
  });
1556
1717
 
1557
1718
  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
+ }
1558
1722
  if (!isAsyncAvailable()) {
1559
1723
  return {
1560
1724
  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." }],
@@ -1567,10 +1731,11 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1567
1731
  pi: deps.pi,
1568
1732
  cwd: ctx.cwd,
1569
1733
  currentSessionId: deps.state.currentSessionId!,
1734
+ parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
1570
1735
  currentModelProvider: ctx.model?.provider,
1571
1736
  currentModel: ctx.model,
1572
1737
  };
1573
- const asyncChain = wrapChainTasksForFork(chainResult.requestedAsync.chain, params.context);
1738
+ const asyncChain = wrapChainTasksForFork(chainResult.requestedAsync.chain, contextPolicy);
1574
1739
  return executeAsyncChain(id, {
1575
1740
  chain: asyncChain,
1576
1741
  task: params.task,
@@ -1584,7 +1749,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1584
1749
  shareEnabled,
1585
1750
  sessionRoot,
1586
1751
  chainSkills: chainResult.requestedAsync.chainSkills,
1587
- sessionFilesByFlatIndex: collectChainSessionFiles(asyncChain, sessionFileForIndex),
1752
+ sessionFilesByFlatIndex: collectChainSessionFiles(asyncChain, sessionFileForTask),
1588
1753
  dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
1589
1754
  maxSubagentDepth: currentMaxSubagentDepth,
1590
1755
  worktreeSetupHook: deps.config.worktreeSetupHook,
@@ -1630,11 +1795,13 @@ interface ForegroundParallelRunInput {
1630
1795
  runId: string;
1631
1796
  sessionDirForIndex: (idx?: number) => string | undefined;
1632
1797
  sessionFileForIndex: (idx?: number) => string | undefined;
1798
+ sessionFileForTask: (agentName: string, idx?: number) => string | undefined;
1633
1799
  shareEnabled: boolean;
1634
1800
  artifactConfig: ArtifactConfig;
1635
1801
  artifactsDir: string;
1636
1802
  maxOutput?: MaxOutputConfig;
1637
1803
  paramsCwd: string;
1804
+ progressDir: string;
1638
1805
  maxSubagentDepths: number[];
1639
1806
  availableModels: ModelInfo[];
1640
1807
  modelOverrides: (string | undefined)[];
@@ -1650,6 +1817,8 @@ interface ForegroundParallelRunInput {
1650
1817
  liveProgress: (AgentProgress | undefined)[];
1651
1818
  onUpdate?: (r: AgentToolResult<Details>) => void;
1652
1819
  worktreeSetup?: WorktreeSetup;
1820
+ timeoutMs?: number;
1821
+ deadlineAt?: number;
1653
1822
  }
1654
1823
 
1655
1824
  function buildParallelModeError(message: string): AgentToolResult<Details> {
@@ -1760,7 +1929,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
1760
1929
  ? buildChainInstructions({ ...behavior, output: false, progress: false }, taskCwd, false)
1761
1930
  : { prefix: "", suffix: "" };
1762
1931
  const progressInstructions = behavior
1763
- ? buildChainInstructions({ ...behavior, output: false, reads: false }, input.paramsCwd, index === input.firstProgressIndex)
1932
+ ? buildChainInstructions({ ...behavior, output: false, reads: false }, input.progressDir, index === input.firstProgressIndex)
1764
1933
  : { prefix: "", suffix: "" };
1765
1934
  const outputPath = resolveSingleOutputPath(behavior?.output, input.ctx.cwd, taskCwd);
1766
1935
  const taskText = injectSingleOutputInstruction(
@@ -1783,6 +1952,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
1783
1952
  }
1784
1953
  const agentConfig = input.agents.find((agent) => agent.name === task.agent);
1785
1954
  return runSync(input.ctx.cwd, input.agents, task.agent, taskText, {
1955
+ parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
1786
1956
  cwd: taskCwd,
1787
1957
  signal: input.signal,
1788
1958
  interruptSignal: interruptController.signal,
@@ -1791,7 +1961,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
1791
1961
  runId: input.runId,
1792
1962
  index,
1793
1963
  sessionDir: input.sessionDirForIndex(index),
1794
- sessionFile: input.sessionFileForIndex(index),
1964
+ sessionFile: input.sessionFileForTask(task.agent, index),
1795
1965
  share: input.shareEnabled,
1796
1966
  artifactsDir: input.artifactConfig.enabled ? input.artifactsDir : undefined,
1797
1967
  artifactConfig: input.artifactConfig,
@@ -1810,39 +1980,41 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
1810
1980
  skills: effectiveSkills === false ? [] : effectiveSkills,
1811
1981
  acceptance: task.acceptance,
1812
1982
  acceptanceContext: { mode: "parallel" },
1813
- onUpdate: input.onUpdate
1814
- ? (progressUpdate) => {
1815
- const stepResults = progressUpdate.details?.results || [];
1816
- const stepProgress = progressUpdate.details?.progress || [];
1817
- if (input.foregroundControl && stepProgress.length > 0) {
1818
- const current = stepProgress[0];
1819
- input.foregroundControl.currentAgent = task.agent;
1820
- input.foregroundControl.currentIndex = index;
1821
- input.foregroundControl.currentActivityState = current?.activityState;
1822
- input.foregroundControl.lastActivityAt = current?.lastActivityAt;
1823
- input.foregroundControl.currentTool = current?.currentTool;
1824
- input.foregroundControl.currentToolStartedAt = current?.currentToolStartedAt;
1825
- input.foregroundControl.currentPath = current?.currentPath;
1826
- input.foregroundControl.turnCount = current?.turnCount;
1827
- input.foregroundControl.tokens = current?.tokens;
1828
- input.foregroundControl.toolCount = current?.toolCount;
1829
- input.foregroundControl.updatedAt = Date.now();
1830
- }
1831
- if (stepResults.length > 0) input.liveResults[index] = stepResults[0];
1832
- if (stepProgress.length > 0) input.liveProgress[index] = stepProgress[0];
1833
- const mergedResults = input.liveResults.filter((result): result is SingleResult => result !== undefined);
1834
- const mergedProgress = input.liveProgress.filter((progress): progress is AgentProgress => progress !== undefined);
1835
- input.onUpdate?.({
1836
- content: progressUpdate.content,
1837
- details: {
1838
- mode: "parallel",
1839
- results: mergedResults,
1840
- progress: mergedProgress,
1841
- controlEvents: progressUpdate.details?.controlEvents,
1842
- totalSteps: input.tasks.length,
1843
- },
1844
- });
1983
+ timeoutMs: input.timeoutMs,
1984
+ deadlineAt: input.deadlineAt,
1985
+ onUpdate: input.onUpdate
1986
+ ? (progressUpdate) => {
1987
+ const stepResults = progressUpdate.details?.results || [];
1988
+ const stepProgress = progressUpdate.details?.progress || [];
1989
+ if (input.foregroundControl && stepProgress.length > 0) {
1990
+ const current = stepProgress[0];
1991
+ input.foregroundControl.currentAgent = task.agent;
1992
+ input.foregroundControl.currentIndex = index;
1993
+ input.foregroundControl.currentActivityState = current?.activityState;
1994
+ input.foregroundControl.lastActivityAt = current?.lastActivityAt;
1995
+ input.foregroundControl.currentTool = current?.currentTool;
1996
+ input.foregroundControl.currentToolStartedAt = current?.currentToolStartedAt;
1997
+ input.foregroundControl.currentPath = current?.currentPath;
1998
+ input.foregroundControl.turnCount = current?.turnCount;
1999
+ input.foregroundControl.tokens = current?.tokens;
2000
+ input.foregroundControl.toolCount = current?.toolCount;
2001
+ input.foregroundControl.updatedAt = Date.now();
1845
2002
  }
2003
+ if (stepResults.length > 0) input.liveResults[index] = stepResults[0];
2004
+ if (stepProgress.length > 0) input.liveProgress[index] = stepProgress[0];
2005
+ const mergedResults = input.liveResults.filter((result): result is SingleResult => result !== undefined);
2006
+ const mergedProgress = input.liveProgress.filter((progress): progress is AgentProgress => progress !== undefined);
2007
+ input.onUpdate?.({
2008
+ content: progressUpdate.content,
2009
+ details: {
2010
+ mode: "parallel",
2011
+ results: mergedResults,
2012
+ progress: mergedProgress,
2013
+ controlEvents: progressUpdate.details?.controlEvents,
2014
+ totalSteps: input.tasks.length,
2015
+ },
2016
+ });
2017
+ }
1846
2018
  : undefined,
1847
2019
  }).finally(() => {
1848
2020
  if (input.foregroundControl?.currentIndex === index) {
@@ -1863,6 +2035,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1863
2035
  runId,
1864
2036
  sessionDirForIndex,
1865
2037
  sessionFileForIndex,
2038
+ sessionFileForTask,
1866
2039
  shareEnabled,
1867
2040
  artifactConfig,
1868
2041
  artifactsDir,
@@ -1870,6 +2043,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1870
2043
  onUpdate,
1871
2044
  sessionRoot,
1872
2045
  controlConfig,
2046
+ contextPolicy,
1873
2047
  } = data;
1874
2048
  const onControlEvent = createForegroundControlNotifier(data, deps);
1875
2049
  const childIntercomTarget = data.intercomBridge.active ? resolveSubagentIntercomTarget : undefined;
@@ -1972,6 +2146,9 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1972
2146
  }
1973
2147
 
1974
2148
  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
+ }
1975
2152
  if (!isAsyncAvailable()) {
1976
2153
  return {
1977
2154
  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." }],
@@ -1984,11 +2161,12 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1984
2161
  pi: deps.pi,
1985
2162
  cwd: ctx.cwd,
1986
2163
  currentSessionId: deps.state.currentSessionId!,
2164
+ parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
1987
2165
  currentModelProvider: ctx.model?.provider,
1988
2166
  currentModel: ctx.model,
1989
2167
  };
1990
2168
  const parallelTasks = tasks.map((t, i) => {
1991
- const taskText = params.context === "fork" ? wrapForkTask(taskTexts[i]!) : taskTexts[i]!;
2169
+ const taskText = shouldForkAgent(contextPolicy, t.agent) ? wrapForkTask(taskTexts[i]!) : taskTexts[i]!;
1992
2170
  const progress = taskDisallowsFileUpdates(taskText) ? false : behaviorOverrides[i]?.progress;
1993
2171
  return {
1994
2172
  agent: t.agent,
@@ -2016,7 +2194,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2016
2194
  shareEnabled,
2017
2195
  sessionRoot,
2018
2196
  chainSkills: [],
2019
- sessionFilesByFlatIndex: tasks.map((_, index) => sessionFileForIndex(index)),
2197
+ sessionFilesByFlatIndex: tasks.map((task, index) => sessionFileForTask(task.agent, index)),
2020
2198
  maxSubagentDepth: currentMaxSubagentDepth,
2021
2199
  worktreeSetupHook: deps.config.worktreeSetupHook,
2022
2200
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
@@ -2059,14 +2237,14 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2059
2237
  }
2060
2238
 
2061
2239
  const parallelProgressPrecreated = firstProgressIndex !== -1;
2062
- if (parallelProgressPrecreated) writeInitialProgressFile(effectiveCwd);
2240
+ const parallelProgressDir = path.join(artifactsDir, "progress", runId);
2241
+ if (parallelProgressPrecreated) writeInitialProgressFile(parallelProgressDir);
2063
2242
 
2064
- if (params.context === "fork") {
2065
- for (let i = 0; i < taskTexts.length; i++) {
2066
- taskTexts[i] = wrapForkTask(taskTexts[i]!);
2067
- }
2243
+ for (let i = 0; i < taskTexts.length; i++) {
2244
+ if (shouldForkAgent(contextPolicy, tasks[i]!.agent)) taskTexts[i] = wrapForkTask(taskTexts[i]!);
2068
2245
  }
2069
2246
 
2247
+ const deadlineAt = data.deadlineAt ?? (data.timeoutMs !== undefined ? Date.now() + data.timeoutMs : undefined);
2070
2248
  const results = await runForegroundParallelTasks({
2071
2249
  tasks,
2072
2250
  taskTexts,
@@ -2077,11 +2255,13 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2077
2255
  runId,
2078
2256
  sessionDirForIndex,
2079
2257
  sessionFileForIndex,
2258
+ sessionFileForTask,
2080
2259
  shareEnabled,
2081
2260
  artifactConfig,
2082
2261
  artifactsDir,
2083
2262
  maxOutput: params.maxOutput,
2084
2263
  paramsCwd: effectiveCwd,
2264
+ progressDir: parallelProgressDir,
2085
2265
  availableModels,
2086
2266
  modelOverrides,
2087
2267
  behaviors,
@@ -2097,6 +2277,8 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2097
2277
  liveProgress,
2098
2278
  onUpdate,
2099
2279
  worktreeSetup,
2280
+ timeoutMs: data.timeoutMs,
2281
+ deadlineAt,
2100
2282
  });
2101
2283
  for (let i = 0; i < results.length; i++) {
2102
2284
  const run = results[i]!;
@@ -2157,6 +2339,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2157
2339
  output: result.truncation?.text || getSingleResultOutput(result),
2158
2340
  exitCode: result.exitCode,
2159
2341
  error: result.error,
2342
+ timedOut: result.timedOut,
2160
2343
  })),
2161
2344
  (i, agent) => `=== Task ${i + 1}: ${agent} ===`,
2162
2345
  );
@@ -2184,13 +2367,14 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2184
2367
  signal,
2185
2368
  runId,
2186
2369
  sessionDirForIndex,
2187
- sessionFileForIndex,
2370
+ sessionFileForTask,
2188
2371
  shareEnabled,
2189
2372
  artifactConfig,
2190
2373
  artifactsDir,
2191
2374
  onUpdate,
2192
2375
  sessionRoot,
2193
2376
  controlConfig,
2377
+ contextPolicy,
2194
2378
  } = data;
2195
2379
  const onControlEvent = createForegroundControlNotifier(data, deps);
2196
2380
  const childIntercomTarget = data.intercomBridge.active ? resolveSubagentIntercomTarget(runId, params.agent!, 0) : undefined;
@@ -2254,6 +2438,9 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2254
2438
  if (override?.skills !== undefined) skillOverride = override.skills;
2255
2439
 
2256
2440
  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
+ }
2257
2444
  if (!isAsyncAvailable()) {
2258
2445
  return {
2259
2446
  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." }],
@@ -2266,12 +2453,13 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2266
2453
  pi: deps.pi,
2267
2454
  cwd: ctx.cwd,
2268
2455
  currentSessionId: deps.state.currentSessionId!,
2456
+ parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
2269
2457
  currentModelProvider: ctx.model?.provider,
2270
2458
  currentModel: ctx.model,
2271
2459
  };
2272
2460
  return executeAsyncSingle(id, {
2273
2461
  agent: params.agent!,
2274
- task: params.context === "fork" ? wrapForkTask(task) : task,
2462
+ task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(task) : task,
2275
2463
  agentConfig,
2276
2464
  ctx: asyncCtx,
2277
2465
  availableModels,
@@ -2281,7 +2469,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2281
2469
  artifactConfig,
2282
2470
  shareEnabled,
2283
2471
  sessionRoot,
2284
- sessionFile: sessionFileForIndex(0),
2472
+ sessionFile: sessionFileForTask(params.agent!, 0),
2285
2473
  skills: skillOverride === false ? [] : skillOverride,
2286
2474
  output: effectiveOutput,
2287
2475
  outputMode: effectiveOutputMode,
@@ -2296,7 +2484,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2296
2484
  }
2297
2485
  }
2298
2486
 
2299
- if (params.context === "fork") {
2487
+ if (shouldForkAgent(contextPolicy, params.agent!)) {
2300
2488
  task = wrapForkTask(task);
2301
2489
  }
2302
2490
  const cleanTask = task;
@@ -2349,7 +2537,9 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2349
2537
  }
2350
2538
  : undefined;
2351
2539
 
2540
+ const deadlineAt = data.deadlineAt ?? (data.timeoutMs !== undefined ? Date.now() + data.timeoutMs : undefined);
2352
2541
  const r = await runSync(ctx.cwd, agents, params.agent!, task, {
2542
+ parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
2353
2543
  cwd: effectiveCwd,
2354
2544
  signal,
2355
2545
  interruptSignal: interruptController.signal,
@@ -2357,7 +2547,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2357
2547
  intercomEvents: deps.pi.events,
2358
2548
  runId,
2359
2549
  sessionDir: sessionDirForIndex(0),
2360
- sessionFile: sessionFileForIndex(0),
2550
+ sessionFile: sessionFileForTask(params.agent!, 0),
2361
2551
  share: shareEnabled,
2362
2552
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
2363
2553
  artifactConfig,
@@ -2378,6 +2568,8 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2378
2568
  skills: effectiveSkills,
2379
2569
  acceptance: params.acceptance,
2380
2570
  acceptanceContext: { mode: "single" },
2571
+ timeoutMs: data.timeoutMs,
2572
+ deadlineAt,
2381
2573
  });
2382
2574
  if (foregroundControl?.currentIndex === 0) {
2383
2575
  foregroundControl.interrupt = undefined;
@@ -2462,6 +2654,23 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2462
2654
  };
2463
2655
  }
2464
2656
 
2657
+ function inferExecutionMode(params: SubagentParamsLike): SubagentRunMode {
2658
+ if ((params.chain?.length ?? 0) > 0) return "chain";
2659
+ if ((params.tasks?.length ?? 0) > 0) return "parallel";
2660
+ return "single";
2661
+ }
2662
+
2663
+ function duplicateSubagentCallResult(params: SubagentParamsLike): AgentToolResult<Details> {
2664
+ return {
2665
+ content: [{
2666
+ type: "text",
2667
+ text: "Rejected: a subagent call is already in progress. Issue exactly ONE subagent call per turn.",
2668
+ }],
2669
+ isError: true,
2670
+ details: { mode: inferExecutionMode(params), results: [] },
2671
+ };
2672
+ }
2673
+
2465
2674
  export function createSubagentExecutor(deps: ExecutorDeps): {
2466
2675
  execute: (
2467
2676
  id: string,
@@ -2498,7 +2707,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2498
2707
  let orchestratorTarget: string | undefined;
2499
2708
  try {
2500
2709
  orchestratorTarget = resolveIntercomSessionTarget(deps.pi.getSessionName(), ctx.sessionManager.getSessionId());
2501
- } catch {}
2710
+ } catch (error) {
2711
+ if (!sessionError) sessionError = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
2712
+ }
2502
2713
  return {
2503
2714
  content: [{
2504
2715
  type: "text",
@@ -2624,13 +2835,16 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2624
2835
  depth,
2625
2836
  deps.config.forceTopLevelAsync === true,
2626
2837
  );
2838
+ const foregroundTimeout = resolveForegroundTimeout(effectiveParams);
2839
+ if (foregroundTimeout.error) return buildRequestedModeError(effectiveParams, foregroundTimeout.error);
2627
2840
 
2628
2841
  const scope: AgentScope = resolveExecutionAgentScope(effectiveParams.agentScope);
2629
2842
  const effectiveCwd = effectiveParams.cwd ?? ctx.cwd;
2630
2843
  const parentSessionFile = ctx.sessionManager.getSessionFile() ?? null;
2631
2844
  deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
2632
2845
  const discoveredAgents = deps.discoverAgents(effectiveCwd, scope).agents;
2633
- effectiveParams = applyAgentDefaultContext(effectiveParams, discoveredAgents);
2846
+ const contextPolicy = resolveAgentDefaultContextPolicy(effectiveParams, discoveredAgents);
2847
+ effectiveParams = contextPolicy.params;
2634
2848
  const sessionName = resolveIntercomSessionTarget(deps.pi.getSessionName(), ctx.sessionManager.getSessionId());
2635
2849
  const intercomBridge = resolveIntercomBridge({
2636
2850
  config: deps.config.intercomBridge,
@@ -2664,15 +2878,18 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2664
2878
  );
2665
2879
  if (validationError) return validationError;
2666
2880
 
2667
- let sessionFileForIndex: (idx?: number) => string | undefined = () => undefined;
2881
+ let forkSessionFileForIndex: (idx?: number) => string | undefined = () => undefined;
2668
2882
  try {
2669
- sessionFileForIndex = createForkContextResolver(ctx.sessionManager, effectiveParams.context).sessionFileForIndex;
2883
+ forkSessionFileForIndex = createForkContextResolver(ctx.sessionManager, contextPolicy.usesFork ? "fork" : undefined).sessionFileForIndex;
2670
2884
  } catch (error) {
2671
2885
  return toExecutionErrorResult(effectiveParams, error);
2672
2886
  }
2673
2887
  const requestedAsync = effectiveParams.async ?? deps.asyncByDefault;
2674
2888
  const backgroundRequestedWhileClarifying = (hasChain || hasTasks) && requestedAsync && effectiveParams.clarify === true;
2675
2889
  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
+ }
2676
2893
  const controlConfig = resolveControlConfig(deps.config.control, effectiveParams.control);
2677
2894
 
2678
2895
  const artifactConfig: ArtifactConfig = {
@@ -2701,8 +2918,19 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2701
2918
  }
2702
2919
  const sessionDirForIndex = (idx?: number) =>
2703
2920
  path.join(sessionRoot, `run-${idx ?? 0}`);
2921
+ const forkSessionFileForTask = (agentName: string, idx?: number) =>
2922
+ shouldForkAgent(contextPolicy, agentName) ? forkSessionFileForIndex(idx) : undefined;
2923
+ const childSessionFileForTask = (agentName: string, idx?: number) =>
2924
+ forkSessionFileForTask(agentName, idx) ?? path.join(sessionDirForIndex(idx), "session.jsonl");
2704
2925
  const childSessionFileForIndex = (idx?: number) =>
2705
- sessionFileForIndex(idx) ?? path.join(sessionDirForIndex(idx), "session.jsonl");
2926
+ path.join(sessionDirForIndex(idx), "session.jsonl");
2927
+ try {
2928
+ preflightForkSessionsForStaticTasks(effectiveParams, contextPolicy, forkSessionFileForTask);
2929
+ } catch (error) {
2930
+ return toExecutionErrorResult(effectiveParams, error);
2931
+ }
2932
+ const chainBindingsError = validateExecutionChainBindings(effectiveParams, deps.config.chain?.dynamicFanout?.maxItems);
2933
+ if (chainBindingsError) return chainBindingsError;
2706
2934
 
2707
2935
  const onUpdateWithContext = onUpdate
2708
2936
  ? (r: AgentToolResult<Details>) => onUpdate(withForkContext(r, effectiveParams.context))
@@ -2720,6 +2948,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2720
2948
  sessionRoot,
2721
2949
  sessionDirForIndex,
2722
2950
  sessionFileForIndex: childSessionFileForIndex,
2951
+ sessionFileForTask: childSessionFileForTask,
2723
2952
  artifactConfig,
2724
2953
  artifactsDir,
2725
2954
  backgroundRequestedWhileClarifying,
@@ -2727,6 +2956,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2727
2956
  controlConfig,
2728
2957
  intercomBridge,
2729
2958
  nestedRoute,
2959
+ timeoutMs: foregroundTimeout.timeoutMs,
2960
+ contextPolicy,
2730
2961
  };
2731
2962
 
2732
2963
  const foregroundMode: "single" | "parallel" | "chain" = hasChain ? "chain" : hasTasks ? "parallel" : "single";
@@ -2851,5 +3082,22 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2851
3082
  }, effectiveParams.context);
2852
3083
  };
2853
3084
 
2854
- return { execute };
3085
+ const executeWithSingleDispatchGuard = async (
3086
+ id: string,
3087
+ params: SubagentParamsLike,
3088
+ signal: AbortSignal,
3089
+ onUpdate: ((r: AgentToolResult<Details>) => void) | undefined,
3090
+ ctx: ExtensionContext,
3091
+ ): Promise<AgentToolResult<Details>> => {
3092
+ if (params.action) return execute(id, params, signal, onUpdate, ctx);
3093
+ if (deps.state.subagentInProgress === true) return duplicateSubagentCallResult(params);
3094
+ deps.state.subagentInProgress = true;
3095
+ try {
3096
+ return await execute(id, params, signal, onUpdate, ctx);
3097
+ } finally {
3098
+ deps.state.subagentInProgress = false;
3099
+ }
3100
+ };
3101
+
3102
+ return { execute: executeWithSingleDispatchGuard };
2855
3103
  }