pi-subagents 0.28.0 → 0.30.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 (47) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +26 -62
  3. package/package.json +1 -1
  4. package/skills/pi-subagents/SKILL.md +29 -35
  5. package/src/agents/agent-management.ts +29 -22
  6. package/src/agents/agent-selection.ts +2 -0
  7. package/src/agents/agent-serializer.ts +5 -10
  8. package/src/agents/agents.ts +339 -47
  9. package/src/agents/chain-serializer.ts +4 -9
  10. package/src/agents/proactive-skills.ts +191 -0
  11. package/src/extension/doctor.ts +4 -3
  12. package/src/extension/fanout-child.ts +1 -3
  13. package/src/extension/index.ts +6 -9
  14. package/src/extension/schemas.ts +63 -26
  15. package/src/intercom/intercom-bridge.ts +11 -1
  16. package/src/intercom/result-intercom.ts +0 -5
  17. package/src/runs/background/async-execution.ts +186 -74
  18. package/src/runs/background/async-resume.ts +53 -5
  19. package/src/runs/background/async-status.ts +4 -1
  20. package/src/runs/background/chain-append.ts +282 -0
  21. package/src/runs/background/chain-root-attachment.ts +161 -0
  22. package/src/runs/background/run-status.ts +2 -7
  23. package/src/runs/background/subagent-runner.ts +160 -219
  24. package/src/runs/foreground/chain-execution.ts +62 -58
  25. package/src/runs/foreground/execution.ts +39 -343
  26. package/src/runs/foreground/subagent-executor.ts +316 -111
  27. package/src/runs/shared/acceptance.ts +605 -22
  28. package/src/runs/shared/chain-outputs.ts +23 -8
  29. package/src/runs/shared/completion-guard.ts +3 -26
  30. package/src/runs/shared/dynamic-fanout.ts +1 -1
  31. package/src/runs/shared/model-fallback.ts +38 -0
  32. package/src/runs/shared/parallel-utils.ts +13 -10
  33. package/src/runs/shared/pi-args.ts +3 -2
  34. package/src/runs/shared/subagent-control.ts +8 -11
  35. package/src/runs/shared/subagent-prompt-runtime.ts +3 -2
  36. package/src/runs/shared/workflow-graph.ts +2 -6
  37. package/src/shared/atomic-json.ts +68 -11
  38. package/src/shared/settings.ts +1 -0
  39. package/src/shared/types.ts +20 -49
  40. package/src/shared/utils.ts +2 -8
  41. package/src/slash/slash-bridge.ts +3 -1
  42. package/src/slash/slash-commands.ts +1 -1
  43. package/src/tui/render.ts +14 -29
  44. package/src/runs/shared/acceptance-contract.ts +0 -318
  45. package/src/runs/shared/acceptance-evaluation.ts +0 -221
  46. package/src/runs/shared/acceptance-finalization.ts +0 -173
  47. package/src/runs/shared/acceptance-reports.ts +0 -127
@@ -13,7 +13,7 @@ import { handleManagementAction } from "../../agents/agent-management.ts";
13
13
  import { buildDoctorReport } from "../../extension/doctor.ts";
14
14
  import { clearPendingForegroundControlNotices } from "../../extension/control-notices.ts";
15
15
  import { runSync } from "./execution.ts";
16
- import { resolveModelCandidate } from "../shared/model-fallback.ts";
16
+ import { resolveModelCandidate, resolveSubagentModelOverride } from "../shared/model-fallback.ts";
17
17
  import { aggregateParallelOutputs } from "../shared/parallel-utils.ts";
18
18
  import { recordRun } from "../shared/run-history.ts";
19
19
  import {
@@ -31,7 +31,9 @@ import {
31
31
  type StepOverrides,
32
32
  } from "../../shared/settings.ts";
33
33
  import { discoverAvailableSkills, normalizeSkillInput } from "../../agents/skills.ts";
34
- import { executeAsyncChain, executeAsyncSingle, formatAsyncStartedMessage, isAsyncAvailable } from "../background/async-execution.ts";
34
+ import { buildAsyncRunnerSteps, executeAsyncChain, executeAsyncSingle, formatAsyncStartedMessage, isAsyncAvailable } from "../background/async-execution.ts";
35
+ import { enqueueChainAppendRequest, readPendingChainAppendRequests, runnerStepOutputNames } from "../background/chain-append.ts";
36
+ import { ChainOutputValidationError, validateChainOutputBindingsWithContext } from "../shared/chain-outputs.ts";
35
37
  import { createForkContextResolver } from "../../shared/fork-context.ts";
36
38
  import { resolveCurrentSessionId } from "../../shared/session-identity.ts";
37
39
  import { applyIntercomBridgeToAgent, INTERCOM_BRIDGE_MARKER, resolveIntercomBridge, resolveIntercomSessionTarget, resolveSubagentIntercomTarget, type IntercomBridgeState } from "../../intercom/intercom-bridge.ts";
@@ -47,13 +49,13 @@ import {
47
49
  resolveSubagentResultStatus,
48
50
  stripDetailsOutputsForIntercomReceipt,
49
51
  } from "../../intercom/result-intercom.ts";
50
- import { buildRevivedAsyncTask, resolveAsyncResumeTarget } from "../background/async-resume.ts";
52
+ import { buildRevivedAsyncTask, interruptLiveAsyncResumeTarget, resolveAsyncResumeTarget } from "../background/async-resume.ts";
53
+ import { resolveAsyncRootResultPath } from "../background/chain-root-attachment.ts";
51
54
  import { createNestedRoute, readNestedControlResults, resolveInheritedNestedRouteFromEnv, resolveNestedAsyncDir, resolveNestedParentAddressFromEnv, updateForegroundNestedProjection, writeNestedControlRequest, writeNestedEvent, type NestedRunResolutionScope } from "../shared/nested-events.ts";
52
55
  import { resolveSubagentRunId, type ResolvedSubagentRunId } from "../background/run-id-resolver.ts";
53
56
  import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
54
57
  import { inspectSubagentStatus } from "../background/run-status.ts";
55
58
  import { applyForceTopLevelAsyncOverride } from "../background/top-level-async.ts";
56
- import { validateAcceptanceInput } from "../shared/acceptance.ts";
57
59
  import {
58
60
  cleanupWorktrees,
59
61
  createWorktrees,
@@ -80,7 +82,9 @@ import {
80
82
  type SingleResult,
81
83
  type SubagentRunMode,
82
84
  type SubagentState,
85
+ ASYNC_DIR,
83
86
  DEFAULT_ARTIFACT_CONFIG,
87
+ RESULTS_DIR,
84
88
  SUBAGENT_ACTIONS,
85
89
  SUBAGENT_CONTROL_EVENT,
86
90
  SUBAGENT_CONTROL_INTERCOM_EVENT,
@@ -121,8 +125,6 @@ export interface SubagentParamsLike {
121
125
  chain?: ChainStep[];
122
126
  tasks?: TaskParam[];
123
127
  concurrency?: number;
124
- timeoutMs?: number;
125
- maxRuntimeMs?: number;
126
128
  worktree?: boolean;
127
129
  context?: "fresh" | "fork";
128
130
  async?: boolean;
@@ -153,6 +155,7 @@ interface ExecutorDeps {
153
155
  expandTilde: (p: string) => string;
154
156
  discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[] };
155
157
  allowMutatingManagementActions?: boolean;
158
+ kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
156
159
  }
157
160
 
158
161
  interface ExecutionContextData {
@@ -171,7 +174,6 @@ interface ExecutionContextData {
171
174
  artifactsDir: string;
172
175
  backgroundRequestedWhileClarifying: boolean;
173
176
  effectiveAsync: boolean;
174
- foregroundTimeoutMs?: number;
175
177
  controlConfig: ResolvedControlConfig;
176
178
  intercomBridge: IntercomBridgeState;
177
179
  nestedRoute?: NestedRouteInfo;
@@ -253,7 +255,7 @@ function rememberForegroundRun(state: SubagentState, input: { runId: string; mod
253
255
  children: input.results.map((result, index) => ({
254
256
  agent: result.agent,
255
257
  index,
256
- status: resolveSubagentResultStatus({ exitCode: result.exitCode, interrupted: result.interrupted, detached: result.detached, timedOut: result.timedOut }),
258
+ status: resolveSubagentResultStatus({ exitCode: result.exitCode, interrupted: result.interrupted, detached: result.detached }),
257
259
  ...(result.sessionFile ? { sessionFile: result.sessionFile } : {}),
258
260
  })),
259
261
  });
@@ -321,7 +323,7 @@ function isExactResumeError(error: unknown, source: "async" | "foreground", requ
321
323
  return new RegExp(`\\b${source} run '${escapeRegExp(requested)}'`, "i").test(error.message);
322
324
  }
323
325
 
324
- function resolveResumeTarget(params: SubagentParamsLike, state: SubagentState): ResumeSourceTarget {
326
+ function resolveResumeTarget(params: SubagentParamsLike, state: SubagentState, options: { asyncRequireSessionFile?: boolean } = {}): ResumeSourceTarget {
325
327
  const requested = (params.id ?? params.runId)?.trim() ?? "";
326
328
  let foregroundTarget: ForegroundResumeSourceTarget | undefined;
327
329
  let foregroundError: unknown;
@@ -335,7 +337,7 @@ function resolveResumeTarget(params: SubagentParamsLike, state: SubagentState):
335
337
  foregroundError = error;
336
338
  }
337
339
  try {
338
- asyncTarget = { source: "async", ...resolveAsyncResumeTarget(params) };
340
+ asyncTarget = { source: "async", ...resolveAsyncResumeTarget(params, {}, { requireSessionFile: options.asyncRequireSessionFile }) };
339
341
  } catch (error) {
340
342
  asyncError = error;
341
343
  }
@@ -406,7 +408,7 @@ function emitControlNotification(input: {
406
408
  }
407
409
  }
408
410
 
409
- function interruptAsyncRun(state: SubagentState, runId: string | undefined): AgentToolResult<Details> | null {
411
+ function interruptAsyncRun(state: SubagentState, runId: string | undefined, kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean): AgentToolResult<Details> | null {
410
412
  const target = getAsyncInterruptTarget(state, runId);
411
413
  if (!target) return null;
412
414
  const status = readStatus(target.asyncDir);
@@ -418,7 +420,7 @@ function interruptAsyncRun(state: SubagentState, runId: string | undefined): Age
418
420
  };
419
421
  }
420
422
  try {
421
- process.kill(status.pid, ASYNC_INTERRUPT_SIGNAL);
423
+ (kill ?? process.kill)(status.pid, ASYNC_INTERRUPT_SIGNAL);
422
424
  const tracked = state.asyncJobs.get(target.asyncId);
423
425
  if (tracked) {
424
426
  tracked.activityState = undefined;
@@ -438,6 +440,186 @@ function interruptAsyncRun(state: SubagentState, runId: string | undefined): Age
438
440
  }
439
441
  }
440
442
 
443
+ function duplicateNames(names: string[]): string[] {
444
+ const seen = new Set<string>();
445
+ const duplicates = new Set<string>();
446
+ for (const name of names) {
447
+ if (seen.has(name)) duplicates.add(name);
448
+ else seen.add(name);
449
+ }
450
+ return [...duplicates];
451
+ }
452
+
453
+ function appendStepToAsyncChain(input: {
454
+ params: SubagentParamsLike;
455
+ requestCwd: string;
456
+ ctx: ExtensionContext;
457
+ deps: ExecutorDeps;
458
+ }): AgentToolResult<Details> {
459
+ const targetRunId = input.params.id ?? input.params.runId;
460
+ if (!targetRunId) {
461
+ return {
462
+ content: [{ type: "text", text: "action='append-step' requires id." }],
463
+ isError: true,
464
+ details: { mode: "management", results: [] },
465
+ };
466
+ }
467
+ if (!input.params.chain || input.params.chain.length !== 1) {
468
+ return {
469
+ content: [{ type: "text", text: "action='append-step' requires chain with exactly one step." }],
470
+ isError: true,
471
+ details: { mode: "management", results: [] },
472
+ };
473
+ }
474
+
475
+ let resolved: ResolvedSubagentRunId | undefined;
476
+ try {
477
+ resolved = resolveSubagentRunId(targetRunId, { state: input.deps.state, nested: nestedResolutionScopeForExecutor(input.deps) });
478
+ } catch (error) {
479
+ const message = error instanceof Error ? error.message : String(error);
480
+ return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
481
+ }
482
+ if (!resolved) {
483
+ return {
484
+ content: [{ type: "text", text: `No async chain run found for '${targetRunId}'.` }],
485
+ isError: true,
486
+ details: { mode: "management", results: [] },
487
+ };
488
+ }
489
+ if (resolved.kind !== "async" || !resolved.location.asyncDir) {
490
+ return {
491
+ content: [{ type: "text", text: `Run '${resolved.id}' is not an append-capable async chain run.` }],
492
+ isError: true,
493
+ details: { mode: "management", results: [] },
494
+ };
495
+ }
496
+
497
+ const status = readStatus(resolved.location.asyncDir);
498
+ if (!status) {
499
+ return {
500
+ content: [{ type: "text", text: `No async run status found for '${resolved.id}'.` }],
501
+ isError: true,
502
+ details: { mode: "management", results: [] },
503
+ };
504
+ }
505
+ if (status.mode !== "chain") {
506
+ return {
507
+ content: [{ type: "text", text: `Run '${resolved.id}' is ${status.mode}; only active chain runs accept appended steps.` }],
508
+ isError: true,
509
+ details: { mode: "management", results: [] },
510
+ };
511
+ }
512
+ if (status.state !== "running") {
513
+ return {
514
+ content: [{ type: "text", text: `Run '${resolved.id}' is ${status.state}; only running chain runs accept appended steps.` }],
515
+ isError: true,
516
+ details: { mode: "management", results: [] },
517
+ };
518
+ }
519
+ const stillInProgress = (status.steps ?? []).some((step) => step.status === "running" || step.status === "pending") || (status.pendingAppends ?? 0) > 0;
520
+ if (!stillInProgress) {
521
+ return {
522
+ content: [{ type: "text", text: `Run '${resolved.id}' has no running or pending chain steps left; append-step must target an in-progress chain.` }],
523
+ isError: true,
524
+ details: { mode: "management", results: [] },
525
+ };
526
+ }
527
+
528
+ const pendingAppendRequests = readPendingChainAppendRequests(resolved.location.asyncDir);
529
+ const reservedOutputNames = new Set<string>([
530
+ ...Object.keys(status.outputs ?? {}),
531
+ ...(status.steps ?? []).map((step) => step.outputName).filter((name): name is string => Boolean(name)),
532
+ ...pendingAppendRequests.flatMap((request) => runnerStepOutputNames(request.steps)),
533
+ ]);
534
+ try {
535
+ validateChainOutputBindingsWithContext(input.params.chain, { maxItems: input.deps.config.chain?.dynamicFanout?.maxItems }, {
536
+ priorOutputNames: reservedOutputNames,
537
+ startStepIndex: status.chainStepCount ?? status.steps?.length ?? 0,
538
+ });
539
+ } catch (error) {
540
+ if (!(error instanceof ChainOutputValidationError)) throw error;
541
+ return {
542
+ content: [{ type: "text", text: `Cannot append step to run '${resolved.id}': ${error.message}` }],
543
+ isError: true,
544
+ details: { mode: "management", results: [] },
545
+ };
546
+ }
547
+
548
+ const scope: AgentScope = resolveExecutionAgentScope(input.params.agentScope);
549
+ const agents = input.deps.discoverAgents(input.requestCwd, scope).agents;
550
+ const chainSkillInput = normalizeSkillInput(input.params.skill);
551
+ const chainSkills = chainSkillInput === false ? [] : (chainSkillInput ?? []);
552
+ const asyncCtx = {
553
+ pi: input.deps.pi,
554
+ cwd: input.ctx.cwd,
555
+ currentSessionId: resolveCurrentSessionId(input.ctx.sessionManager),
556
+ currentModelProvider: input.ctx.model?.provider,
557
+ currentModel: input.ctx.model,
558
+ };
559
+ const built = buildAsyncRunnerSteps(resolved.id, {
560
+ chain: wrapChainTasksForFork(input.params.chain, input.params.context),
561
+ task: input.params.task,
562
+ resultMode: "chain",
563
+ agents,
564
+ ctx: asyncCtx,
565
+ availableModels: input.ctx.modelRegistry.getAvailable().map(toModelInfo),
566
+ cwd: status.cwd ?? input.requestCwd,
567
+ chainSkills,
568
+ dynamicFanoutMaxItems: input.deps.config.chain?.dynamicFanout?.maxItems,
569
+ maxSubagentDepth: resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth),
570
+ asyncDir: resolved.location.asyncDir,
571
+ validateOutputBindings: false,
572
+ });
573
+ if ("error" in built) {
574
+ return {
575
+ content: [{ type: "text", text: built.error }],
576
+ isError: true,
577
+ details: { mode: "management", results: [] },
578
+ };
579
+ }
580
+ const appendedOutputNames = runnerStepOutputNames(built.steps);
581
+ const duplicateAppendedOutputs = duplicateNames(appendedOutputNames);
582
+ if (duplicateAppendedOutputs.length > 0) {
583
+ return {
584
+ content: [{ type: "text", text: `Cannot append step to run '${resolved.id}': duplicate output name in appended step: ${duplicateAppendedOutputs.join(", ")}.` }],
585
+ isError: true,
586
+ details: { mode: "management", results: [] },
587
+ };
588
+ }
589
+ const pendingOutputNames = new Set(pendingAppendRequests.flatMap((request) => runnerStepOutputNames(request.steps)));
590
+ const pendingDuplicateOutputs = appendedOutputNames.filter((name) => pendingOutputNames.has(name));
591
+ if (pendingDuplicateOutputs.length > 0) {
592
+ return {
593
+ content: [{ type: "text", text: `Cannot append step to run '${resolved.id}': output name already belongs to a pending append: ${pendingDuplicateOutputs.join(", ")}.` }],
594
+ isError: true,
595
+ details: { mode: "management", results: [] },
596
+ };
597
+ }
598
+
599
+ try {
600
+ const result = enqueueChainAppendRequest({
601
+ asyncDir: resolved.location.asyncDir,
602
+ runId: resolved.id,
603
+ steps: built.steps,
604
+ });
605
+ const stepText = built.steps.length === 1 ? "step" : "steps";
606
+ return {
607
+ content: [{
608
+ type: "text",
609
+ text: `Append queued for chain run ${resolved.id}: ${built.steps.length} ${stepText}. It becomes eligible after the chain's already-queued steps finish. Pending appends: ${result.pendingCount}.`,
610
+ }],
611
+ details: { mode: "management", results: [], asyncId: resolved.id, asyncDir: resolved.location.asyncDir },
612
+ };
613
+ } catch (error) {
614
+ const message = error instanceof Error ? error.message : String(error);
615
+ return {
616
+ content: [{ type: "text", text: `Failed to append step to chain run ${resolved.id}: ${message}` }],
617
+ isError: true,
618
+ details: { mode: "management", results: [] },
619
+ };
620
+ }
621
+ }
622
+
441
623
  function nestedRunSessionFile(run: NestedRunSummary): string | undefined {
442
624
  return run.sessionFile ?? (run.steps?.length === 1 ? run.steps[0]?.sessionFile : undefined);
443
625
  }
@@ -558,7 +740,8 @@ async function resumeAsyncRun(input: {
558
740
  deps: ExecutorDeps;
559
741
  }): Promise<AgentToolResult<Details>> {
560
742
  const followUp = (input.params.message ?? input.params.task ?? "").trim();
561
- if (!followUp) {
743
+ const attachChain = (input.params.chain?.length ?? 0) > 0 ? input.params.chain as ChainStep[] : undefined;
744
+ if (!followUp && !attachChain) {
562
745
  return {
563
746
  content: [{ type: "text", text: "action='resume' requires message." }],
564
747
  isError: true,
@@ -572,6 +755,13 @@ async function resumeAsyncRun(input: {
572
755
  const requestedId = input.params.id ?? input.params.runId;
573
756
  const resolved = requestedId ? resolveSubagentRunId(requestedId, { state: input.deps.state, nested: nestedResolutionScopeForExecutor(input.deps) }) : undefined;
574
757
  if (resolved?.kind === "nested") {
758
+ if (attachChain) {
759
+ return {
760
+ content: [{ type: "text", text: "Attaching a running subagent as a chain root is currently available for top-level async runs only." }],
761
+ isError: true,
762
+ details: { mode: "management", results: [] },
763
+ };
764
+ }
575
765
  if (resolved.match.run.state === "running" || resolved.match.run.state === "queued") {
576
766
  return resumeLiveNestedRun({ target: resolved, message: followUp });
577
767
  }
@@ -581,14 +771,26 @@ async function resumeAsyncRun(input: {
581
771
  ];
582
772
  target = resolveNestedResumeTarget(resolved, trustedSessionRoots);
583
773
  } else {
584
- target = resolveResumeTarget(input.params, input.deps.state);
774
+ target = resolveResumeTarget(input.params, input.deps.state, { asyncRequireSessionFile: !attachChain });
585
775
  }
586
776
  } catch (error) {
587
777
  const message = error instanceof Error ? error.message : String(error);
588
778
  return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
589
779
  }
590
780
 
591
- if (target.kind === "live") {
781
+ if (target.kind === "live" && !attachChain) {
782
+ const interrupt = interruptLiveAsyncResumeTarget({
783
+ target,
784
+ state: input.deps.state,
785
+ kill: input.deps.kill,
786
+ });
787
+ if (!interrupt.ok) {
788
+ return {
789
+ content: [{ type: "text", text: interrupt.message }],
790
+ isError: true,
791
+ details: { mode: "management", results: [] },
792
+ };
793
+ }
592
794
  const delivered = await deliverSubagentIntercomMessageEvent(
593
795
  input.deps.pi.events,
594
796
  target.intercomTarget,
@@ -598,7 +800,7 @@ async function resumeAsyncRun(input: {
598
800
  );
599
801
  if (delivered) {
600
802
  return {
601
- content: [{ type: "text", text: [`Delivered follow-up to live async child.`, `Run: ${target.runId}`, `Intercom target: ${target.intercomTarget}`].join("\n") }],
803
+ content: [{ type: "text", text: [`Interrupted live async child, then delivered follow-up.`, `Run: ${target.runId}`, `Intercom target: ${target.intercomTarget}`].join("\n") }],
602
804
  details: { mode: "management", results: [] },
603
805
  };
604
806
  }
@@ -641,6 +843,73 @@ async function resumeAsyncRun(input: {
641
843
  };
642
844
  }
643
845
 
846
+ if (attachChain) {
847
+ if (target.source !== "async") {
848
+ return {
849
+ content: [{ type: "text", text: "Attaching a running subagent as a chain root is currently available for async runs only." }],
850
+ isError: true,
851
+ details: { mode: "management", results: [] },
852
+ };
853
+ }
854
+ if (!isAsyncAvailable()) {
855
+ return {
856
+ content: [{ type: "text", text: "Async mode requires upstream jiti for TypeScript execution but it could not be found. Ensure the pi-subagents package dependencies are installed." }],
857
+ isError: true,
858
+ details: { mode: "chain", results: [] },
859
+ };
860
+ }
861
+ const runId = randomUUID().slice(0, 8);
862
+ const artifactConfig: ArtifactConfig = { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false };
863
+ const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo);
864
+ const chain = wrapChainTasksForFork(attachChain, input.params.context);
865
+ const normalized = normalizeSkillInput(input.params.skill);
866
+ const result = executeAsyncChain(runId, {
867
+ chain,
868
+ task: (input.params.task ?? followUp) || undefined,
869
+ attachRoot: {
870
+ runId: target.runId,
871
+ asyncDir: target.asyncDir ?? path.join(ASYNC_DIR, target.runId),
872
+ resultPath: resolveAsyncRootResultPath(RESULTS_DIR, target.runId),
873
+ index: target.index,
874
+ agent: target.agent,
875
+ label: `Attached ${target.runId}`,
876
+ },
877
+ agents,
878
+ ctx: {
879
+ pi: input.deps.pi,
880
+ cwd: input.requestCwd,
881
+ currentSessionId: input.deps.state.currentSessionId,
882
+ currentModelProvider: input.ctx.model?.provider,
883
+ currentModel: input.ctx.model,
884
+ },
885
+ availableModels,
886
+ cwd: effectiveCwd,
887
+ maxOutput: input.params.maxOutput,
888
+ artifactsDir: input.deps.tempArtifactsDir,
889
+ artifactConfig,
890
+ shareEnabled: input.params.share === true,
891
+ sessionRoot: input.deps.getSubagentSessionRoot(parentSessionFile),
892
+ chainSkills: normalized === false ? [] : (normalized ?? []),
893
+ dynamicFanoutMaxItems: input.deps.config.chain?.dynamicFanout?.maxItems,
894
+ maxSubagentDepth: resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth),
895
+ worktreeSetupHook: input.deps.config.worktreeSetupHook,
896
+ worktreeSetupHookTimeoutMs: input.deps.config.worktreeSetupHookTimeoutMs,
897
+ controlConfig: resolveControlConfig(input.deps.config.control, input.params.control),
898
+ controlIntercomTarget: intercomBridge.active ? intercomBridge.orchestratorTarget : undefined,
899
+ childIntercomTarget: intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
900
+ });
901
+ if (result.isError) return result;
902
+ const attachedId = result.details.asyncId ?? runId;
903
+ const lines = [
904
+ `Attached async subagent ${target.runId} as the first step of a new chain.`,
905
+ `Chain run: ${attachedId}`,
906
+ `Root: ${target.agent} (step ${target.index + 1})`,
907
+ result.details.asyncDir ? `Async dir: ${result.details.asyncDir}` : undefined,
908
+ `Status if needed: subagent({ action: "status", id: "${attachedId}" })`,
909
+ ].filter((line): line is string => Boolean(line));
910
+ return { content: [{ type: "text", text: formatAsyncStartedMessage(lines.join("\n")) }], details: result.details };
911
+ }
912
+
644
913
  const runId = randomUUID().slice(0, 8);
645
914
  const artifactConfig: ArtifactConfig = { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false };
646
915
  const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo);
@@ -653,6 +922,7 @@ async function resumeAsyncRun(input: {
653
922
  cwd: input.requestCwd,
654
923
  currentSessionId: input.deps.state.currentSessionId,
655
924
  currentModelProvider: input.ctx.model?.provider,
925
+ currentModel: input.ctx.model,
656
926
  },
657
927
  cwd: effectiveCwd,
658
928
  maxOutput: input.params.maxOutput,
@@ -694,6 +964,19 @@ function resultSummaryForIntercom(result: SingleResult): string {
694
964
  return output || result.error || "(no output)";
695
965
  }
696
966
 
967
+ function formatFailedSingleRunOutput(result: SingleResult, displayOutput: string): string {
968
+ const error = result.error || "Failed";
969
+ const output = displayOutput.trim();
970
+ const lines = [error];
971
+ if (output && output !== error.trim()) {
972
+ lines.push("", "Output:", output);
973
+ }
974
+ if (result.artifactPaths?.outputPath) {
975
+ lines.push("", `Output artifact: ${result.artifactPaths.outputPath}`);
976
+ }
977
+ return lines.join("\n");
978
+ }
979
+
697
980
  function createForegroundControlNotifier(data: Pick<ExecutionContextData, "controlConfig" | "intercomBridge">, deps: Pick<ExecutorDeps, "pi">): (event: ControlEvent) => void {
698
981
  return (event) => emitControlNotification({
699
982
  pi: deps.pi,
@@ -719,7 +1002,6 @@ async function emitForegroundResultIntercom(input: {
719
1002
  exitCode: result.exitCode,
720
1003
  interrupted: result.interrupted,
721
1004
  detached: result.detached,
722
- timedOut: result.timedOut,
723
1005
  }),
724
1006
  summary: resultSummaryForIntercom(result),
725
1007
  index,
@@ -765,51 +1047,6 @@ async function maybeBuildForegroundIntercomReceipt(input: {
765
1047
  };
766
1048
  }
767
1049
 
768
- function validationErrorResult(mode: Details["mode"], text: string): AgentToolResult<Details> {
769
- return { content: [{ type: "text", text }], isError: true, details: { mode, results: [] } };
770
- }
771
-
772
- function resolveForegroundTimeoutMs(params: SubagentParamsLike): { timeoutMs?: number; error?: string } {
773
- const rawTimeout = (params as { timeoutMs?: unknown }).timeoutMs;
774
- const rawMaxRuntime = (params as { maxRuntimeMs?: unknown }).maxRuntimeMs;
775
- for (const [name, value] of [["timeoutMs", rawTimeout], ["maxRuntimeMs", rawMaxRuntime]] as const) {
776
- if (value !== undefined && (typeof value !== "number" || !Number.isInteger(value) || value < 1)) {
777
- return { error: `${name} must be a positive integer.` };
778
- }
779
- }
780
- if (rawTimeout !== undefined && rawMaxRuntime !== undefined && rawTimeout !== rawMaxRuntime) {
781
- return { error: "timeoutMs and maxRuntimeMs are aliases; provide only one or use identical values." };
782
- }
783
- const timeoutMs = rawTimeout ?? rawMaxRuntime;
784
- return timeoutMs === undefined ? {} : { timeoutMs };
785
- }
786
-
787
- function validateAcceptanceForExecution(params: SubagentParamsLike): AgentToolResult<Details> | null {
788
- const topLevelErrors = validateAcceptanceInput(params.acceptance);
789
- if (topLevelErrors.length > 0) return validationErrorResult("single", topLevelErrors.join(" "));
790
- for (const [index, task] of (params.tasks ?? []).entries()) {
791
- const errors = validateAcceptanceInput(task.acceptance, `tasks[${index}].acceptance`);
792
- if (errors.length > 0) return validationErrorResult("parallel", errors.join(" "));
793
- }
794
- for (const [stepIndex, step] of (params.chain ?? []).entries()) {
795
- if (isParallelStep(step)) {
796
- if (Object.hasOwn(step, "acceptance")) return validationErrorResult("chain", `chain[${stepIndex}].acceptance is not supported on static parallel groups; set acceptance on each parallel task.`);
797
- for (const [taskIndex, task] of step.parallel.entries()) {
798
- const errors = validateAcceptanceInput(task.acceptance, `chain[${stepIndex}].parallel[${taskIndex}].acceptance`);
799
- if (errors.length > 0) return validationErrorResult("chain", errors.join(" "));
800
- }
801
- } else if (isDynamicParallelStep(step)) {
802
- if (Object.hasOwn(step, "acceptance")) return validationErrorResult("chain", `chain[${stepIndex}].acceptance is not supported on dynamic fanout groups; set acceptance on chain[${stepIndex}].parallel.acceptance for each materialized child.`);
803
- const errors = validateAcceptanceInput(step.parallel.acceptance, `chain[${stepIndex}].parallel.acceptance`);
804
- if (errors.length > 0) return validationErrorResult("chain", errors.join(" "));
805
- } else {
806
- const stepErrors = validateAcceptanceInput(step.acceptance, `chain[${stepIndex}].acceptance`);
807
- if (stepErrors.length > 0) return validationErrorResult("chain", stepErrors.join(" "));
808
- }
809
- }
810
- return null;
811
- }
812
-
813
1050
  function validateExecutionInput(
814
1051
  params: SubagentParamsLike,
815
1052
  agents: AgentConfig[],
@@ -818,9 +1055,6 @@ function validateExecutionInput(
818
1055
  hasSingle: boolean,
819
1056
  allowClarifyTaskPrompt: boolean,
820
1057
  ): AgentToolResult<Details> | null {
821
- const acceptanceError = validateAcceptanceForExecution(params);
822
- if (acceptanceError) return acceptanceError;
823
-
824
1058
  if (Number(hasChain) + Number(hasTasks) + Number(hasSingle) !== 1) {
825
1059
  return {
826
1060
  content: [
@@ -834,9 +1068,6 @@ function validateExecutionInput(
834
1068
  };
835
1069
  }
836
1070
 
837
- const timeoutResolution = resolveForegroundTimeoutMs(params);
838
- if (timeoutResolution.error) return validationErrorResult(getRequestedModeLabel(params), timeoutResolution.error);
839
-
840
1071
  if (hasSingle && params.agent && !agents.find((agent) => agent.name === params.agent)) {
841
1072
  return {
842
1073
  content: [{ type: "text", text: `Unknown agent: ${params.agent}` }],
@@ -1138,6 +1369,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1138
1369
  cwd: ctx.cwd,
1139
1370
  currentSessionId: deps.state.currentSessionId!,
1140
1371
  currentModelProvider: ctx.model?.provider,
1372
+ currentModel: ctx.model,
1141
1373
  };
1142
1374
  const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo);
1143
1375
  const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
@@ -1148,7 +1380,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1148
1380
  if (hasTasks && params.tasks) {
1149
1381
  const agentConfigs = params.tasks.map((task) => agents.find((agent) => agent.name === task.agent));
1150
1382
  const modelOverrides = params.tasks.map((task, index) =>
1151
- resolveModelCandidate(task.model ?? agentConfigs[index]?.model, availableModels, currentProvider),
1383
+ resolveSubagentModelOverride(task.model ?? agentConfigs[index]?.model, ctx.model, availableModels, currentProvider),
1152
1384
  );
1153
1385
  const skillOverrides = params.tasks.map((task) => normalizeSkillInput(task.skill));
1154
1386
  const parallelTasks = params.tasks.map((task, index) => ({
@@ -1235,7 +1467,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1235
1467
  const normalizedSkills = normalizeSkillInput(params.skill);
1236
1468
  const skills = normalizedSkills === false ? [] : normalizedSkills;
1237
1469
  const maxSubagentDepth = resolveChildMaxSubagentDepth(currentMaxSubagentDepth, a.maxSubagentDepth);
1238
- const modelOverride = resolveModelCandidate((params.model as string | undefined) ?? a.model, availableModels, currentProvider);
1470
+ const modelOverride = resolveSubagentModelOverride((params.model as string | undefined) ?? a.model, ctx.model, availableModels, currentProvider);
1239
1471
  return executeAsyncSingle(id, {
1240
1472
  agent: params.agent!,
1241
1473
  task: params.context === "fork" ? wrapForkTask(params.task ?? "") : (params.task ?? ""),
@@ -1310,7 +1542,6 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1310
1542
  onUpdate,
1311
1543
  onControlEvent,
1312
1544
  controlConfig,
1313
- ...(data.foregroundTimeoutMs !== undefined ? { timeoutMs: data.foregroundTimeoutMs } : {}),
1314
1545
  childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(runId, agent, index) : undefined,
1315
1546
  orchestratorIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
1316
1547
  foregroundControl,
@@ -1337,6 +1568,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1337
1568
  cwd: ctx.cwd,
1338
1569
  currentSessionId: deps.state.currentSessionId!,
1339
1570
  currentModelProvider: ctx.model?.provider,
1571
+ currentModel: ctx.model,
1340
1572
  };
1341
1573
  const asyncChain = wrapChainTasksForFork(chainResult.requestedAsync.chain, params.context);
1342
1574
  return executeAsyncChain(id, {
@@ -1367,7 +1599,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1367
1599
  const chainDetails = chainResult.details ? compactForegroundDetails({ ...chainResult.details, runId }) : undefined;
1368
1600
  if (foregroundControl) updateForegroundNestedProjection(foregroundControl);
1369
1601
  if (chainDetails) rememberForegroundRun(deps.state, { runId, mode: "chain", cwd: effectiveCwd, results: chainDetails.results });
1370
- const intercomReceipt = chainDetails && !chainDetails.results.some((result) => result.interrupted || result.detached || result.timedOut)
1602
+ const intercomReceipt = chainDetails && !chainDetails.results.some((result) => result.interrupted || result.detached)
1371
1603
  ? await maybeBuildForegroundIntercomReceipt({
1372
1604
  pi: deps.pi,
1373
1605
  intercomBridge: data.intercomBridge,
@@ -1402,8 +1634,6 @@ interface ForegroundParallelRunInput {
1402
1634
  artifactConfig: ArtifactConfig;
1403
1635
  artifactsDir: string;
1404
1636
  maxOutput?: MaxOutputConfig;
1405
- timeoutMs?: number;
1406
- timeoutAt?: number;
1407
1637
  paramsCwd: string;
1408
1638
  maxSubagentDepths: number[];
1409
1639
  availableModels: ModelInfo[];
@@ -1556,7 +1786,6 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
1556
1786
  cwd: taskCwd,
1557
1787
  signal: input.signal,
1558
1788
  interruptSignal: interruptController.signal,
1559
- ...(input.timeoutMs !== undefined && input.timeoutAt !== undefined ? { timeoutMs: input.timeoutMs, timeoutAt: input.timeoutAt } : {}),
1560
1789
  allowIntercomDetach: agentConfig?.systemPrompt?.includes(INTERCOM_BRIDGE_MARKER) === true,
1561
1790
  intercomEvents: input.intercomEvents,
1562
1791
  runId: input.runId,
@@ -1570,8 +1799,6 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
1570
1799
  outputPath,
1571
1800
  outputMode: behavior?.outputMode,
1572
1801
  maxSubagentDepth: input.maxSubagentDepths[index],
1573
- maxExecutionTimeMs: agentConfig?.maxExecutionTimeMs,
1574
- maxTokens: agentConfig?.maxTokens,
1575
1802
  controlConfig: input.controlConfig,
1576
1803
  onControlEvent: input.onControlEvent,
1577
1804
  intercomSessionName: input.childIntercomTarget?.(task.agent, index),
@@ -1697,7 +1924,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1697
1924
  ...(task.model ? { model: task.model } : {}),
1698
1925
  }));
1699
1926
  const modelOverrides: (string | undefined)[] = tasks.map((_, i) =>
1700
- resolveModelCandidate(behaviorOverrides[i]?.model ?? agentConfigs[i]?.model, availableModels, currentProvider),
1927
+ resolveSubagentModelOverride(behaviorOverrides[i]?.model ?? agentConfigs[i]?.model, ctx.model, availableModels, currentProvider),
1701
1928
  );
1702
1929
 
1703
1930
  if (params.clarify === true && ctx.hasUI) {
@@ -1758,6 +1985,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1758
1985
  cwd: ctx.cwd,
1759
1986
  currentSessionId: deps.state.currentSessionId!,
1760
1987
  currentModelProvider: ctx.model?.provider,
1988
+ currentModel: ctx.model,
1761
1989
  };
1762
1990
  const parallelTasks = tasks.map((t, i) => {
1763
1991
  const taskText = params.context === "fork" ? wrapForkTask(taskTexts[i]!) : taskTexts[i]!;
@@ -1839,7 +2067,6 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1839
2067
  }
1840
2068
  }
1841
2069
 
1842
- const timeoutAt = data.foregroundTimeoutMs !== undefined ? Date.now() + data.foregroundTimeoutMs : undefined;
1843
2070
  const results = await runForegroundParallelTasks({
1844
2071
  tasks,
1845
2072
  taskTexts,
@@ -1854,7 +2081,6 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1854
2081
  artifactConfig,
1855
2082
  artifactsDir,
1856
2083
  maxOutput: params.maxOutput,
1857
- ...(data.foregroundTimeoutMs !== undefined && timeoutAt !== undefined ? { timeoutMs: data.foregroundTimeoutMs, timeoutAt } : {}),
1858
2084
  paramsCwd: effectiveCwd,
1859
2085
  availableModels,
1860
2086
  modelOverrides,
@@ -1882,7 +2108,6 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1882
2108
  if (result.artifactPaths) allArtifactPaths.push(result.artifactPaths);
1883
2109
  }
1884
2110
 
1885
- const timedOut = results.find((result) => result.timedOut);
1886
2111
  const interrupted = results.find((result) => result.interrupted);
1887
2112
  const details = compactForegroundDetails({
1888
2113
  mode: "parallel",
@@ -1892,13 +2117,6 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1892
2117
  artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
1893
2118
  });
1894
2119
  rememberForegroundRun(deps.state, { runId, mode: "parallel", cwd: effectiveCwd, results: details.results });
1895
- if (timedOut) {
1896
- return {
1897
- content: [{ type: "text", text: `Parallel run timed out (${timedOut.agent}): ${timedOut.error ?? "timeout expired"}` }],
1898
- details,
1899
- isError: true,
1900
- };
1901
- }
1902
2120
  if (interrupted) {
1903
2121
  return {
1904
2122
  content: [{ type: "text", text: `Parallel run paused after interrupt (${interrupted.agent}). Waiting for explicit next action.` }],
@@ -1990,8 +2208,9 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1990
2208
  const currentProvider = ctx.model?.provider;
1991
2209
  const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo);
1992
2210
  let task = params.task ?? "";
1993
- let modelOverride: string | undefined = resolveModelCandidate(
2211
+ let modelOverride: string | undefined = resolveSubagentModelOverride(
1994
2212
  (params.model as string | undefined) ?? agentConfig.model,
2213
+ ctx.model,
1995
2214
  availableModels,
1996
2215
  currentProvider,
1997
2216
  );
@@ -2048,6 +2267,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2048
2267
  cwd: ctx.cwd,
2049
2268
  currentSessionId: deps.state.currentSessionId!,
2050
2269
  currentModelProvider: ctx.model?.provider,
2270
+ currentModel: ctx.model,
2051
2271
  };
2052
2272
  return executeAsyncSingle(id, {
2053
2273
  agent: params.agent!,
@@ -2129,12 +2349,10 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2129
2349
  }
2130
2350
  : undefined;
2131
2351
 
2132
- const timeoutAt = data.foregroundTimeoutMs !== undefined ? Date.now() + data.foregroundTimeoutMs : undefined;
2133
2352
  const r = await runSync(ctx.cwd, agents, params.agent!, task, {
2134
2353
  cwd: effectiveCwd,
2135
2354
  signal,
2136
2355
  interruptSignal: interruptController.signal,
2137
- ...(data.foregroundTimeoutMs !== undefined && timeoutAt !== undefined ? { timeoutMs: data.foregroundTimeoutMs, timeoutAt } : {}),
2138
2356
  allowIntercomDetach: agentConfig.systemPrompt?.includes(INTERCOM_BRIDGE_MARKER) === true,
2139
2357
  intercomEvents: deps.pi.events,
2140
2358
  runId,
@@ -2147,8 +2365,6 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2147
2365
  outputPath,
2148
2366
  outputMode: effectiveOutputMode,
2149
2367
  maxSubagentDepth,
2150
- maxExecutionTimeMs: agentConfig.maxExecutionTimeMs,
2151
- maxTokens: agentConfig.maxTokens,
2152
2368
  onUpdate: forwardSingleUpdate,
2153
2369
  controlConfig,
2154
2370
  onControlEvent,
@@ -2201,7 +2417,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2201
2417
  });
2202
2418
  rememberForegroundRun(deps.state, { runId, mode: "single", cwd: effectiveCwd, results: details.results });
2203
2419
 
2204
- if (!r.detached && !r.interrupted && !r.timedOut) {
2420
+ if (!r.detached && !r.interrupted) {
2205
2421
  if (foregroundControl) updateForegroundNestedProjection(foregroundControl);
2206
2422
  const intercomReceipt = await maybeBuildForegroundIntercomReceipt({
2207
2423
  pi: deps.pi,
@@ -2227,14 +2443,6 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2227
2443
  };
2228
2444
  }
2229
2445
 
2230
- if (r.timedOut) {
2231
- return {
2232
- content: [{ type: "text", text: `Run timed out (${params.agent}): ${r.error ?? "timeout expired"}` }],
2233
- details,
2234
- isError: true,
2235
- };
2236
- }
2237
-
2238
2446
  if (r.interrupted) {
2239
2447
  return {
2240
2448
  content: [{ type: "text", text: `Run paused after interrupt (${params.agent}). Waiting for explicit next action.` }],
@@ -2244,7 +2452,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2244
2452
 
2245
2453
  if (r.exitCode !== 0)
2246
2454
  return {
2247
- content: [{ type: "text", text: r.error || "Failed" }],
2455
+ content: [{ type: "text", text: formatFailedSingleRunOutput(r, finalizedOutput.displayOutput) }],
2248
2456
  details,
2249
2457
  isError: true,
2250
2458
  };
@@ -2333,6 +2541,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2333
2541
  if (params.action === "resume") {
2334
2542
  return resumeAsyncRun({ params: paramsWithResolvedCwd, requestCwd, ctx, deps });
2335
2543
  }
2544
+ if (params.action === "append-step") {
2545
+ return appendStepToAsyncChain({ params: paramsWithResolvedCwd, requestCwd, ctx, deps });
2546
+ }
2336
2547
  if (params.action === "interrupt") {
2337
2548
  const targetRunId = paramsWithResolvedCwd.runId ?? paramsWithResolvedCwd.id;
2338
2549
  let resolved: ResolvedSubagentRunId | undefined;
@@ -2362,7 +2573,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2362
2573
  details: { mode: "management", results: [] },
2363
2574
  };
2364
2575
  }
2365
- const asyncInterruptResult = interruptAsyncRun(deps.state, resolved?.kind === "async" ? resolved.id : targetRunId);
2576
+ const asyncInterruptResult = interruptAsyncRun(deps.state, resolved?.kind === "async" ? resolved.id : targetRunId, deps.kill);
2366
2577
  if (asyncInterruptResult) return asyncInterruptResult;
2367
2578
  return {
2368
2579
  content: [{ type: "text", text: "No interrupt-capable run found in this session." }],
@@ -2384,7 +2595,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2384
2595
  details: { mode: "management" as const, results: [] },
2385
2596
  };
2386
2597
  }
2387
- return handleManagementAction(params.action, paramsWithResolvedCwd, { ...ctx, cwd: requestCwd });
2598
+ return handleManagementAction(params.action, paramsWithResolvedCwd, { ...ctx, cwd: requestCwd, config: deps.config });
2388
2599
  }
2389
2600
 
2390
2601
  const { blocked, depth, maxDepth } = checkSubagentDepth(deps.config.maxSubagentDepth);
@@ -2462,11 +2673,6 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2462
2673
  const requestedAsync = effectiveParams.async ?? deps.asyncByDefault;
2463
2674
  const backgroundRequestedWhileClarifying = (hasChain || hasTasks) && requestedAsync && effectiveParams.clarify === true;
2464
2675
  const effectiveAsync = requestedAsync && effectiveParams.clarify !== true;
2465
- const foregroundTimeout = resolveForegroundTimeoutMs(effectiveParams);
2466
- if (foregroundTimeout.error) return buildRequestedModeError(effectiveParams, foregroundTimeout.error);
2467
- if (effectiveAsync && foregroundTimeout.timeoutMs !== undefined) {
2468
- return buildRequestedModeError(effectiveParams, "timeoutMs/maxRuntimeMs only applies to foreground subagent runs. Omit async:true or use action:'interrupt' for background runs.");
2469
- }
2470
2676
  const controlConfig = resolveControlConfig(deps.config.control, effectiveParams.control);
2471
2677
 
2472
2678
  const artifactConfig: ArtifactConfig = {
@@ -2518,7 +2724,6 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2518
2724
  artifactsDir,
2519
2725
  backgroundRequestedWhileClarifying,
2520
2726
  effectiveAsync,
2521
- ...(foregroundTimeout.timeoutMs !== undefined ? { foregroundTimeoutMs: foregroundTimeout.timeoutMs } : {}),
2522
2727
  controlConfig,
2523
2728
  intercomBridge,
2524
2729
  nestedRoute,