pi-subagents 0.30.0 → 0.31.1

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 (43) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +189 -18
  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 +58 -4
  17. package/src/extension/schemas.ts +10 -76
  18. package/src/intercom/intercom-bridge.ts +27 -4
  19. package/src/profiles/profiles.ts +637 -0
  20. package/src/runs/background/async-execution.ts +14 -4
  21. package/src/runs/background/async-job-tracker.ts +56 -11
  22. package/src/runs/background/async-resume.ts +11 -13
  23. package/src/runs/background/control-channel.ts +177 -0
  24. package/src/runs/background/result-watcher.ts +11 -2
  25. package/src/runs/background/stale-run-reconciler.ts +9 -4
  26. package/src/runs/background/subagent-runner.ts +86 -3
  27. package/src/runs/foreground/chain-execution.ts +26 -2
  28. package/src/runs/foreground/execution.ts +113 -8
  29. package/src/runs/foreground/subagent-executor.ts +356 -86
  30. package/src/runs/shared/acceptance.ts +285 -34
  31. package/src/runs/shared/completion-guard.ts +1 -1
  32. package/src/runs/shared/dynamic-fanout.ts +4 -2
  33. package/src/runs/shared/mcp-direct-tool-allowlist.ts +2 -2
  34. package/src/runs/shared/parallel-utils.ts +6 -1
  35. package/src/runs/shared/pi-args.ts +9 -1
  36. package/src/runs/shared/single-output.ts +15 -1
  37. package/src/runs/shared/subagent-prompt-runtime.ts +1 -0
  38. package/src/shared/settings.ts +1 -0
  39. package/src/shared/types.ts +9 -2
  40. package/src/shared/utils.ts +19 -1
  41. package/src/slash/prompt-template-bridge.ts +26 -3
  42. package/src/slash/slash-commands.ts +642 -43
  43. 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";
@@ -50,6 +51,8 @@ import {
50
51
  stripDetailsOutputsForIntercomReceipt,
51
52
  } from "../../intercom/result-intercom.ts";
52
53
  import { buildRevivedAsyncTask, interruptLiveAsyncResumeTarget, resolveAsyncResumeTarget } from "../background/async-resume.ts";
54
+ import { deliverInterruptRequest } from "../background/control-channel.ts";
55
+ import { reconcileAsyncRun } from "../background/stale-run-reconciler.ts";
53
56
  import { resolveAsyncRootResultPath } from "../background/chain-root-attachment.ts";
54
57
  import { createNestedRoute, readNestedControlResults, resolveInheritedNestedRouteFromEnv, resolveNestedAsyncDir, resolveNestedParentAddressFromEnv, updateForegroundNestedProjection, writeNestedControlRequest, writeNestedEvent, type NestedRunResolutionScope } from "../shared/nested-events.ts";
55
58
  import { resolveSubagentRunId, type ResolvedSubagentRunId } from "../background/run-id-resolver.ts";
@@ -96,7 +99,6 @@ import {
96
99
  wrapForkTask,
97
100
  } from "../../shared/types.ts";
98
101
 
99
- const ASYNC_INTERRUPT_SIGNAL: NodeJS.Signals = process.platform === "win32" ? "SIGBREAK" : "SIGUSR2";
100
102
  const MUTATING_MANAGEMENT_ACTIONS = new Set(["create", "update", "delete"]);
101
103
 
102
104
  interface TaskParam {
@@ -128,6 +130,8 @@ export interface SubagentParamsLike {
128
130
  worktree?: boolean;
129
131
  context?: "fresh" | "fork";
130
132
  async?: boolean;
133
+ timeoutMs?: number;
134
+ maxRuntimeMs?: number;
131
135
  clarify?: boolean;
132
136
  share?: boolean;
133
137
  control?: ControlConfig;
@@ -170,6 +174,7 @@ interface ExecutionContextData {
170
174
  sessionRoot: string;
171
175
  sessionDirForIndex: (idx?: number) => string;
172
176
  sessionFileForIndex: (idx?: number) => string | undefined;
177
+ sessionFileForTask: (agentName: string, idx?: number) => string | undefined;
173
178
  artifactConfig: ArtifactConfig;
174
179
  artifactsDir: string;
175
180
  backgroundRequestedWhileClarifying: boolean;
@@ -177,6 +182,9 @@ interface ExecutionContextData {
177
182
  controlConfig: ResolvedControlConfig;
178
183
  intercomBridge: IntercomBridgeState;
179
184
  nestedRoute?: NestedRouteInfo;
185
+ timeoutMs?: number;
186
+ deadlineAt?: number;
187
+ contextPolicy: AgentDefaultContextPolicy;
180
188
  }
181
189
 
182
190
  function resolveRequestedCwd(runtimeCwd: string, requestedCwd: string | undefined): string {
@@ -365,7 +373,17 @@ function resolveResumeTarget(params: SubagentParamsLike, state: SubagentState, o
365
373
  throw new Error("Run not found. Provide id or runId.");
366
374
  }
367
375
 
368
- function getAsyncInterruptTarget(state: SubagentState, runId: string | undefined): { asyncId: string; asyncDir: string } | undefined {
376
+ function getAsyncInterruptTarget(
377
+ state: SubagentState,
378
+ runId: string | undefined,
379
+ location?: { asyncDir: string | null; resolvedId?: string },
380
+ ): { asyncId: string; asyncDir: string } | undefined {
381
+ if (location?.asyncDir) {
382
+ return {
383
+ asyncId: location.resolvedId ?? runId ?? path.basename(location.asyncDir),
384
+ asyncDir: location.asyncDir,
385
+ };
386
+ }
369
387
  if (runId) {
370
388
  const direct = state.asyncJobs.get(runId);
371
389
  if (direct) return { asyncId: direct.asyncId, asyncDir: direct.asyncDir };
@@ -408,10 +426,15 @@ function emitControlNotification(input: {
408
426
  }
409
427
  }
410
428
 
411
- function interruptAsyncRun(state: SubagentState, runId: string | undefined, kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean): AgentToolResult<Details> | null {
412
- const target = getAsyncInterruptTarget(state, runId);
429
+ function interruptAsyncRun(
430
+ state: SubagentState,
431
+ runId: string | undefined,
432
+ kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean,
433
+ location?: { asyncDir: string | null; resolvedId?: string },
434
+ ): AgentToolResult<Details> | null {
435
+ const target = getAsyncInterruptTarget(state, runId, location);
413
436
  if (!target) return null;
414
- const status = readStatus(target.asyncDir);
437
+ const status = reconcileAsyncRun(target.asyncDir, { kill }).status;
415
438
  if (!status || status.state !== "running" || typeof status.pid !== "number") {
416
439
  return {
417
440
  content: [{ type: "text", text: `No running async run with an interrupt-capable pid was found for '${runId ?? "current"}'.` }],
@@ -420,7 +443,7 @@ function interruptAsyncRun(state: SubagentState, runId: string | undefined, kill
420
443
  };
421
444
  }
422
445
  try {
423
- (kill ?? process.kill)(status.pid, ASYNC_INTERRUPT_SIGNAL);
446
+ deliverInterruptRequest({ asyncDir: target.asyncDir, pid: status.pid, kill, source: "interrupt-action" });
424
447
  const tracked = state.asyncJobs.get(target.asyncId);
425
448
  if (tracked) {
426
449
  tracked.activityState = undefined;
@@ -471,6 +494,14 @@ function appendStepToAsyncChain(input: {
471
494
  details: { mode: "management", results: [] },
472
495
  };
473
496
  }
497
+ const acceptanceErrors = validateExecutionAcceptance(input.params);
498
+ if (acceptanceErrors.length > 0) {
499
+ return {
500
+ content: [{ type: "text", text: `Cannot append step: ${acceptanceErrors.join(" ")}` }],
501
+ isError: true,
502
+ details: { mode: "management", results: [] },
503
+ };
504
+ }
474
505
 
475
506
  let resolved: ResolvedSubagentRunId | undefined;
476
507
  try {
@@ -547,17 +578,19 @@ function appendStepToAsyncChain(input: {
547
578
 
548
579
  const scope: AgentScope = resolveExecutionAgentScope(input.params.agentScope);
549
580
  const agents = input.deps.discoverAgents(input.requestCwd, scope).agents;
581
+ const contextPolicy = resolveExplicitContextPolicy(input.params);
550
582
  const chainSkillInput = normalizeSkillInput(input.params.skill);
551
583
  const chainSkills = chainSkillInput === false ? [] : (chainSkillInput ?? []);
552
584
  const asyncCtx = {
553
585
  pi: input.deps.pi,
554
586
  cwd: input.ctx.cwd,
555
587
  currentSessionId: resolveCurrentSessionId(input.ctx.sessionManager),
588
+ parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
556
589
  currentModelProvider: input.ctx.model?.provider,
557
590
  currentModel: input.ctx.model,
558
591
  };
559
592
  const built = buildAsyncRunnerSteps(resolved.id, {
560
- chain: wrapChainTasksForFork(input.params.chain, input.params.context),
593
+ chain: wrapChainTasksForFork(input.params.chain, contextPolicy),
561
594
  task: input.params.task,
562
595
  resultMode: "chain",
563
596
  agents,
@@ -702,11 +735,11 @@ function directNestedAsyncInterrupt(target: ResolvedSubagentRunId & { kind: "nes
702
735
  const run = target.match.run;
703
736
  const asyncDir = resolveNestedAsyncDir(target.match.rootRunId, run);
704
737
  if (!asyncDir) return undefined;
705
- const status = readStatus(asyncDir);
738
+ const status = reconcileAsyncRun(asyncDir, { resultsDir: path.join(RESULTS_DIR, "nested", target.match.rootRunId) }).status;
706
739
  const pid = typeof status?.pid === "number" && status.pid > 0 ? status.pid : run.pid;
707
740
  if (!status || status.state !== "running" || typeof pid !== "number" || pid <= 0) return undefined;
708
741
  try {
709
- process.kill(pid, ASYNC_INTERRUPT_SIGNAL);
742
+ deliverInterruptRequest({ asyncDir, pid, source: "nested-interrupt" });
710
743
  return { content: [{ type: "text", text: `Interrupt requested for nested async run ${run.id}.` }], details: { mode: "management", results: [] } };
711
744
  } catch (error) {
712
745
  const message = error instanceof Error ? error.message : String(error);
@@ -783,6 +816,7 @@ async function resumeAsyncRun(input: {
783
816
  target,
784
817
  state: input.deps.state,
785
818
  kill: input.deps.kill,
819
+ resultsDir: RESULTS_DIR,
786
820
  });
787
821
  if (!interrupt.ok) {
788
822
  return {
@@ -861,7 +895,8 @@ async function resumeAsyncRun(input: {
861
895
  const runId = randomUUID().slice(0, 8);
862
896
  const artifactConfig: ArtifactConfig = { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false };
863
897
  const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo);
864
- const chain = wrapChainTasksForFork(attachChain, input.params.context);
898
+ const contextPolicy = resolveExplicitContextPolicy(input.params);
899
+ const chain = wrapChainTasksForFork(attachChain, contextPolicy);
865
900
  const normalized = normalizeSkillInput(input.params.skill);
866
901
  const result = executeAsyncChain(runId, {
867
902
  chain,
@@ -879,6 +914,7 @@ async function resumeAsyncRun(input: {
879
914
  pi: input.deps.pi,
880
915
  cwd: input.requestCwd,
881
916
  currentSessionId: input.deps.state.currentSessionId,
917
+ parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
882
918
  currentModelProvider: input.ctx.model?.provider,
883
919
  currentModel: input.ctx.model,
884
920
  },
@@ -921,6 +957,7 @@ async function resumeAsyncRun(input: {
921
957
  pi: input.deps.pi,
922
958
  cwd: input.requestCwd,
923
959
  currentSessionId: input.deps.state.currentSessionId,
960
+ parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
924
961
  currentModelProvider: input.ctx.model?.provider,
925
962
  currentModel: input.ctx.model,
926
963
  },
@@ -1068,6 +1105,15 @@ function validateExecutionInput(
1068
1105
  };
1069
1106
  }
1070
1107
 
1108
+ const acceptanceErrors = validateExecutionAcceptance(params);
1109
+ if (acceptanceErrors.length > 0) {
1110
+ return {
1111
+ content: [{ type: "text", text: acceptanceErrors.join(" ") }],
1112
+ isError: true,
1113
+ details: { mode: getRequestedModeLabel(params), results: [] },
1114
+ };
1115
+ }
1116
+
1071
1117
  if (hasSingle && params.agent && !agents.find((agent) => agent.name === params.agent)) {
1072
1118
  return {
1073
1119
  content: [{ type: "text", text: `Unknown agent: ${params.agent}` }],
@@ -1145,6 +1191,42 @@ function validateExecutionInput(
1145
1191
  return null;
1146
1192
  }
1147
1193
 
1194
+ function validateExecutionChainBindings(params: SubagentParamsLike, dynamicFanoutMaxItems?: number): AgentToolResult<Details> | null {
1195
+ if ((params.chain?.length ?? 0) === 0) return null;
1196
+ try {
1197
+ validateChainOutputBindingsWithContext(params.chain as ChainStep[], { maxItems: dynamicFanoutMaxItems });
1198
+ } catch (error) {
1199
+ if (error instanceof ChainOutputValidationError) {
1200
+ return {
1201
+ content: [{ type: "text", text: error.message }],
1202
+ isError: true,
1203
+ details: { mode: "chain" as const, results: [] },
1204
+ };
1205
+ }
1206
+ throw error;
1207
+ }
1208
+ return null;
1209
+ }
1210
+
1211
+ function validateExecutionAcceptance(params: SubagentParamsLike): string[] {
1212
+ const errors: string[] = [];
1213
+ errors.push(...validateAcceptanceInput(params.acceptance, "acceptance"));
1214
+ for (const [index, task] of (params.tasks ?? []).entries()) {
1215
+ errors.push(...validateAcceptanceInput(task.acceptance, `tasks[${index}].acceptance`));
1216
+ }
1217
+ for (const [stepIndex, step] of (params.chain ?? []).entries()) {
1218
+ errors.push(...validateAcceptanceInput((step as { acceptance?: unknown }).acceptance, `chain[${stepIndex}].acceptance`));
1219
+ if (isParallelStep(step)) {
1220
+ for (const [taskIndex, task] of step.parallel.entries()) {
1221
+ errors.push(...validateAcceptanceInput(task.acceptance, `chain[${stepIndex}].parallel[${taskIndex}].acceptance`));
1222
+ }
1223
+ } else if (isDynamicParallelStep(step)) {
1224
+ errors.push(...validateAcceptanceInput(step.parallel.acceptance, `chain[${stepIndex}].parallel.acceptance`));
1225
+ }
1226
+ }
1227
+ return errors;
1228
+ }
1229
+
1148
1230
  function getRequestedModeLabel(params: SubagentParamsLike): Details["mode"] {
1149
1231
  if ((params.chain?.length ?? 0) > 0) return "chain";
1150
1232
  if ((params.tasks?.length ?? 0) > 0) return "parallel";
@@ -1152,16 +1234,46 @@ function getRequestedModeLabel(params: SubagentParamsLike): Details["mode"] {
1152
1234
  return "single";
1153
1235
  }
1154
1236
 
1155
- function applyAgentDefaultContext(params: SubagentParamsLike, agents: AgentConfig[]): SubagentParamsLike {
1156
- if (params.context !== undefined) return params;
1237
+ interface AgentDefaultContextPolicy {
1238
+ params: SubagentParamsLike;
1239
+ contextForAgent(agentName: string): "fresh" | "fork";
1240
+ usesFork: boolean;
1241
+ }
1242
+
1243
+ function resolveAgentDefaultContextPolicy(params: SubagentParamsLike, agents: AgentConfig[]): AgentDefaultContextPolicy {
1244
+ if (params.context !== undefined) {
1245
+ return resolveExplicitContextPolicy(params);
1246
+ }
1157
1247
  const byName = new Map(agents.map((agent) => [agent.name, agent]));
1248
+ const contextForAgent = (agentName: string): "fresh" | "fork" =>
1249
+ byName.get(agentName)?.defaultContext === "fork" ? "fork" : "fresh";
1250
+ const usesFork = collectRequestedAgentNames(params).some((name) => contextForAgent(name) === "fork");
1251
+ return {
1252
+ params: usesFork ? { ...params, context: "fork" } : params,
1253
+ contextForAgent,
1254
+ usesFork,
1255
+ };
1256
+ }
1257
+
1258
+ function resolveExplicitContextPolicy(params: SubagentParamsLike): AgentDefaultContextPolicy {
1259
+ const context = params.context === "fork" ? "fork" : "fresh";
1260
+ return {
1261
+ params,
1262
+ contextForAgent: () => context,
1263
+ usesFork: context === "fork",
1264
+ };
1265
+ }
1266
+
1267
+ function collectRequestedAgentNames(params: SubagentParamsLike): string[] {
1158
1268
  const names: string[] = [];
1159
1269
  if (params.agent) names.push(params.agent);
1160
1270
  for (const task of params.tasks ?? []) names.push(task.agent);
1161
1271
  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;
1272
+ return names;
1273
+ }
1274
+
1275
+ function shouldForkAgent(contextPolicy: AgentDefaultContextPolicy, agentName: string): boolean {
1276
+ return contextPolicy.contextForAgent(agentName) === "fork";
1165
1277
  }
1166
1278
 
1167
1279
  function buildRequestedModeError(params: SubagentParamsLike, message: string): AgentToolResult<Details> {
@@ -1175,6 +1287,22 @@ function buildRequestedModeError(params: SubagentParamsLike, message: string): A
1175
1287
  );
1176
1288
  }
1177
1289
 
1290
+ function resolveForegroundTimeout(params: SubagentParamsLike): { timeoutMs?: number; error?: string } {
1291
+ const rawTimeout = params.timeoutMs;
1292
+ const rawMaxRuntime = params.maxRuntimeMs;
1293
+ if (rawTimeout === undefined && rawMaxRuntime === undefined) return {};
1294
+ for (const [name, value] of [["timeoutMs", rawTimeout], ["maxRuntimeMs", rawMaxRuntime]] as const) {
1295
+ if (value === undefined) continue;
1296
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
1297
+ return { error: `${name} must be a positive integer.` };
1298
+ }
1299
+ }
1300
+ if (rawTimeout !== undefined && rawMaxRuntime !== undefined && rawTimeout !== rawMaxRuntime) {
1301
+ return { error: "timeoutMs and maxRuntimeMs are aliases; provide only one value or use the same value for both." };
1302
+ }
1303
+ return { timeoutMs: rawTimeout ?? rawMaxRuntime };
1304
+ }
1305
+
1178
1306
  function expandTopLevelTaskCounts(tasks: TaskParam[]): { tasks?: TaskParam[]; error?: string } {
1179
1307
  const expanded: TaskParam[] = [];
1180
1308
  for (let taskIndex = 0; taskIndex < tasks.length; taskIndex++) {
@@ -1262,14 +1390,14 @@ function toExecutionErrorResult(params: SubagentParamsLike, error: unknown): Age
1262
1390
 
1263
1391
  function collectChainSessionFiles(
1264
1392
  chain: ChainStep[],
1265
- sessionFileForIndex: (idx?: number) => string | undefined,
1393
+ sessionFileForTask: (agentName: string, idx?: number) => string | undefined,
1266
1394
  ): (string | undefined)[] {
1267
1395
  const sessionFiles: (string | undefined)[] = [];
1268
1396
  let flatIndex = 0;
1269
1397
  for (const step of chain) {
1270
1398
  if (isParallelStep(step)) {
1271
- for (let i = 0; i < step.parallel.length; i++) {
1272
- sessionFiles.push(sessionFileForIndex(flatIndex));
1399
+ for (const task of step.parallel) {
1400
+ sessionFiles.push(sessionFileForTask(task.agent, flatIndex));
1273
1401
  flatIndex++;
1274
1402
  }
1275
1403
  continue;
@@ -1278,21 +1406,22 @@ function collectChainSessionFiles(
1278
1406
  sessionFiles.push(undefined);
1279
1407
  continue;
1280
1408
  }
1281
- sessionFiles.push(sessionFileForIndex(flatIndex));
1409
+ sessionFiles.push(sessionFileForTask((step as SequentialStep).agent, flatIndex));
1282
1410
  flatIndex++;
1283
1411
  }
1284
1412
  return sessionFiles;
1285
1413
  }
1286
1414
 
1287
- function wrapChainTasksForFork(chain: ChainStep[], context: SubagentParamsLike["context"]): ChainStep[] {
1288
- if (context !== "fork") return chain;
1415
+ function wrapChainTasksForFork(chain: ChainStep[], contextPolicy: AgentDefaultContextPolicy): ChainStep[] {
1289
1416
  return chain.map((step, stepIndex) => {
1290
1417
  if (isParallelStep(step)) {
1291
1418
  return {
1292
1419
  ...step,
1293
1420
  parallel: step.parallel.map((task) => ({
1294
1421
  ...task,
1295
- task: wrapForkTask(task.task ?? "{previous}"),
1422
+ task: shouldForkAgent(contextPolicy, task.agent)
1423
+ ? wrapForkTask(task.task ?? "{previous}")
1424
+ : task.task,
1296
1425
  })),
1297
1426
  };
1298
1427
  }
@@ -1301,18 +1430,59 @@ function wrapChainTasksForFork(chain: ChainStep[], context: SubagentParamsLike["
1301
1430
  ...step,
1302
1431
  parallel: {
1303
1432
  ...step.parallel,
1304
- task: wrapForkTask(step.parallel.task ?? "{previous}"),
1433
+ task: shouldForkAgent(contextPolicy, step.parallel.agent)
1434
+ ? wrapForkTask(step.parallel.task ?? "{previous}")
1435
+ : step.parallel.task,
1305
1436
  },
1306
1437
  };
1307
1438
  }
1308
1439
  const sequential = step as SequentialStep;
1309
1440
  return {
1310
1441
  ...sequential,
1311
- task: wrapForkTask(sequential.task ?? (stepIndex === 0 ? "{task}" : "{previous}")),
1442
+ task: shouldForkAgent(contextPolicy, sequential.agent)
1443
+ ? wrapForkTask(sequential.task ?? (stepIndex === 0 ? "{task}" : "{previous}"))
1444
+ : sequential.task,
1312
1445
  };
1313
1446
  });
1314
1447
  }
1315
1448
 
1449
+ function preflightForkSessionsForStaticTasks(
1450
+ params: SubagentParamsLike,
1451
+ contextPolicy: AgentDefaultContextPolicy,
1452
+ sessionFileForTask: (agentName: string, idx?: number) => string | undefined,
1453
+ ): void {
1454
+ if (!contextPolicy.usesFork) return;
1455
+ if (params.agent) {
1456
+ if (shouldForkAgent(contextPolicy, params.agent)) sessionFileForTask(params.agent, 0);
1457
+ return;
1458
+ }
1459
+ if (params.tasks) {
1460
+ params.tasks.forEach((task, index) => {
1461
+ if (shouldForkAgent(contextPolicy, task.agent)) sessionFileForTask(task.agent, index);
1462
+ });
1463
+ return;
1464
+ }
1465
+ if (!params.chain?.length) return;
1466
+ let flatIndex = 0;
1467
+ for (const step of params.chain) {
1468
+ if (isParallelStep(step)) {
1469
+ for (const task of step.parallel) {
1470
+ if (shouldForkAgent(contextPolicy, task.agent)) sessionFileForTask(task.agent, flatIndex);
1471
+ flatIndex++;
1472
+ }
1473
+ continue;
1474
+ }
1475
+ if (isDynamicParallelStep(step)) {
1476
+ if (shouldForkAgent(contextPolicy, step.parallel.agent)) sessionFileForTask(step.parallel.agent, flatIndex);
1477
+ flatIndex++;
1478
+ continue;
1479
+ }
1480
+ const sequential = step as SequentialStep;
1481
+ if (shouldForkAgent(contextPolicy, sequential.agent)) sessionFileForTask(sequential.agent, flatIndex);
1482
+ flatIndex++;
1483
+ }
1484
+ }
1485
+
1316
1486
  function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentToolResult<Details> | null {
1317
1487
  const {
1318
1488
  params,
@@ -1322,12 +1492,14 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1322
1492
  shareEnabled,
1323
1493
  sessionRoot,
1324
1494
  sessionFileForIndex,
1495
+ sessionFileForTask,
1325
1496
  artifactConfig,
1326
1497
  artifactsDir,
1327
1498
  effectiveAsync,
1328
1499
  controlConfig,
1329
1500
  intercomBridge,
1330
1501
  nestedRoute,
1502
+ contextPolicy,
1331
1503
  } = data;
1332
1504
  const hasChain = (params.chain?.length ?? 0) > 0;
1333
1505
  const hasTasks = (params.tasks?.length ?? 0) > 0;
@@ -1368,6 +1540,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1368
1540
  pi: deps.pi,
1369
1541
  cwd: ctx.cwd,
1370
1542
  currentSessionId: deps.state.currentSessionId!,
1543
+ parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
1371
1544
  currentModelProvider: ctx.model?.provider,
1372
1545
  currentModel: ctx.model,
1373
1546
  };
@@ -1385,7 +1558,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1385
1558
  const skillOverrides = params.tasks.map((task) => normalizeSkillInput(task.skill));
1386
1559
  const parallelTasks = params.tasks.map((task, index) => ({
1387
1560
  agent: task.agent,
1388
- task: params.context === "fork" ? wrapForkTask(task.task) : task.task,
1561
+ task: shouldForkAgent(contextPolicy, task.agent) ? wrapForkTask(task.task) : task.task,
1389
1562
  cwd: task.cwd,
1390
1563
  ...(modelOverrides[index] ? { model: modelOverrides[index] } : {}),
1391
1564
  ...(skillOverrides[index] !== undefined ? { skill: skillOverrides[index] } : {}),
@@ -1412,7 +1585,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1412
1585
  shareEnabled,
1413
1586
  sessionRoot,
1414
1587
  chainSkills: [],
1415
- sessionFilesByFlatIndex: params.tasks.map((_, index) => sessionFileForIndex(index)),
1588
+ sessionFilesByFlatIndex: params.tasks.map((task, index) => sessionFileForTask(task.agent, index)),
1416
1589
  maxSubagentDepth: currentMaxSubagentDepth,
1417
1590
  worktreeSetupHook: deps.config.worktreeSetupHook,
1418
1591
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
@@ -1426,7 +1599,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1426
1599
  if (hasChain && params.chain) {
1427
1600
  const normalized = normalizeSkillInput(params.skill);
1428
1601
  const chainSkills = normalized === false ? [] : (normalized ?? []);
1429
- const chain = wrapChainTasksForFork(params.chain as ChainStep[], params.context);
1602
+ const chain = wrapChainTasksForFork(params.chain as ChainStep[], contextPolicy);
1430
1603
  return executeAsyncChain(id, {
1431
1604
  chain,
1432
1605
  task: params.task,
@@ -1440,7 +1613,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1440
1613
  shareEnabled,
1441
1614
  sessionRoot,
1442
1615
  chainSkills,
1443
- sessionFilesByFlatIndex: collectChainSessionFiles(chain, sessionFileForIndex),
1616
+ sessionFilesByFlatIndex: collectChainSessionFiles(chain, sessionFileForTask),
1444
1617
  dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
1445
1618
  maxSubagentDepth: currentMaxSubagentDepth,
1446
1619
  worktreeSetupHook: deps.config.worktreeSetupHook,
@@ -1470,7 +1643,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1470
1643
  const modelOverride = resolveSubagentModelOverride((params.model as string | undefined) ?? a.model, ctx.model, availableModels, currentProvider);
1471
1644
  return executeAsyncSingle(id, {
1472
1645
  agent: params.agent!,
1473
- task: params.context === "fork" ? wrapForkTask(params.task ?? "") : (params.task ?? ""),
1646
+ task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(params.task ?? "") : (params.task ?? ""),
1474
1647
  agentConfig: a,
1475
1648
  ctx: asyncCtx,
1476
1649
  availableModels,
@@ -1480,7 +1653,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1480
1653
  artifactConfig,
1481
1654
  shareEnabled,
1482
1655
  sessionRoot,
1483
- sessionFile: sessionFileForIndex(0),
1656
+ sessionFile: sessionFileForTask(params.agent!, 0),
1484
1657
  skills,
1485
1658
  output: effectiveOutput,
1486
1659
  outputMode: effectiveOutputMode,
@@ -1510,18 +1683,20 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1510
1683
  shareEnabled,
1511
1684
  sessionDirForIndex,
1512
1685
  sessionFileForIndex,
1686
+ sessionFileForTask,
1513
1687
  artifactsDir,
1514
1688
  artifactConfig,
1515
1689
  onUpdate,
1516
1690
  sessionRoot,
1517
1691
  controlConfig,
1692
+ contextPolicy,
1518
1693
  } = data;
1519
1694
  const onControlEvent = createForegroundControlNotifier(data, deps);
1520
1695
  const childIntercomTarget = data.intercomBridge.active ? resolveSubagentIntercomTarget : undefined;
1521
1696
  const foregroundControl = deps.state.foregroundControls.get(runId);
1522
1697
  const normalized = normalizeSkillInput(params.skill);
1523
1698
  const chainSkills = normalized === false ? [] : (normalized ?? []);
1524
- const chain = wrapChainTasksForFork(params.chain as ChainStep[], params.context);
1699
+ const chain = wrapChainTasksForFork(params.chain as ChainStep[], contextPolicy);
1525
1700
  const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
1526
1701
  const chainResult = await executeChain({
1527
1702
  chain,
@@ -1535,6 +1710,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1535
1710
  shareEnabled,
1536
1711
  sessionDirForIndex,
1537
1712
  sessionFileForIndex,
1713
+ sessionFileForTask,
1538
1714
  artifactsDir,
1539
1715
  artifactConfig,
1540
1716
  includeProgress: params.includeProgress,
@@ -1552,9 +1728,14 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1552
1728
  maxSubagentDepth: currentMaxSubagentDepth,
1553
1729
  worktreeSetupHook: deps.config.worktreeSetupHook,
1554
1730
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
1731
+ timeoutMs: data.timeoutMs,
1732
+ deadlineAt: data.deadlineAt,
1555
1733
  });
1556
1734
 
1557
1735
  if (chainResult.requestedAsync) {
1736
+ if (data.timeoutMs !== undefined) {
1737
+ return buildRequestedModeError(params, "timeoutMs/maxRuntimeMs are only supported for foreground runs; background launch from clarify cannot preserve the timeout.");
1738
+ }
1558
1739
  if (!isAsyncAvailable()) {
1559
1740
  return {
1560
1741
  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 +1748,11 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1567
1748
  pi: deps.pi,
1568
1749
  cwd: ctx.cwd,
1569
1750
  currentSessionId: deps.state.currentSessionId!,
1751
+ parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
1570
1752
  currentModelProvider: ctx.model?.provider,
1571
1753
  currentModel: ctx.model,
1572
1754
  };
1573
- const asyncChain = wrapChainTasksForFork(chainResult.requestedAsync.chain, params.context);
1755
+ const asyncChain = wrapChainTasksForFork(chainResult.requestedAsync.chain, contextPolicy);
1574
1756
  return executeAsyncChain(id, {
1575
1757
  chain: asyncChain,
1576
1758
  task: params.task,
@@ -1584,7 +1766,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1584
1766
  shareEnabled,
1585
1767
  sessionRoot,
1586
1768
  chainSkills: chainResult.requestedAsync.chainSkills,
1587
- sessionFilesByFlatIndex: collectChainSessionFiles(asyncChain, sessionFileForIndex),
1769
+ sessionFilesByFlatIndex: collectChainSessionFiles(asyncChain, sessionFileForTask),
1588
1770
  dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
1589
1771
  maxSubagentDepth: currentMaxSubagentDepth,
1590
1772
  worktreeSetupHook: deps.config.worktreeSetupHook,
@@ -1630,11 +1812,13 @@ interface ForegroundParallelRunInput {
1630
1812
  runId: string;
1631
1813
  sessionDirForIndex: (idx?: number) => string | undefined;
1632
1814
  sessionFileForIndex: (idx?: number) => string | undefined;
1815
+ sessionFileForTask: (agentName: string, idx?: number) => string | undefined;
1633
1816
  shareEnabled: boolean;
1634
1817
  artifactConfig: ArtifactConfig;
1635
1818
  artifactsDir: string;
1636
1819
  maxOutput?: MaxOutputConfig;
1637
1820
  paramsCwd: string;
1821
+ progressDir: string;
1638
1822
  maxSubagentDepths: number[];
1639
1823
  availableModels: ModelInfo[];
1640
1824
  modelOverrides: (string | undefined)[];
@@ -1650,6 +1834,8 @@ interface ForegroundParallelRunInput {
1650
1834
  liveProgress: (AgentProgress | undefined)[];
1651
1835
  onUpdate?: (r: AgentToolResult<Details>) => void;
1652
1836
  worktreeSetup?: WorktreeSetup;
1837
+ timeoutMs?: number;
1838
+ deadlineAt?: number;
1653
1839
  }
1654
1840
 
1655
1841
  function buildParallelModeError(message: string): AgentToolResult<Details> {
@@ -1760,7 +1946,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
1760
1946
  ? buildChainInstructions({ ...behavior, output: false, progress: false }, taskCwd, false)
1761
1947
  : { prefix: "", suffix: "" };
1762
1948
  const progressInstructions = behavior
1763
- ? buildChainInstructions({ ...behavior, output: false, reads: false }, input.paramsCwd, index === input.firstProgressIndex)
1949
+ ? buildChainInstructions({ ...behavior, output: false, reads: false }, input.progressDir, index === input.firstProgressIndex)
1764
1950
  : { prefix: "", suffix: "" };
1765
1951
  const outputPath = resolveSingleOutputPath(behavior?.output, input.ctx.cwd, taskCwd);
1766
1952
  const taskText = injectSingleOutputInstruction(
@@ -1783,6 +1969,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
1783
1969
  }
1784
1970
  const agentConfig = input.agents.find((agent) => agent.name === task.agent);
1785
1971
  return runSync(input.ctx.cwd, input.agents, task.agent, taskText, {
1972
+ parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
1786
1973
  cwd: taskCwd,
1787
1974
  signal: input.signal,
1788
1975
  interruptSignal: interruptController.signal,
@@ -1791,7 +1978,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
1791
1978
  runId: input.runId,
1792
1979
  index,
1793
1980
  sessionDir: input.sessionDirForIndex(index),
1794
- sessionFile: input.sessionFileForIndex(index),
1981
+ sessionFile: input.sessionFileForTask(task.agent, index),
1795
1982
  share: input.shareEnabled,
1796
1983
  artifactsDir: input.artifactConfig.enabled ? input.artifactsDir : undefined,
1797
1984
  artifactConfig: input.artifactConfig,
@@ -1810,39 +1997,41 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
1810
1997
  skills: effectiveSkills === false ? [] : effectiveSkills,
1811
1998
  acceptance: task.acceptance,
1812
1999
  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
- });
2000
+ timeoutMs: input.timeoutMs,
2001
+ deadlineAt: input.deadlineAt,
2002
+ onUpdate: input.onUpdate
2003
+ ? (progressUpdate) => {
2004
+ const stepResults = progressUpdate.details?.results || [];
2005
+ const stepProgress = progressUpdate.details?.progress || [];
2006
+ if (input.foregroundControl && stepProgress.length > 0) {
2007
+ const current = stepProgress[0];
2008
+ input.foregroundControl.currentAgent = task.agent;
2009
+ input.foregroundControl.currentIndex = index;
2010
+ input.foregroundControl.currentActivityState = current?.activityState;
2011
+ input.foregroundControl.lastActivityAt = current?.lastActivityAt;
2012
+ input.foregroundControl.currentTool = current?.currentTool;
2013
+ input.foregroundControl.currentToolStartedAt = current?.currentToolStartedAt;
2014
+ input.foregroundControl.currentPath = current?.currentPath;
2015
+ input.foregroundControl.turnCount = current?.turnCount;
2016
+ input.foregroundControl.tokens = current?.tokens;
2017
+ input.foregroundControl.toolCount = current?.toolCount;
2018
+ input.foregroundControl.updatedAt = Date.now();
1845
2019
  }
2020
+ if (stepResults.length > 0) input.liveResults[index] = stepResults[0];
2021
+ if (stepProgress.length > 0) input.liveProgress[index] = stepProgress[0];
2022
+ const mergedResults = input.liveResults.filter((result): result is SingleResult => result !== undefined);
2023
+ const mergedProgress = input.liveProgress.filter((progress): progress is AgentProgress => progress !== undefined);
2024
+ input.onUpdate?.({
2025
+ content: progressUpdate.content,
2026
+ details: {
2027
+ mode: "parallel",
2028
+ results: mergedResults,
2029
+ progress: mergedProgress,
2030
+ controlEvents: progressUpdate.details?.controlEvents,
2031
+ totalSteps: input.tasks.length,
2032
+ },
2033
+ });
2034
+ }
1846
2035
  : undefined,
1847
2036
  }).finally(() => {
1848
2037
  if (input.foregroundControl?.currentIndex === index) {
@@ -1863,6 +2052,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1863
2052
  runId,
1864
2053
  sessionDirForIndex,
1865
2054
  sessionFileForIndex,
2055
+ sessionFileForTask,
1866
2056
  shareEnabled,
1867
2057
  artifactConfig,
1868
2058
  artifactsDir,
@@ -1870,6 +2060,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1870
2060
  onUpdate,
1871
2061
  sessionRoot,
1872
2062
  controlConfig,
2063
+ contextPolicy,
1873
2064
  } = data;
1874
2065
  const onControlEvent = createForegroundControlNotifier(data, deps);
1875
2066
  const childIntercomTarget = data.intercomBridge.active ? resolveSubagentIntercomTarget : undefined;
@@ -1972,6 +2163,9 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1972
2163
  }
1973
2164
 
1974
2165
  if (result.runInBackground) {
2166
+ if (data.timeoutMs !== undefined) {
2167
+ return buildRequestedModeError(params, "timeoutMs/maxRuntimeMs are only supported for foreground runs; background launch from clarify cannot preserve the timeout.");
2168
+ }
1975
2169
  if (!isAsyncAvailable()) {
1976
2170
  return {
1977
2171
  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 +2178,12 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1984
2178
  pi: deps.pi,
1985
2179
  cwd: ctx.cwd,
1986
2180
  currentSessionId: deps.state.currentSessionId!,
2181
+ parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
1987
2182
  currentModelProvider: ctx.model?.provider,
1988
2183
  currentModel: ctx.model,
1989
2184
  };
1990
2185
  const parallelTasks = tasks.map((t, i) => {
1991
- const taskText = params.context === "fork" ? wrapForkTask(taskTexts[i]!) : taskTexts[i]!;
2186
+ const taskText = shouldForkAgent(contextPolicy, t.agent) ? wrapForkTask(taskTexts[i]!) : taskTexts[i]!;
1992
2187
  const progress = taskDisallowsFileUpdates(taskText) ? false : behaviorOverrides[i]?.progress;
1993
2188
  return {
1994
2189
  agent: t.agent,
@@ -2016,7 +2211,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2016
2211
  shareEnabled,
2017
2212
  sessionRoot,
2018
2213
  chainSkills: [],
2019
- sessionFilesByFlatIndex: tasks.map((_, index) => sessionFileForIndex(index)),
2214
+ sessionFilesByFlatIndex: tasks.map((task, index) => sessionFileForTask(task.agent, index)),
2020
2215
  maxSubagentDepth: currentMaxSubagentDepth,
2021
2216
  worktreeSetupHook: deps.config.worktreeSetupHook,
2022
2217
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
@@ -2059,14 +2254,14 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2059
2254
  }
2060
2255
 
2061
2256
  const parallelProgressPrecreated = firstProgressIndex !== -1;
2062
- if (parallelProgressPrecreated) writeInitialProgressFile(effectiveCwd);
2257
+ const parallelProgressDir = path.join(artifactsDir, "progress", runId);
2258
+ if (parallelProgressPrecreated) writeInitialProgressFile(parallelProgressDir);
2063
2259
 
2064
- if (params.context === "fork") {
2065
- for (let i = 0; i < taskTexts.length; i++) {
2066
- taskTexts[i] = wrapForkTask(taskTexts[i]!);
2067
- }
2260
+ for (let i = 0; i < taskTexts.length; i++) {
2261
+ if (shouldForkAgent(contextPolicy, tasks[i]!.agent)) taskTexts[i] = wrapForkTask(taskTexts[i]!);
2068
2262
  }
2069
2263
 
2264
+ const deadlineAt = data.deadlineAt ?? (data.timeoutMs !== undefined ? Date.now() + data.timeoutMs : undefined);
2070
2265
  const results = await runForegroundParallelTasks({
2071
2266
  tasks,
2072
2267
  taskTexts,
@@ -2077,11 +2272,13 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2077
2272
  runId,
2078
2273
  sessionDirForIndex,
2079
2274
  sessionFileForIndex,
2275
+ sessionFileForTask,
2080
2276
  shareEnabled,
2081
2277
  artifactConfig,
2082
2278
  artifactsDir,
2083
2279
  maxOutput: params.maxOutput,
2084
2280
  paramsCwd: effectiveCwd,
2281
+ progressDir: parallelProgressDir,
2085
2282
  availableModels,
2086
2283
  modelOverrides,
2087
2284
  behaviors,
@@ -2097,6 +2294,8 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2097
2294
  liveProgress,
2098
2295
  onUpdate,
2099
2296
  worktreeSetup,
2297
+ timeoutMs: data.timeoutMs,
2298
+ deadlineAt,
2100
2299
  });
2101
2300
  for (let i = 0; i < results.length; i++) {
2102
2301
  const run = results[i]!;
@@ -2157,6 +2356,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2157
2356
  output: result.truncation?.text || getSingleResultOutput(result),
2158
2357
  exitCode: result.exitCode,
2159
2358
  error: result.error,
2359
+ timedOut: result.timedOut,
2160
2360
  })),
2161
2361
  (i, agent) => `=== Task ${i + 1}: ${agent} ===`,
2162
2362
  );
@@ -2184,13 +2384,14 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2184
2384
  signal,
2185
2385
  runId,
2186
2386
  sessionDirForIndex,
2187
- sessionFileForIndex,
2387
+ sessionFileForTask,
2188
2388
  shareEnabled,
2189
2389
  artifactConfig,
2190
2390
  artifactsDir,
2191
2391
  onUpdate,
2192
2392
  sessionRoot,
2193
2393
  controlConfig,
2394
+ contextPolicy,
2194
2395
  } = data;
2195
2396
  const onControlEvent = createForegroundControlNotifier(data, deps);
2196
2397
  const childIntercomTarget = data.intercomBridge.active ? resolveSubagentIntercomTarget(runId, params.agent!, 0) : undefined;
@@ -2254,6 +2455,9 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2254
2455
  if (override?.skills !== undefined) skillOverride = override.skills;
2255
2456
 
2256
2457
  if (result.runInBackground) {
2458
+ if (data.timeoutMs !== undefined) {
2459
+ return buildRequestedModeError(params, "timeoutMs/maxRuntimeMs are only supported for foreground runs; background launch from clarify cannot preserve the timeout.");
2460
+ }
2257
2461
  if (!isAsyncAvailable()) {
2258
2462
  return {
2259
2463
  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 +2470,13 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2266
2470
  pi: deps.pi,
2267
2471
  cwd: ctx.cwd,
2268
2472
  currentSessionId: deps.state.currentSessionId!,
2473
+ parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
2269
2474
  currentModelProvider: ctx.model?.provider,
2270
2475
  currentModel: ctx.model,
2271
2476
  };
2272
2477
  return executeAsyncSingle(id, {
2273
2478
  agent: params.agent!,
2274
- task: params.context === "fork" ? wrapForkTask(task) : task,
2479
+ task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(task) : task,
2275
2480
  agentConfig,
2276
2481
  ctx: asyncCtx,
2277
2482
  availableModels,
@@ -2281,7 +2486,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2281
2486
  artifactConfig,
2282
2487
  shareEnabled,
2283
2488
  sessionRoot,
2284
- sessionFile: sessionFileForIndex(0),
2489
+ sessionFile: sessionFileForTask(params.agent!, 0),
2285
2490
  skills: skillOverride === false ? [] : skillOverride,
2286
2491
  output: effectiveOutput,
2287
2492
  outputMode: effectiveOutputMode,
@@ -2296,7 +2501,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2296
2501
  }
2297
2502
  }
2298
2503
 
2299
- if (params.context === "fork") {
2504
+ if (shouldForkAgent(contextPolicy, params.agent!)) {
2300
2505
  task = wrapForkTask(task);
2301
2506
  }
2302
2507
  const cleanTask = task;
@@ -2349,7 +2554,9 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2349
2554
  }
2350
2555
  : undefined;
2351
2556
 
2557
+ const deadlineAt = data.deadlineAt ?? (data.timeoutMs !== undefined ? Date.now() + data.timeoutMs : undefined);
2352
2558
  const r = await runSync(ctx.cwd, agents, params.agent!, task, {
2559
+ parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
2353
2560
  cwd: effectiveCwd,
2354
2561
  signal,
2355
2562
  interruptSignal: interruptController.signal,
@@ -2357,7 +2564,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2357
2564
  intercomEvents: deps.pi.events,
2358
2565
  runId,
2359
2566
  sessionDir: sessionDirForIndex(0),
2360
- sessionFile: sessionFileForIndex(0),
2567
+ sessionFile: sessionFileForTask(params.agent!, 0),
2361
2568
  share: shareEnabled,
2362
2569
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
2363
2570
  artifactConfig,
@@ -2378,6 +2585,8 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2378
2585
  skills: effectiveSkills,
2379
2586
  acceptance: params.acceptance,
2380
2587
  acceptanceContext: { mode: "single" },
2588
+ timeoutMs: data.timeoutMs,
2589
+ deadlineAt,
2381
2590
  });
2382
2591
  if (foregroundControl?.currentIndex === 0) {
2383
2592
  foregroundControl.interrupt = undefined;
@@ -2462,6 +2671,23 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2462
2671
  };
2463
2672
  }
2464
2673
 
2674
+ function inferExecutionMode(params: SubagentParamsLike): SubagentRunMode {
2675
+ if ((params.chain?.length ?? 0) > 0) return "chain";
2676
+ if ((params.tasks?.length ?? 0) > 0) return "parallel";
2677
+ return "single";
2678
+ }
2679
+
2680
+ function duplicateSubagentCallResult(params: SubagentParamsLike): AgentToolResult<Details> {
2681
+ return {
2682
+ content: [{
2683
+ type: "text",
2684
+ text: "Rejected: a subagent call is already in progress. Issue exactly ONE subagent call per turn.",
2685
+ }],
2686
+ isError: true,
2687
+ details: { mode: inferExecutionMode(params), results: [] },
2688
+ };
2689
+ }
2690
+
2465
2691
  export function createSubagentExecutor(deps: ExecutorDeps): {
2466
2692
  execute: (
2467
2693
  id: string,
@@ -2498,7 +2724,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2498
2724
  let orchestratorTarget: string | undefined;
2499
2725
  try {
2500
2726
  orchestratorTarget = resolveIntercomSessionTarget(deps.pi.getSessionName(), ctx.sessionManager.getSessionId());
2501
- } catch {}
2727
+ } catch (error) {
2728
+ if (!sessionError) sessionError = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
2729
+ }
2502
2730
  return {
2503
2731
  content: [{
2504
2732
  type: "text",
@@ -2573,7 +2801,12 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2573
2801
  details: { mode: "management", results: [] },
2574
2802
  };
2575
2803
  }
2576
- const asyncInterruptResult = interruptAsyncRun(deps.state, resolved?.kind === "async" ? resolved.id : targetRunId, deps.kill);
2804
+ const asyncInterruptResult = interruptAsyncRun(
2805
+ deps.state,
2806
+ resolved?.kind === "async" ? resolved.id : targetRunId,
2807
+ deps.kill,
2808
+ resolved?.kind === "async" ? resolved.location : undefined,
2809
+ );
2577
2810
  if (asyncInterruptResult) return asyncInterruptResult;
2578
2811
  return {
2579
2812
  content: [{ type: "text", text: "No interrupt-capable run found in this session." }],
@@ -2624,13 +2857,16 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2624
2857
  depth,
2625
2858
  deps.config.forceTopLevelAsync === true,
2626
2859
  );
2860
+ const foregroundTimeout = resolveForegroundTimeout(effectiveParams);
2861
+ if (foregroundTimeout.error) return buildRequestedModeError(effectiveParams, foregroundTimeout.error);
2627
2862
 
2628
2863
  const scope: AgentScope = resolveExecutionAgentScope(effectiveParams.agentScope);
2629
2864
  const effectiveCwd = effectiveParams.cwd ?? ctx.cwd;
2630
2865
  const parentSessionFile = ctx.sessionManager.getSessionFile() ?? null;
2631
2866
  deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
2632
2867
  const discoveredAgents = deps.discoverAgents(effectiveCwd, scope).agents;
2633
- effectiveParams = applyAgentDefaultContext(effectiveParams, discoveredAgents);
2868
+ const contextPolicy = resolveAgentDefaultContextPolicy(effectiveParams, discoveredAgents);
2869
+ effectiveParams = contextPolicy.params;
2634
2870
  const sessionName = resolveIntercomSessionTarget(deps.pi.getSessionName(), ctx.sessionManager.getSessionId());
2635
2871
  const intercomBridge = resolveIntercomBridge({
2636
2872
  config: deps.config.intercomBridge,
@@ -2664,15 +2900,18 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2664
2900
  );
2665
2901
  if (validationError) return validationError;
2666
2902
 
2667
- let sessionFileForIndex: (idx?: number) => string | undefined = () => undefined;
2903
+ let forkSessionFileForIndex: (idx?: number) => string | undefined = () => undefined;
2668
2904
  try {
2669
- sessionFileForIndex = createForkContextResolver(ctx.sessionManager, effectiveParams.context).sessionFileForIndex;
2905
+ forkSessionFileForIndex = createForkContextResolver(ctx.sessionManager, contextPolicy.usesFork ? "fork" : undefined).sessionFileForIndex;
2670
2906
  } catch (error) {
2671
2907
  return toExecutionErrorResult(effectiveParams, error);
2672
2908
  }
2673
2909
  const requestedAsync = effectiveParams.async ?? deps.asyncByDefault;
2674
2910
  const backgroundRequestedWhileClarifying = (hasChain || hasTasks) && requestedAsync && effectiveParams.clarify === true;
2675
2911
  const effectiveAsync = requestedAsync && effectiveParams.clarify !== true;
2912
+ if (foregroundTimeout.timeoutMs !== undefined && effectiveAsync) {
2913
+ return buildRequestedModeError(effectiveParams, "timeoutMs/maxRuntimeMs are only supported for foreground runs; set async: false or omit the timeout for background runs.");
2914
+ }
2676
2915
  const controlConfig = resolveControlConfig(deps.config.control, effectiveParams.control);
2677
2916
 
2678
2917
  const artifactConfig: ArtifactConfig = {
@@ -2701,8 +2940,19 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2701
2940
  }
2702
2941
  const sessionDirForIndex = (idx?: number) =>
2703
2942
  path.join(sessionRoot, `run-${idx ?? 0}`);
2943
+ const forkSessionFileForTask = (agentName: string, idx?: number) =>
2944
+ shouldForkAgent(contextPolicy, agentName) ? forkSessionFileForIndex(idx) : undefined;
2945
+ const childSessionFileForTask = (agentName: string, idx?: number) =>
2946
+ forkSessionFileForTask(agentName, idx) ?? path.join(sessionDirForIndex(idx), "session.jsonl");
2704
2947
  const childSessionFileForIndex = (idx?: number) =>
2705
- sessionFileForIndex(idx) ?? path.join(sessionDirForIndex(idx), "session.jsonl");
2948
+ path.join(sessionDirForIndex(idx), "session.jsonl");
2949
+ try {
2950
+ preflightForkSessionsForStaticTasks(effectiveParams, contextPolicy, forkSessionFileForTask);
2951
+ } catch (error) {
2952
+ return toExecutionErrorResult(effectiveParams, error);
2953
+ }
2954
+ const chainBindingsError = validateExecutionChainBindings(effectiveParams, deps.config.chain?.dynamicFanout?.maxItems);
2955
+ if (chainBindingsError) return chainBindingsError;
2706
2956
 
2707
2957
  const onUpdateWithContext = onUpdate
2708
2958
  ? (r: AgentToolResult<Details>) => onUpdate(withForkContext(r, effectiveParams.context))
@@ -2720,6 +2970,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2720
2970
  sessionRoot,
2721
2971
  sessionDirForIndex,
2722
2972
  sessionFileForIndex: childSessionFileForIndex,
2973
+ sessionFileForTask: childSessionFileForTask,
2723
2974
  artifactConfig,
2724
2975
  artifactsDir,
2725
2976
  backgroundRequestedWhileClarifying,
@@ -2727,6 +2978,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2727
2978
  controlConfig,
2728
2979
  intercomBridge,
2729
2980
  nestedRoute,
2981
+ timeoutMs: foregroundTimeout.timeoutMs,
2982
+ contextPolicy,
2730
2983
  };
2731
2984
 
2732
2985
  const foregroundMode: "single" | "parallel" | "chain" = hasChain ? "chain" : hasTasks ? "parallel" : "single";
@@ -2851,5 +3104,22 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2851
3104
  }, effectiveParams.context);
2852
3105
  };
2853
3106
 
2854
- return { execute };
3107
+ const executeWithSingleDispatchGuard = async (
3108
+ id: string,
3109
+ params: SubagentParamsLike,
3110
+ signal: AbortSignal,
3111
+ onUpdate: ((r: AgentToolResult<Details>) => void) | undefined,
3112
+ ctx: ExtensionContext,
3113
+ ): Promise<AgentToolResult<Details>> => {
3114
+ if (params.action) return execute(id, params, signal, onUpdate, ctx);
3115
+ if (deps.state.subagentInProgress === true) return duplicateSubagentCallResult(params);
3116
+ deps.state.subagentInProgress = true;
3117
+ try {
3118
+ return await execute(id, params, signal, onUpdate, ctx);
3119
+ } finally {
3120
+ deps.state.subagentInProgress = false;
3121
+ }
3122
+ };
3123
+
3124
+ return { execute: executeWithSingleDispatchGuard };
2855
3125
  }