pi-subagents 0.34.0 → 0.35.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 (98) hide show
  1. package/CHANGELOG.md +78 -9
  2. package/README.md +213 -32
  3. package/index.ts +1 -0
  4. package/install.mjs +1 -1
  5. package/package.json +23 -8
  6. package/prompts/review-loop.md +3 -1
  7. package/skills/pi-subagents/SKILL.md +87 -25
  8. package/src/agents/agent-management.ts +82 -15
  9. package/src/agents/agent-serializer.ts +19 -0
  10. package/src/agents/agents.ts +91 -49
  11. package/src/agents/frontmatter.ts +67 -13
  12. package/src/agents/skills.ts +25 -12
  13. package/src/api/background-work.ts +197 -0
  14. package/src/api/delegation.ts +158 -0
  15. package/src/extension/chain-validation.ts +165 -0
  16. package/src/extension/doctor.ts +15 -0
  17. package/src/extension/fanout-child.ts +3 -1
  18. package/src/extension/index.ts +65 -124
  19. package/src/extension/rpc.ts +10 -2
  20. package/src/extension/schemas.ts +18 -14
  21. package/src/extension/steering-notices.ts +35 -0
  22. package/src/extension/tool-description.ts +20 -9
  23. package/src/intercom/intercom-bridge.ts +3 -2
  24. package/src/intercom/native-supervisor-channel.ts +9 -1
  25. package/src/intercom/result-intercom.ts +4 -0
  26. package/src/runs/background/async-execution.ts +293 -44
  27. package/src/runs/background/async-job-tracker.ts +56 -9
  28. package/src/runs/background/async-resume.ts +159 -52
  29. package/src/runs/background/async-status.ts +25 -18
  30. package/src/runs/background/auto-drain.ts +67 -0
  31. package/src/runs/background/chain-root-attachment.ts +16 -8
  32. package/src/runs/background/control-channel.ts +260 -13
  33. package/src/runs/background/fleet-view.ts +23 -2
  34. package/src/runs/background/notify.ts +79 -10
  35. package/src/runs/background/result-watcher.ts +12 -9
  36. package/src/runs/background/run-id-resolver.ts +14 -2
  37. package/src/runs/background/run-status.ts +23 -15
  38. package/src/runs/background/scheduled-runs.ts +3 -0
  39. package/src/runs/background/stale-run-reconciler.ts +32 -10
  40. package/src/runs/background/steering.ts +237 -0
  41. package/src/runs/background/subagent-runner.ts +898 -236
  42. package/src/runs/background/subagent-wait.ts +484 -0
  43. package/src/runs/background/top-level-async.ts +2 -1
  44. package/src/runs/background/wait-config.ts +36 -0
  45. package/src/runs/background/wait-tool.ts +26 -0
  46. package/src/runs/foreground/async-steering-action.ts +230 -0
  47. package/src/runs/foreground/chain-clarify.ts +22 -6
  48. package/src/runs/foreground/chain-execution.ts +50 -32
  49. package/src/runs/foreground/execution.ts +308 -94
  50. package/src/runs/foreground/subagent-executor.ts +592 -268
  51. package/src/runs/shared/acceptance.ts +355 -97
  52. package/src/runs/shared/child-protocol.ts +121 -0
  53. package/src/runs/shared/completion-guard.ts +8 -127
  54. package/src/runs/shared/dynamic-fanout.ts +6 -4
  55. package/src/runs/shared/model-fallback.ts +36 -0
  56. package/src/runs/shared/nested-events.ts +9 -4
  57. package/src/runs/shared/nested-render.ts +4 -1
  58. package/src/runs/shared/parallel-utils.ts +7 -0
  59. package/src/runs/shared/pi-args.ts +34 -7
  60. package/src/runs/shared/pi-spawn.ts +18 -12
  61. package/src/runs/shared/session-lease.ts +279 -0
  62. package/src/runs/shared/single-output.ts +61 -6
  63. package/src/runs/shared/spawn-budget.ts +128 -0
  64. package/src/runs/shared/subagent-control.ts +10 -6
  65. package/src/runs/shared/subagent-prompt-runtime.ts +127 -26
  66. package/src/runs/shared/task-intent.ts +176 -0
  67. package/src/runs/shared/tool-availability.ts +65 -0
  68. package/src/runs/shared/turn-budget.ts +49 -4
  69. package/src/shared/atomic-json.ts +4 -1
  70. package/src/shared/fork-context.ts +28 -3
  71. package/src/shared/model-info.ts +7 -4
  72. package/src/shared/status-format.ts +7 -1
  73. package/src/shared/types.ts +203 -25
  74. package/src/shared/utils.ts +35 -7
  75. package/src/slash/delegation-adapters.ts +457 -0
  76. package/src/slash/delegation-request.ts +103 -0
  77. package/src/slash/prompt-template-bridge.ts +167 -344
  78. package/src/slash/slash-commands.ts +239 -6
  79. package/src/slash/subagents-admin.ts +428 -0
  80. package/src/slash/subagents-editor.ts +86 -0
  81. package/src/tui/fleet.ts +405 -0
  82. package/src/tui/render.ts +90 -16
  83. package/src/watchdog/change-signature.ts +127 -0
  84. package/src/watchdog/child-status.ts +205 -0
  85. package/src/watchdog/emission-guard.ts +123 -0
  86. package/src/watchdog/lsp-diagnostics.ts +532 -0
  87. package/src/watchdog/model-selection.ts +167 -0
  88. package/src/watchdog/register-child.ts +117 -0
  89. package/src/watchdog/register-main.ts +433 -0
  90. package/src/watchdog/render.ts +54 -0
  91. package/src/watchdog/review.ts +293 -0
  92. package/src/watchdog/runtime.ts +712 -0
  93. package/src/watchdog/settings.ts +528 -0
  94. package/src/watchdog/tool-actions.ts +155 -0
  95. package/src/watchdog/turn-delta.ts +161 -0
  96. package/src/watchdog/types.ts +188 -0
  97. package/src/watchdog/warning-format.ts +73 -0
  98. package/src/runs/background/wait.ts +0 -394
@@ -13,7 +13,9 @@ 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, resolveSubagentModelOverride } from "../shared/model-fallback.ts";
16
+ import { handleWatchdogToolAction, WATCHDOG_TOOL_ACTIONS } from "../../watchdog/tool-actions.ts";
17
+ import type { MainWatchdogRuntime } from "../../watchdog/runtime.ts";
18
+ import { buildModelCandidates, resolveEffectiveSubagentModel, resolveModelCandidate } from "../shared/model-fallback.ts";
17
19
  import type { ModelScopeConfig } from "../shared/model-scope.ts";
18
20
  import { aggregateParallelOutputs } from "../shared/parallel-utils.ts";
19
21
  import { recordRun } from "../shared/run-history.ts";
@@ -36,12 +38,13 @@ import { buildAsyncRunnerSteps, executeAsyncChain, executeAsyncSingle, formatAsy
36
38
  import type { ScheduledRunAction } from "../background/scheduled-runs.ts";
37
39
  import { enqueueChainAppendRequest, readPendingChainAppendRequests, runnerStepOutputNames } from "../background/chain-append.ts";
38
40
  import { ChainOutputValidationError, validateChainOutputBindingsWithContext } from "../shared/chain-outputs.ts";
39
- import { validateAcceptanceInput } from "../shared/acceptance.ts";
40
- import { createForkContextResolver } from "../../shared/fork-context.ts";
41
+ import { validateExecutionAcceptance } from "../shared/acceptance.ts";
42
+ import { createForkContextResolver, forkedChildRequiresThinkingOff } from "../../shared/fork-context.ts";
41
43
  import { resolveCurrentSessionId } from "../../shared/session-identity.ts";
42
44
  import { applyIntercomBridgeToAgent, INTERCOM_BRIDGE_MARKER, resolveIntercomBridge, resolveIntercomSessionTarget, resolveSubagentIntercomTarget, type IntercomBridgeState } from "../../intercom/intercom-bridge.ts";
43
45
  import { formatControlIntercomMessage, formatControlNoticeMessage, resolveControlConfig, shouldNotifyControlEvent } from "../shared/subagent-control.ts";
44
- import { DEFAULT_TURN_BUDGET_GRACE_TURNS } from "../shared/turn-budget.ts";
46
+ import { resolveTurnBudgetConfig } from "../shared/turn-budget.ts";
47
+ import { formatSpawnBudget, getSpawnBudgetSnapshot, grantSpawnBudget, preflightSpawnBudget, preflightSpawnBudgetGrant, reserveSpawnBudget } from "../shared/spawn-budget.ts";
45
48
  import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
46
49
  import { finalizeSingleOutput, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
47
50
  import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, readStatus, resolveChildCwd, sumResultsCost, sumResultsUsage } from "../../shared/utils.ts";
@@ -49,14 +52,15 @@ import { DEFAULT_GLOBAL_CONCURRENCY_LIMIT, Semaphore } from "../shared/parallel-
49
52
  import {
50
53
  attachNestedChildrenToResultChildren,
51
54
  buildSubagentResultIntercomPayload,
52
- deliverSubagentIntercomMessageEvent,
53
55
  deliverSubagentResultIntercomEvent,
54
56
  formatSubagentResultReceipt,
55
57
  resolveSubagentResultStatus,
56
58
  stripDetailsOutputsForIntercomReceipt,
57
59
  } from "../../intercom/result-intercom.ts";
58
- import { buildRevivedAsyncTask, interruptLiveAsyncResumeTarget, resolveAsyncResumeTarget, resolveAsyncRunLocation } from "../background/async-resume.ts";
59
- import { deliverInterruptRequest, requestAsyncSteer } from "../background/control-channel.ts";
60
+ import { applySteeringRecoveryAgentConfig, buildRevivedAsyncTask, resolveAsyncResumeTarget, resolveAsyncRunLocation } from "../background/async-resume.ts";
61
+ import { deliverInterruptRequest, deliverStopRequest, requestAsyncSteer } from "../background/control-channel.ts";
62
+ import { waitForSteeringAction } from "../background/steering.ts";
63
+ import { steerAsyncRun } from "./async-steering-action.ts";
60
64
  import { reconcileAsyncRun } from "../background/stale-run-reconciler.ts";
61
65
  import { resolveAsyncRootResultPath } from "../background/chain-root-attachment.ts";
62
66
  import { attachRootChildrenToSteps, createNestedRoute, readNestedControlResults, resolveInheritedNestedRouteFromEnv, resolveNestedAsyncDir, resolveNestedParentAddressFromEnv, updateForegroundNestedProjection, writeNestedControlRequest, writeNestedEvent, type NestedRunResolutionScope } from "../shared/nested-events.ts";
@@ -75,6 +79,7 @@ import {
75
79
  } from "../shared/worktree.ts";
76
80
  import {
77
81
  type AgentProgress,
82
+ type AsyncStatus,
78
83
  type AcceptanceInput,
79
84
  type ArtifactConfig,
80
85
  type ArtifactPaths,
@@ -96,12 +101,13 @@ import {
96
101
  type SubagentState,
97
102
  ASYNC_DIR,
98
103
  DEFAULT_ARTIFACT_CONFIG,
104
+ DEFAULT_FORK_PREAMBLE,
99
105
  RESULTS_DIR,
100
106
  SUBAGENT_ACTIONS,
101
107
  SUBAGENT_CONTROL_EVENT,
102
108
  SUBAGENT_CONTROL_INTERCOM_EVENT,
109
+ SUBAGENT_FOREGROUND_COMPLETE_EVENT,
103
110
  checkSubagentDepth,
104
- resolveMaxSubagentSpawnsPerSession,
105
111
  resolveTopLevelParallelConcurrency,
106
112
  resolveTopLevelParallelMaxTasks,
107
113
  resolveChildMaxSubagentDepth,
@@ -109,8 +115,7 @@ import {
109
115
  wrapForkTask,
110
116
  } from "../../shared/types.ts";
111
117
 
112
- const MUTATING_MANAGEMENT_ACTIONS = new Set(["create", "update", "delete", "eject", "disable", "enable", "reset"]);
113
-
118
+ const MUTATING_MANAGEMENT_ACTIONS = new Set(["create", "update", "delete", "eject", "disable", "enable", "reset", "grant-spawn-budget", "watchdog.configure"]);
114
119
  interface TaskParam {
115
120
  agent: string;
116
121
  task: string;
@@ -143,6 +148,7 @@ export interface SubagentParamsLike {
143
148
  worktree?: boolean;
144
149
  context?: "fresh" | "fork";
145
150
  async?: boolean;
151
+ foregroundOnly?: boolean;
146
152
  timeoutMs?: number;
147
153
  maxRuntimeMs?: number;
148
154
  turnBudget?: TurnBudgetConfig;
@@ -156,6 +162,9 @@ export interface SubagentParamsLike {
156
162
  artifacts?: boolean;
157
163
  includeProgress?: boolean;
158
164
  model?: string;
165
+ thinking?: string | false;
166
+ scope?: string;
167
+ target?: string;
159
168
  skill?: string | string[] | boolean;
160
169
  output?: string | boolean;
161
170
  outputMode?: "inline" | "file-only";
@@ -164,6 +173,7 @@ export interface SubagentParamsLike {
164
173
  acceptance?: AcceptanceInput;
165
174
  schedule?: string;
166
175
  scheduleName?: string;
176
+ additional?: number;
167
177
  }
168
178
 
169
179
  interface ExecutorDeps {
@@ -171,7 +181,9 @@ interface ExecutorDeps {
171
181
  state: SubagentState;
172
182
  config: ExtensionConfig;
173
183
  asyncByDefault: boolean;
184
+ waitToolEnabled?: boolean;
174
185
  handleScheduledRunAction?: (params: SubagentParamsLike, ctx: ExtensionContext) => Promise<AgentToolResult<Details>>;
186
+ watchdog?: MainWatchdogRuntime;
175
187
  tempArtifactsDir: string;
176
188
  getSubagentSessionRoot: (parentSessionFile: string | null) => string;
177
189
  expandTilde: (p: string) => string;
@@ -180,6 +192,9 @@ interface ExecutorDeps {
180
192
  kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
181
193
  }
182
194
 
195
+ type ForkSessionFileForTask = (agentName: string, idx?: number, modelOverride?: string) => string | undefined;
196
+ type ForkThinkingOverrideForTask = (agentName: string, idx?: number, modelOverride?: string) => AgentConfig["thinking"] | undefined;
197
+
183
198
  interface ExecutionContextData {
184
199
  params: SubagentParamsLike;
185
200
  effectiveCwd: string;
@@ -192,8 +207,8 @@ interface ExecutionContextData {
192
207
  sessionRoot: string;
193
208
  sessionDirForIndex: (idx?: number) => string;
194
209
  sessionFileForIndex: (idx?: number) => string | undefined;
195
- sessionFileForTask: (agentName: string, idx?: number) => string | undefined;
196
- thinkingOverrideForTask: (agentName: string, idx?: number) => AgentConfig["thinking"] | undefined;
210
+ sessionFileForTask: ForkSessionFileForTask;
211
+ thinkingOverrideForTask: ForkThinkingOverrideForTask;
197
212
  artifactConfig: ArtifactConfig;
198
213
  artifactsDir: string;
199
214
  backgroundRequestedWhileClarifying: boolean;
@@ -208,6 +223,7 @@ interface ExecutionContextData {
208
223
  configToolBudget?: ResolvedToolBudget;
209
224
  contextPolicy: AgentDefaultContextPolicy;
210
225
  modelScope?: ModelScopeConfig;
226
+ parentSessionId: string | null;
211
227
  }
212
228
 
213
229
  function resolveRequestedCwd(runtimeCwd: string, requestedCwd: string | undefined): string {
@@ -263,22 +279,34 @@ function trustedSessionRootsForStatus(ctx: ExtensionContext, deps: ExecutorDeps)
263
279
  return [...new Set(roots)];
264
280
  }
265
281
 
266
- function reserveSubagentSpawns(input: { state: SubagentState; config: ExtensionConfig; sessionId: string | null; requested: number; mode: "single" | "parallel" | "chain" }): AgentToolResult<Details> | undefined {
267
- if (input.requested <= 0) return undefined;
268
- if (input.state.subagentSpawns?.sessionId !== input.sessionId) {
269
- input.state.subagentSpawns = { sessionId: input.sessionId, count: 0 };
270
- }
271
- const maxSpawns = resolveMaxSubagentSpawnsPerSession(input.config.maxSubagentSpawnsPerSession);
272
- const used = input.state.subagentSpawns.count;
273
- if (used + input.requested > maxSpawns) {
274
- return {
275
- content: [{ type: "text", text: `Subagent spawn limit reached for this session (${used}/${maxSpawns} used, ${input.requested} requested). Complete the work directly or start a new session.` }],
276
- isError: true,
277
- details: { mode: input.mode, results: [] },
278
- };
279
- }
280
- input.state.subagentSpawns.count = used + input.requested;
281
- return undefined;
282
+ function spawnBudgetErrorResult(message: string, mode: "single" | "parallel" | "chain"): AgentToolResult<Details> {
283
+ return {
284
+ content: [{ type: "text", text: message }],
285
+ isError: true,
286
+ details: { mode, results: [] },
287
+ };
288
+ }
289
+
290
+ function withSpawnBudgetStatus(
291
+ result: AgentToolResult<Details>,
292
+ state: SubagentState,
293
+ config: ExtensionConfig,
294
+ sessionId: string | null,
295
+ ): AgentToolResult<Details> {
296
+ const spawnBudget = getSpawnBudgetSnapshot(state, config, sessionId);
297
+ return {
298
+ ...result,
299
+ content: result.content.map((item, index) => index === 0 && item.type === "text"
300
+ ? { ...item, text: `${formatSpawnBudget(spawnBudget)}\n${item.text}` }
301
+ : item),
302
+ details: { ...result.details, spawnBudget },
303
+ };
304
+ }
305
+
306
+ function hasActiveSubagentChildren(state: SubagentState): boolean {
307
+ if (state.subagentInProgress || state.foregroundControls.size > 0) return true;
308
+ const isActive = (status: string) => status === "queued" || status === "running";
309
+ return [...state.asyncJobs.values(), ...(state.fleetJobs?.values() ?? [])].some((job) => isActive(job.status));
282
310
  }
283
311
 
284
312
  function countRequestedSubagentSpawns(params: SubagentParamsLike, config: ExtensionConfig): number {
@@ -315,13 +343,15 @@ function foregroundStatusResult(control: SubagentState["foregroundControls"] ext
315
343
  function trimRememberedForegroundRuns(state: SubagentState): void {
316
344
  if (!state.foregroundRuns) return;
317
345
  while (state.foregroundRuns.size > 50) {
318
- const oldest = [...state.foregroundRuns.values()].sort((left, right) => left.updatedAt - right.updatedAt)[0];
319
- if (!oldest) break;
320
- state.foregroundRuns.delete(oldest.runId);
346
+ const oldestTerminal = [...state.foregroundRuns.values()]
347
+ .filter((run) => !run.children.some((child) => child.status === "detached"))
348
+ .sort((left, right) => left.updatedAt - right.updatedAt)[0];
349
+ if (!oldestTerminal) break;
350
+ state.foregroundRuns.delete(oldestTerminal.runId);
321
351
  }
322
352
  }
323
353
 
324
- function rememberForegroundRun(state: SubagentState, input: { runId: string; mode: "single" | "parallel" | "chain"; cwd: string; results: SingleResult[] }): void {
354
+ function rememberForegroundRun(state: SubagentState, input: { runId: string; mode: "single" | "parallel" | "chain"; cwd: string; sessionId: string | null; results: SingleResult[] }): void {
325
355
  state.foregroundRuns ??= new Map();
326
356
  const previous = state.foregroundRuns.get(input.runId);
327
357
  const updatedAt = Date.now();
@@ -329,6 +359,7 @@ function rememberForegroundRun(state: SubagentState, input: { runId: string; mod
329
359
  runId: input.runId,
330
360
  mode: input.mode,
331
361
  cwd: input.cwd,
362
+ ...(input.sessionId ? { sessionId: input.sessionId } : {}),
332
363
  updatedAt,
333
364
  children: input.results.map((result, index) => {
334
365
  const child = {
@@ -337,12 +368,17 @@ function rememberForegroundRun(state: SubagentState, input: { runId: string; mod
337
368
  status: resolveSubagentResultStatus({ exitCode: result.exitCode, interrupted: result.interrupted, detached: result.detached }),
338
369
  updatedAt,
339
370
  ...(result.exitCode !== undefined ? { exitCode: result.exitCode } : {}),
371
+ ...(result.error ? { error: result.error } : {}),
340
372
  ...(result.finalOutput ? { finalOutput: result.finalOutput } : {}),
373
+ ...(result.outputMode ? { outputMode: result.outputMode } : {}),
374
+ ...(result.savedOutputPath ? { savedOutputPath: result.savedOutputPath } : {}),
375
+ ...(result.outputSaveError ? { outputSaveError: result.outputSaveError } : {}),
341
376
  ...(result.sessionFile ? { sessionFile: result.sessionFile } : {}),
342
377
  ...(result.artifactPaths ? { artifactPaths: result.artifactPaths } : {}),
343
378
  ...(result.transcriptPath ? { transcriptPath: result.transcriptPath } : {}),
344
379
  ...(result.transcriptError ? { transcriptError: result.transcriptError } : {}),
345
380
  ...(result.detachedReason ? { detachedReason: result.detachedReason } : {}),
381
+ ...(result.acceptance ? { acceptance: result.acceptance } : {}),
346
382
  };
347
383
  const recovered = previous?.children[index];
348
384
  return child.status === "detached" && recovered && recovered.status !== "detached" ? recovered : child;
@@ -351,12 +387,12 @@ function rememberForegroundRun(state: SubagentState, input: { runId: string; mod
351
387
  trimRememberedForegroundRuns(state);
352
388
  }
353
389
 
354
- function updateRememberedForegroundChild(state: SubagentState, input: { runId: string; mode: "single" | "parallel" | "chain"; cwd: string; index: number; result: SingleResult }): void {
390
+ function updateRememberedForegroundChild(state: SubagentState, input: { runId: string; mode: "single" | "parallel" | "chain"; cwd: string; sessionId: string | null; index: number; result: SingleResult; events: IntercomEventBus }): void {
355
391
  state.foregroundRuns ??= new Map();
356
392
  const updatedAt = Date.now();
357
393
  let run = state.foregroundRuns.get(input.runId);
358
394
  if (!run) {
359
- run = { runId: input.runId, mode: input.mode, cwd: input.cwd, updatedAt, children: [] };
395
+ run = { runId: input.runId, mode: input.mode, cwd: input.cwd, ...(input.sessionId ? { sessionId: input.sessionId } : {}), updatedAt, children: [] };
360
396
  state.foregroundRuns.set(input.runId, run);
361
397
  }
362
398
  run.updatedAt = updatedAt;
@@ -365,38 +401,67 @@ function updateRememberedForegroundChild(state: SubagentState, input: { runId: s
365
401
  ...child,
366
402
  agent: input.result.agent,
367
403
  index: input.index,
368
- status: resolveSubagentResultStatus({ exitCode: input.result.exitCode, interrupted: input.result.interrupted, detached: false }),
404
+ status: input.result.acceptance?.status === "rejected"
405
+ ? "failed"
406
+ : resolveSubagentResultStatus({ exitCode: input.result.exitCode, interrupted: input.result.interrupted, detached: false }),
369
407
  updatedAt,
370
408
  ...(input.result.exitCode !== undefined ? { exitCode: input.result.exitCode } : {}),
409
+ ...(input.result.error ? { error: input.result.error } : {}),
371
410
  ...(input.result.finalOutput ? { finalOutput: input.result.finalOutput } : {}),
411
+ outputMode: input.result.outputMode,
412
+ savedOutputPath: input.result.savedOutputPath,
413
+ outputSaveError: input.result.outputSaveError,
372
414
  ...(input.result.sessionFile ? { sessionFile: input.result.sessionFile } : {}),
373
415
  ...(input.result.artifactPaths ? { artifactPaths: input.result.artifactPaths } : {}),
374
416
  ...(input.result.transcriptPath ? { transcriptPath: input.result.transcriptPath } : {}),
375
417
  ...(input.result.transcriptError ? { transcriptError: input.result.transcriptError } : {}),
376
418
  ...(input.result.detachedReason ? { detachedReason: input.result.detachedReason } : {}),
419
+ ...(input.result.acceptance ? { acceptance: input.result.acceptance } : {}),
377
420
  };
378
421
  trimRememberedForegroundRuns(state);
422
+ const output = getSingleResultOutput(input.result).trim();
423
+ const success = input.result.exitCode === 0 && input.result.acceptance?.status !== "rejected";
424
+ const summary = !success && input.result.error
425
+ ? `${input.result.error}${output ? `\n\nOutput:\n${output}` : ""}`
426
+ : output || input.result.error || "Detached child exited without final output.";
427
+ input.events.emit(SUBAGENT_FOREGROUND_COMPLETE_EVENT, {
428
+ id: `${input.runId}:${input.index}`,
429
+ runId: input.runId,
430
+ source: "foreground",
431
+ mode: input.mode,
432
+ agent: input.result.agent,
433
+ success,
434
+ summary,
435
+ exitCode: input.result.exitCode,
436
+ state: success ? "complete" : "failed",
437
+ timestamp: updatedAt,
438
+ cwd: input.cwd,
439
+ sessionFile: input.result.sessionFile,
440
+ sessionId: input.sessionId,
441
+ taskIndex: input.index,
442
+ });
379
443
  }
380
444
 
381
- function resolveForegroundResumeTarget(params: SubagentParamsLike, state: SubagentState): { runId: string; mode: "single" | "parallel" | "chain"; state: "complete"; agent: string; index: number; intercomTarget: string; cwd: string; sessionFile: string } | undefined {
445
+ function resolveForegroundResumeTarget(params: SubagentParamsLike, state: SubagentState): { runId: string; mode: "single" | "parallel" | "chain"; state: "complete"; agent: string; index: number; cwd: string; sessionFile: string } | undefined {
382
446
  const requested = (params.id ?? params.runId)?.trim();
383
- if (!requested || !state.foregroundRuns?.size) return undefined;
384
- const direct = state.foregroundRuns.get(requested);
385
- const matches = direct ? [direct] : [...state.foregroundRuns.values()].filter((run) => run.runId.startsWith(requested));
447
+ if (!requested || !state.foregroundRuns?.size || !state.currentSessionId) return undefined;
448
+ const sessionRuns = [...state.foregroundRuns.values()].filter((run) => run.sessionId === state.currentSessionId);
449
+ const direct = sessionRuns.find((run) => run.runId === requested);
450
+ const matches = direct ? [direct] : sessionRuns.filter((run) => run.runId.startsWith(requested));
386
451
  if (matches.length === 0) return undefined;
387
452
  if (matches.length > 1) throw new Error(`Ambiguous foreground run id prefix '${requested}' matched: ${matches.map((run) => run.runId).join(", ")}. Provide a longer id.`);
388
453
  const run = matches[0]!;
454
+ if (run.children.some((child) => child.status === "detached")) throw new Error(`Foreground run '${run.runId}' is detached for intercom coordination and cannot be revived safely while any child may still be live. Reply to the supervisor request first, then wait with subagent_wait({ id: "${run.runId}" }); use status to recover the result and do not launch a replacement while it remains detached.`);
389
455
  if (run.children.length > 1 && params.index === undefined) throw new Error(`Foreground run '${run.runId}' has ${run.children.length} children. Provide index to choose one.`);
390
456
  const index = params.index ?? 0;
391
457
  if (!Number.isInteger(index)) throw new Error(`Foreground run '${run.runId}' index must be an integer.`);
392
458
  if (index < 0 || index >= run.children.length) throw new Error(`Foreground run '${run.runId}' has ${run.children.length} children. Index ${index} is out of range.`);
393
459
  const child = run.children[index]!;
394
- if (child.status === "detached") throw new Error(`Foreground run '${run.runId}' child ${index} is detached for intercom coordination and cannot be revived safely from the remembered foreground state. Reply to the supervisor request first; after the child exits, start a fresh follow-up if needed.`);
395
460
  if (!child.sessionFile) throw new Error(`Foreground run '${run.runId}' child ${index} does not have a persisted session file to resume from.`);
396
461
  if (path.extname(child.sessionFile) !== ".jsonl") throw new Error(`Foreground run '${run.runId}' child ${index} session file must be a .jsonl file: ${child.sessionFile}`);
397
462
  const sessionFile = path.resolve(child.sessionFile);
398
463
  if (!fs.existsSync(sessionFile)) throw new Error(`Foreground run '${run.runId}' child ${index} session file does not exist: ${child.sessionFile}`);
399
- return { runId: run.runId, mode: run.mode, state: "complete", agent: child.agent, index, intercomTarget: resolveSubagentIntercomTarget(run.runId, child.agent, index), cwd: run.cwd, sessionFile };
464
+ return { runId: run.runId, mode: run.mode, state: "complete", agent: child.agent, index, cwd: run.cwd, sessionFile };
400
465
  }
401
466
 
402
467
  type AsyncResumeSourceTarget = ReturnType<typeof resolveAsyncResumeTarget> & { source: "async" };
@@ -408,7 +473,6 @@ type NestedResumeSourceTarget = {
408
473
  state: "complete" | "failed" | "paused";
409
474
  agent: string;
410
475
  index: number;
411
- intercomTarget: string;
412
476
  cwd?: string;
413
477
  sessionFile: string;
414
478
  };
@@ -449,7 +513,13 @@ function resolveResumeTarget(params: SubagentParamsLike, state: SubagentState, o
449
513
  foregroundError = error;
450
514
  }
451
515
  try {
452
- asyncTarget = { source: "async", ...resolveAsyncResumeTarget(params, {}, { requireSessionFile: options.asyncRequireSessionFile }) };
516
+ asyncTarget = {
517
+ source: "async",
518
+ ...resolveAsyncResumeTarget(params, {}, {
519
+ requireSessionFile: options.asyncRequireSessionFile,
520
+ sessionId: state.currentSessionId ?? undefined,
521
+ }),
522
+ };
453
523
  } catch (error) {
454
524
  asyncError = error;
455
525
  }
@@ -481,6 +551,7 @@ function getAsyncInterruptTarget(
481
551
  state: SubagentState,
482
552
  runId: string | undefined,
483
553
  location?: { asyncDir: string | null; resolvedId?: string },
554
+ options: { fallbackToNewest?: boolean } = {},
484
555
  ): { asyncId: string; asyncDir: string } | undefined {
485
556
  if (location?.asyncDir) {
486
557
  return {
@@ -491,6 +562,7 @@ function getAsyncInterruptTarget(
491
562
  if (runId) {
492
563
  const direct = state.asyncJobs.get(runId);
493
564
  if (direct) return { asyncId: direct.asyncId, asyncDir: direct.asyncDir };
565
+ if (options.fallbackToNewest === false) return undefined;
494
566
  }
495
567
  let newest: { asyncId: string; asyncDir: string; updatedAt: number } | undefined;
496
568
  for (const job of state.asyncJobs.values()) {
@@ -567,64 +639,48 @@ function interruptAsyncRun(
567
639
  }
568
640
  }
569
641
 
570
- function steerAsyncRun(input: {
571
- state: SubagentState;
572
- runId: string;
573
- message: string;
574
- index?: number;
575
- kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
576
- location: { asyncDir: string | null; resolvedId?: string };
577
- }): AgentToolResult<Details> {
578
- if (!input.location.asyncDir) {
642
+ function stopAsyncRun(
643
+ state: SubagentState,
644
+ runId: string | undefined,
645
+ kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean,
646
+ location?: { asyncDir: string | null; resolvedId?: string },
647
+ ): AgentToolResult<Details> | null {
648
+ const target = getAsyncInterruptTarget(state, runId, location, { fallbackToNewest: false });
649
+ if (!target) return null;
650
+ const status = reconcileAsyncRun(target.asyncDir, { kill }).status;
651
+ if (state.currentSessionId && status?.sessionId !== state.currentSessionId) {
579
652
  return {
580
- content: [{ type: "text", text: `Async run '${input.runId}' has no live run directory to steer.` }],
653
+ content: [{ type: "text", text: `Async run '${target.asyncId}' was not found in the active session.` }],
581
654
  isError: true,
582
655
  details: { mode: "management", results: [] },
583
656
  };
584
657
  }
585
- const status = reconcileAsyncRun(input.location.asyncDir, { kill: input.kill }).status;
586
658
  if (!status || (status.state !== "running" && status.state !== "queued")) {
587
659
  return {
588
- content: [{ type: "text", text: `Async run '${input.runId}' is not running or queued and cannot be steered.` }],
660
+ content: [{ type: "text", text: `No running or queued async run was found for '${runId ?? "current"}'.` }],
589
661
  isError: true,
590
662
  details: { mode: "management", results: [] },
591
663
  };
592
664
  }
593
- const steps = status.steps ?? [];
594
- if (input.index !== undefined) {
595
- if (input.index < 0 || input.index >= steps.length) {
596
- return {
597
- content: [{ type: "text", text: `Async run '${status.runId}' has ${steps.length} children. Index ${input.index} is out of range.` }],
598
- isError: true,
599
- details: { mode: "management", results: [] },
600
- };
601
- }
602
- const targetStep = steps[input.index];
603
- if (targetStep && targetStep.status !== "running" && targetStep.status !== "pending") {
604
- return {
605
- content: [{ type: "text", text: `Async run '${status.runId}' child ${input.index} is ${targetStep.status} and cannot be steered.` }],
606
- isError: true,
607
- details: { mode: "management", results: [] },
608
- };
609
- }
610
- } else {
611
- const running = steps.filter((step) => step.status === "running");
612
- if (running.length === 0 && steps.length > 1) {
613
- return {
614
- content: [{ type: "text", text: `Async run '${status.runId}' has no running child yet. Provide index to steer a queued child.` }],
615
- isError: true,
616
- details: { mode: "management", results: [] },
617
- };
665
+ try {
666
+ deliverStopRequest({ asyncDir: target.asyncDir, pid: typeof status.pid === "number" ? status.pid : undefined, kill, source: "stop-action" });
667
+ const tracked = state.asyncJobs.get(target.asyncId);
668
+ if (tracked) {
669
+ tracked.activityState = undefined;
670
+ tracked.updatedAt = Date.now();
618
671
  }
672
+ return {
673
+ content: [{ type: "text", text: `Stop requested for async run ${target.asyncId}.` }],
674
+ details: { mode: "management", results: [] },
675
+ };
676
+ } catch (error) {
677
+ const message = error instanceof Error ? error.message : String(error);
678
+ return {
679
+ content: [{ type: "text", text: `Failed to stop async run ${target.asyncId}: ${message}` }],
680
+ isError: true,
681
+ details: { mode: "management", results: [] },
682
+ };
619
683
  }
620
- requestAsyncSteer(input.location.asyncDir, { message: input.message, targetIndex: input.index, source: "steer-action" });
621
- const tracked = input.state.asyncJobs.get(status.runId);
622
- if (tracked) tracked.updatedAt = Date.now();
623
- const childText = input.index !== undefined ? ` child ${input.index}` : " running child";
624
- return {
625
- content: [{ type: "text", text: `Steering queued for async run ${status.runId}${childText}. Delivery requires a live Pi child session that supports mid-run steering.` }],
626
- details: { mode: "management", results: [] },
627
- };
628
684
  }
629
685
 
630
686
  function duplicateNames(names: string[]): string[] {
@@ -754,6 +810,7 @@ function appendStepToAsyncChain(input: {
754
810
  currentModelProvider: input.ctx.model?.provider,
755
811
  currentModel: input.ctx.model,
756
812
  modelScope: discoveredForAppend.modelScope,
813
+ interactive: input.ctx.hasUI,
757
814
  };
758
815
  const built = buildAsyncRunnerSteps(resolved.id, {
759
816
  chain: wrapChainTasksForFork(input.params.chain, contextPolicy),
@@ -766,6 +823,7 @@ function appendStepToAsyncChain(input: {
766
823
  chainSkills,
767
824
  dynamicFanoutMaxItems: input.deps.config.chain?.dynamicFanout?.maxItems,
768
825
  maxSubagentDepth: resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth),
826
+ waitToolEnabled: input.deps.waitToolEnabled,
769
827
  asyncDir: resolved.location.asyncDir,
770
828
  validateOutputBindings: false,
771
829
  });
@@ -858,6 +916,7 @@ function validateNestedSessionFile(run: NestedRunSummary, trustedSessionRoots: s
858
916
  function resolveNestedResumeTarget(match: ResolvedSubagentRunId & { kind: "nested" }, trustedSessionRoots: string[]): NestedResumeSourceTarget {
859
917
  const run = match.match.run;
860
918
  if (run.state === "running" || run.state === "queued") throw new Error(`Nested run '${run.id}' is live; route the follow-up to the owner process instead.`);
919
+ if (run.state === "stopped") throw new Error(`Nested run '${run.id}' was stopped and cannot be resumed. Start a new run instead.`);
861
920
  const agent = nestedRunAgent(run);
862
921
  if (!agent) throw new Error(`Could not determine child agent for nested run '${run.id}'.`);
863
922
  const state = run.state === "complete" || run.state === "failed" || run.state === "paused" ? run.state : "failed";
@@ -869,7 +928,6 @@ function resolveNestedResumeTarget(match: ResolvedSubagentRunId & { kind: "neste
869
928
  state,
870
929
  agent,
871
930
  index: 0,
872
- intercomTarget: resolveSubagentIntercomTarget(run.id, agent, 0),
873
931
  cwd: asyncDir ? path.dirname(asyncDir) : undefined,
874
932
  sessionFile: validateNestedSessionFile(run, trustedSessionRoots),
875
933
  };
@@ -913,7 +971,7 @@ function directNestedAsyncInterrupt(target: ResolvedSubagentRunId & { kind: "nes
913
971
  }
914
972
  }
915
973
 
916
- function directNestedAsyncSteer(input: { target: ResolvedSubagentRunId & { kind: "nested" }; message: string; index?: number }): AgentToolResult<Details> | undefined {
974
+ async function directNestedAsyncSteer(input: { target: ResolvedSubagentRunId & { kind: "nested" }; message: string; index?: number; signal?: AbortSignal }): Promise<AgentToolResult<Details> | undefined> {
917
975
  const run = input.target.match.run;
918
976
  const asyncDir = resolveNestedAsyncDir(input.target.match.rootRunId, run);
919
977
  if (!asyncDir) return undefined;
@@ -925,8 +983,32 @@ function directNestedAsyncSteer(input: { target: ResolvedSubagentRunId & { kind:
925
983
  const step = steps[input.index];
926
984
  if (step && step.status !== "running" && step.status !== "pending") return { content: [{ type: "text", text: `Nested async run ${run.id} child ${input.index} is ${step.status} and cannot be steered.` }], isError: true, details: { mode: "management", results: [] } };
927
985
  }
928
- requestAsyncSteer(asyncDir, { message: input.message, targetIndex: input.index, source: "nested-steer" });
929
- return { content: [{ type: "text", text: `Steering queued for nested async run ${run.id}. Delivery requires a live Pi child session that supports mid-run steering.` }], details: { mode: "management", results: [] } };
986
+ const runningIndexes = steps
987
+ .map((step, index) => step.status === "running" ? index : undefined)
988
+ .filter((index): index is number => index !== undefined);
989
+ const effectiveTargetIndex = input.index ?? (status.mode === "single" && runningIndexes.length === 0 && steps[0]?.status === "pending" ? 0 : undefined);
990
+ const targetIndexes = effectiveTargetIndex !== undefined ? [effectiveTargetIndex] : runningIndexes;
991
+ if (targetIndexes.length === 0) return { content: [{ type: "text", text: `Nested async run ${run.id} has no running child to steer.` }], isError: true, details: { mode: "management", results: [] } };
992
+ const requestId = randomUUID();
993
+ try {
994
+ requestAsyncSteer(asyncDir, {
995
+ message: input.message,
996
+ ...(effectiveTargetIndex !== undefined ? { targetIndex: effectiveTargetIndex } : { targetIndexes }),
997
+ source: "nested-steer",
998
+ id: requestId,
999
+ });
1000
+ } catch (error) {
1001
+ return { content: [{ type: "text", text: `Failed to queue steering for nested async run ${run.id}: ${error instanceof Error ? error.message : String(error)}` }], isError: true, details: { mode: "management", results: [] } };
1002
+ }
1003
+ const targets = targetIndexes.map((index) => ({ index, state: steps[index]?.status === "pending" ? "scheduled" as const : "pending" as const }));
1004
+ if (targets.every((target) => target.state === "scheduled")) {
1005
+ const scheduled = { requestId, state: "scheduled" as const, sourceRunId: run.id, targets };
1006
+ return { content: [{ type: "text", text: `Steering scheduled for nested async run ${run.id} (request ${requestId}).` }], details: { mode: "management", results: [], steering: scheduled } };
1007
+ }
1008
+ const waited = await waitForSteeringAction({ asyncDir, sourceRunId: run.id, requestId, timeoutMs: 3_000, signal: input.signal });
1009
+ const result = waited ?? { requestId, state: "pending" as const, sourceRunId: run.id, targets };
1010
+ const stateText = result.state === "delivered" ? "delivered" : result.state === "failed" ? "failed" : result.state === "partial" ? "partial" : "pending";
1011
+ return { content: [{ type: "text", text: `Steering ${stateText} for nested async run ${run.id} (request ${requestId}).` }], ...(result.state === "failed" || result.state === "partial" ? { isError: true } : {}), details: { mode: "management", results: [], steering: result } };
930
1012
  }
931
1013
 
932
1014
  async function interruptNestedRun(target: ResolvedSubagentRunId & { kind: "nested" }): Promise<AgentToolResult<Details>> {
@@ -948,10 +1030,10 @@ async function resumeLiveNestedRun(input: { target: ResolvedSubagentRunId & { ki
948
1030
  return { content: [{ type: "text", text: `Nested run ${run.id} appears live but its owner route is not reachable. Wait for completion, then retry action='resume'.` }], isError: true, details: { mode: "management", results: [] } };
949
1031
  }
950
1032
 
951
- function steerNestedRun(input: { target: ResolvedSubagentRunId & { kind: "nested" }; message: string; index?: number }): AgentToolResult<Details> {
1033
+ async function steerNestedRun(input: { target: ResolvedSubagentRunId & { kind: "nested" }; message: string; index?: number; signal?: AbortSignal }): Promise<AgentToolResult<Details>> {
952
1034
  const run = input.target.match.run;
953
1035
  if (run.state !== "running" && run.state !== "queued") return { content: [{ type: "text", text: `Nested run ${run.id} is ${run.state} and cannot be steered.` }], isError: true, details: { mode: "management", results: [] } };
954
- const direct = directNestedAsyncSteer(input);
1036
+ const direct = await directNestedAsyncSteer(input);
955
1037
  if (direct) return direct;
956
1038
  return { content: [{ type: "text", text: `Nested run ${run.id} is not a live async Pi child session with a steering inbox. action='steer' cannot target foreground nested runs.` }], isError: true, details: { mode: "management", results: [] } };
957
1039
  }
@@ -961,6 +1043,7 @@ async function resumeAsyncRun(input: {
961
1043
  requestCwd: string;
962
1044
  ctx: ExtensionContext;
963
1045
  deps: ExecutorDeps;
1046
+ absoluteDeadlineAt?: number;
964
1047
  }): Promise<AgentToolResult<Details>> {
965
1048
  const followUp = (input.params.message ?? input.params.task ?? "").trim();
966
1049
  const attachChain = (input.params.chain?.length ?? 0) > 0 ? input.params.chain as ChainStep[] : undefined;
@@ -971,6 +1054,15 @@ async function resumeAsyncRun(input: {
971
1054
  details: { mode: "management", results: [] },
972
1055
  };
973
1056
  }
1057
+ const acceptanceErrors = validateExecutionAcceptance(input.params);
1058
+ if (acceptanceErrors.length > 0) {
1059
+ return {
1060
+ content: [{ type: "text", text: `Cannot resume: ${acceptanceErrors.join(" ")}` }],
1061
+ isError: true,
1062
+ details: { mode: "management", results: [] },
1063
+ };
1064
+ }
1065
+ input.deps.state.currentSessionId = resolveCurrentSessionId(input.ctx.sessionManager);
974
1066
 
975
1067
  let target: ResumeSourceTarget;
976
1068
  const parentSessionFile = input.ctx.sessionManager.getSessionFile() ?? null;
@@ -1009,34 +1101,14 @@ async function resumeAsyncRun(input: {
1009
1101
  }
1010
1102
 
1011
1103
  if (target.kind === "live" && !attachChain) {
1012
- const interrupt = interruptLiveAsyncResumeTarget({
1013
- target,
1014
- state: input.deps.state,
1015
- kill: input.deps.kill,
1016
- resultsDir: RESULTS_DIR,
1017
- });
1018
- if (!interrupt.ok) {
1019
- return {
1020
- content: [{ type: "text", text: interrupt.message }],
1021
- isError: true,
1022
- details: { mode: "management", results: [] },
1023
- };
1024
- }
1025
- const delivered = await deliverSubagentIntercomMessageEvent(
1026
- input.deps.pi.events,
1027
- target.intercomTarget,
1028
- `Follow-up for async run ${target.runId} (${target.agent}):\n\n${followUp}`,
1029
- 500,
1030
- { source: "async-resume", runId: target.runId, agent: target.agent, index: target.index },
1031
- );
1032
- if (delivered) {
1033
- return {
1034
- content: [{ type: "text", text: [`Interrupted live async child, then delivered follow-up.`, `Run: ${target.runId}`, `Intercom target: ${target.intercomTarget}`].join("\n") }],
1035
- details: { mode: "management", results: [] },
1036
- };
1037
- }
1038
1104
  return {
1039
- content: [{ type: "text", text: [`Async child appears live but its intercom target is not registered.`, `Run: ${target.runId}`, `Intercom target: ${target.intercomTarget}`, `Wait for completion, then retry action='resume'.`].join("\n") }],
1105
+ content: [{
1106
+ type: "text",
1107
+ text: [
1108
+ `Async child '${target.runId}' index ${target.index} is still running. action='resume' only revives paused, completed, or failed children.`,
1109
+ `Send live input with subagent({ action: "steer", id: "${target.runId}", index: ${target.index}, message: "..." }).`,
1110
+ ].join("\n"),
1111
+ }],
1040
1112
  isError: true,
1041
1113
  details: { mode: "management", results: [] },
1042
1114
  };
@@ -1066,7 +1138,18 @@ async function resumeAsyncRun(input: {
1066
1138
  const agents = intercomBridge.active
1067
1139
  ? discoveredAgents.map((agent) => applyIntercomBridgeToAgent(agent, intercomBridge))
1068
1140
  : discoveredAgents;
1069
- const agentConfig = agents.find((agent) => agent.name === target.agent);
1141
+ const recoveryDescriptor = "recoveryDescriptor" in target ? target.recoveryDescriptor : undefined;
1142
+ const discoveredAgentConfig = agents.find((agent) => agent.name === target.agent);
1143
+ const agentConfig: AgentConfig | undefined = discoveredAgentConfig ?? (recoveryDescriptor ? {
1144
+ name: recoveryDescriptor.agent,
1145
+ description: "Persisted async recovery contract",
1146
+ systemPrompt: "",
1147
+ systemPromptMode: recoveryDescriptor.systemPromptMode,
1148
+ inheritProjectContext: recoveryDescriptor.inheritProjectContext,
1149
+ inheritSkills: recoveryDescriptor.inheritSkills,
1150
+ source: "project",
1151
+ filePath: recoveryDescriptor.agentFilePath ?? path.join(recoveryDescriptor.cwd, ".pi-subagents-recovery-agent"),
1152
+ } : undefined);
1070
1153
  if (!agentConfig) {
1071
1154
  return {
1072
1155
  content: [{ type: "text", text: `Unknown agent for resume: ${target.agent}` }],
@@ -1094,11 +1177,14 @@ async function resumeAsyncRun(input: {
1094
1177
  const artifactConfig: ArtifactConfig = { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false };
1095
1178
  const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo);
1096
1179
  const contextPolicy = resolveExplicitContextPolicy(input.params);
1180
+ const workflowTask = (input.params.task ?? followUp) || undefined;
1181
+ const goal = resolveAsyncEventGoal(workflowTask, attachChain);
1097
1182
  const chain = wrapChainTasksForFork(attachChain, contextPolicy);
1098
1183
  const normalized = normalizeSkillInput(input.params.skill);
1099
1184
  const result = executeAsyncChain(runId, {
1100
1185
  chain,
1101
- task: (input.params.task ?? followUp) || undefined,
1186
+ task: workflowTask,
1187
+ goal,
1102
1188
  attachRoot: {
1103
1189
  runId: target.runId,
1104
1190
  asyncDir: target.asyncDir ?? path.join(ASYNC_DIR, target.runId),
@@ -1116,6 +1202,7 @@ async function resumeAsyncRun(input: {
1116
1202
  currentModelProvider: input.ctx.model?.provider,
1117
1203
  currentModel: input.ctx.model,
1118
1204
  modelScope,
1205
+ interactive: input.ctx.hasUI,
1119
1206
  },
1120
1207
  availableModels,
1121
1208
  cwd: effectiveCwd,
@@ -1127,6 +1214,7 @@ async function resumeAsyncRun(input: {
1127
1214
  chainSkills: normalized === false ? [] : (normalized ?? []),
1128
1215
  dynamicFanoutMaxItems: input.deps.config.chain?.dynamicFanout?.maxItems,
1129
1216
  maxSubagentDepth: resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth),
1217
+ waitToolEnabled: input.deps.waitToolEnabled,
1130
1218
  worktreeSetupHook: input.deps.config.worktreeSetupHook,
1131
1219
  worktreeSetupHookTimeoutMs: input.deps.config.worktreeSetupHookTimeoutMs,
1132
1220
  worktreeBaseDir: input.deps.config.worktreeBaseDir,
@@ -1144,17 +1232,19 @@ async function resumeAsyncRun(input: {
1144
1232
  result.details.asyncDir ? `Async dir: ${result.details.asyncDir}` : undefined,
1145
1233
  `Status if needed: subagent({ action: "status", id: "${attachedId}" })`,
1146
1234
  ].filter((line): line is string => Boolean(line));
1147
- return { content: [{ type: "text", text: formatAsyncStartedMessage(lines.join("\n")) }], details: result.details };
1235
+ return { content: [{ type: "text", text: formatAsyncStartedMessage(lines.join("\n"), input.ctx.hasUI) }], details: result.details };
1148
1236
  }
1149
1237
 
1150
1238
  const runId = randomUUID().slice(0, 8);
1151
- const artifactConfig: ArtifactConfig = { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false };
1152
- const artifactsDir = getArtifactsDir(parentSessionFile, effectiveCwd);
1239
+ const recoveryAgentConfig = recoveryDescriptor ? applySteeringRecoveryAgentConfig(agentConfig, recoveryDescriptor) : agentConfig;
1240
+ const artifactConfig: ArtifactConfig = recoveryDescriptor?.artifactConfig ?? { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false };
1241
+ const artifactsDir = recoveryDescriptor?.artifactsDir ?? getArtifactsDir(parentSessionFile, effectiveCwd);
1153
1242
  const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo);
1154
1243
  const result = executeAsyncSingle(runId, {
1155
1244
  agent: target.agent,
1156
1245
  task: buildRevivedAsyncTask(target, followUp),
1157
- agentConfig,
1246
+ goal: followUp,
1247
+ agentConfig: recoveryAgentConfig,
1158
1248
  ctx: {
1159
1249
  pi: input.deps.pi,
1160
1250
  cwd: input.requestCwd,
@@ -1163,23 +1253,42 @@ async function resumeAsyncRun(input: {
1163
1253
  currentModelProvider: input.ctx.model?.provider,
1164
1254
  currentModel: input.ctx.model,
1165
1255
  modelScope,
1256
+ interactive: input.ctx.hasUI,
1166
1257
  },
1167
1258
  cwd: effectiveCwd,
1168
- maxOutput: input.params.maxOutput,
1259
+ maxOutput: input.params.maxOutput ?? recoveryDescriptor?.maxOutput,
1169
1260
  artifactsDir,
1170
1261
  artifactConfig,
1171
- shareEnabled: input.params.share === true,
1262
+ shareEnabled: recoveryDescriptor?.share ?? input.params.share === true,
1172
1263
  sessionRoot: input.deps.getSubagentSessionRoot(parentSessionFile),
1264
+ ...(recoveryDescriptor?.sessionDir ? { sessionDir: recoveryDescriptor.sessionDir } : {}),
1173
1265
  sessionFile: target.sessionFile,
1266
+ revivalLease: {
1267
+ sessionFile: target.sessionFile,
1268
+ runId,
1269
+ sourceRunId: target.runId,
1270
+ ...(input.deps.state.currentSessionId ? { parentSessionId: input.deps.state.currentSessionId } : {}),
1271
+ },
1272
+ modelOverride: input.params.model ?? recoveryDescriptor?.model ?? target.model,
1273
+ thinkingOverride: input.params.model ? undefined : recoveryDescriptor?.thinking ?? target.thinking,
1174
1274
  outputBaseDir: resolveSingleRunOutputBaseDir(input.deps, artifactsDir, runId),
1175
- maxSubagentDepth: resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth),
1275
+ maxSubagentDepth: recoveryDescriptor?.maxSubagentDepth ?? resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth),
1276
+ waitToolEnabled: input.deps.waitToolEnabled,
1176
1277
  worktreeSetupHook: input.deps.config.worktreeSetupHook,
1177
1278
  worktreeSetupHookTimeoutMs: input.deps.config.worktreeSetupHookTimeoutMs,
1178
1279
  worktreeBaseDir: input.deps.config.worktreeBaseDir,
1179
- controlConfig: resolveControlConfig(input.deps.config.control, input.params.control),
1280
+ controlConfig: recoveryDescriptor?.controlConfig ?? resolveControlConfig(input.deps.config.control, input.params.control),
1180
1281
  controlIntercomTarget: intercomBridge.active ? intercomBridge.orchestratorTarget : undefined,
1181
1282
  childIntercomTarget: intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
1182
1283
  availableModels,
1284
+ output: recoveryDescriptor?.outputPath,
1285
+ outputMode: recoveryDescriptor?.outputMode,
1286
+ ...(recoveryDescriptor?.skills ? { skills: [...recoveryDescriptor.skills] } : {}),
1287
+ ...(recoveryDescriptor?.acceptance !== undefined && input.params.acceptance === undefined ? { acceptance: recoveryDescriptor.acceptance } : {}),
1288
+ ...(input.params.timeoutMs !== undefined ? { timeoutMs: input.params.timeoutMs } : {}),
1289
+ ...(input.absoluteDeadlineAt !== undefined ? { absoluteDeadlineAt: input.absoluteDeadlineAt } : {}),
1290
+ ...(input.params.turnBudget !== undefined ? { turnBudget: input.params.turnBudget } : {}),
1291
+ ...(input.params.toolBudget !== undefined ? { toolBudget: input.params.toolBudget } : {}),
1183
1292
  });
1184
1293
  if (result.isError) return result;
1185
1294
 
@@ -1195,7 +1304,7 @@ async function resumeAsyncRun(input: {
1195
1304
  revivedTarget ? `Intercom target: ${revivedTarget} (if registered)` : undefined,
1196
1305
  `Status if needed: subagent({ action: "status", id: "${revivedId}" })`,
1197
1306
  ].filter((line): line is string => Boolean(line));
1198
- return { content: [{ type: "text", text: formatAsyncStartedMessage(lines.join("\n")) }], details: result.details };
1307
+ return { content: [{ type: "text", text: formatAsyncStartedMessage(lines.join("\n"), input.ctx.hasUI) }], details: result.details };
1199
1308
  }
1200
1309
 
1201
1310
  function resultSummaryForIntercom(result: SingleResult): string {
@@ -1413,25 +1522,6 @@ function validateExecutionChainBindings(params: SubagentParamsLike, dynamicFanou
1413
1522
  return null;
1414
1523
  }
1415
1524
 
1416
- function validateExecutionAcceptance(params: SubagentParamsLike): string[] {
1417
- const errors: string[] = [];
1418
- errors.push(...validateAcceptanceInput(params.acceptance, "acceptance"));
1419
- for (const [index, task] of (params.tasks ?? []).entries()) {
1420
- errors.push(...validateAcceptanceInput(task.acceptance, `tasks[${index}].acceptance`));
1421
- }
1422
- for (const [stepIndex, step] of (params.chain ?? []).entries()) {
1423
- errors.push(...validateAcceptanceInput((step as { acceptance?: unknown }).acceptance, `chain[${stepIndex}].acceptance`));
1424
- if (isParallelStep(step)) {
1425
- for (const [taskIndex, task] of step.parallel.entries()) {
1426
- errors.push(...validateAcceptanceInput(task.acceptance, `chain[${stepIndex}].parallel[${taskIndex}].acceptance`));
1427
- }
1428
- } else if (isDynamicParallelStep(step)) {
1429
- errors.push(...validateAcceptanceInput(step.parallel.acceptance, `chain[${stepIndex}].parallel.acceptance`));
1430
- }
1431
- }
1432
- return errors;
1433
- }
1434
-
1435
1525
  function getRequestedModeLabel(params: SubagentParamsLike): Details["mode"] {
1436
1526
  if ((params.chain?.length ?? 0) > 0) return "chain";
1437
1527
  if ((params.tasks?.length ?? 0) > 0) return "parallel";
@@ -1492,6 +1582,25 @@ function buildRequestedModeError(params: SubagentParamsLike, message: string): A
1492
1582
  );
1493
1583
  }
1494
1584
 
1585
+ function applySingleAgentLaunchDefaults(params: SubagentParamsLike, agents: AgentConfig[]): SubagentParamsLike {
1586
+ if ((params.chain?.length ?? 0) > 0 || (params.tasks?.length ?? 0) > 0 || !params.agent) return params;
1587
+ const agent = agents.find((candidate) => candidate.name === params.agent);
1588
+ if (!agent) return params;
1589
+ return {
1590
+ ...params,
1591
+ ...(params.async === undefined && agent.defaultAsync !== undefined ? { async: agent.defaultAsync } : {}),
1592
+ ...(params.timeoutMs === undefined && params.maxRuntimeMs === undefined && agent.defaultTimeoutMs !== undefined
1593
+ ? { timeoutMs: agent.defaultTimeoutMs }
1594
+ : {}),
1595
+ ...(params.turnBudget === undefined && agent.defaultTurnBudget !== undefined
1596
+ ? { turnBudget: agent.defaultTurnBudget }
1597
+ : {}),
1598
+ ...(params.acceptance === undefined && agent.defaultAcceptance !== undefined
1599
+ ? { acceptance: agent.defaultAcceptance }
1600
+ : {}),
1601
+ };
1602
+ }
1603
+
1495
1604
  function resolveForegroundTimeout(params: SubagentParamsLike): { timeoutMs?: number; error?: string } {
1496
1605
  const rawTimeout = params.timeoutMs;
1497
1606
  const rawMaxRuntime = params.maxRuntimeMs;
@@ -1508,20 +1617,6 @@ function resolveForegroundTimeout(params: SubagentParamsLike): { timeoutMs?: num
1508
1617
  return { timeoutMs: rawTimeout ?? rawMaxRuntime };
1509
1618
  }
1510
1619
 
1511
- function resolveTurnBudget(params: SubagentParamsLike, config: ExtensionConfig): { turnBudget?: ResolvedTurnBudget; error?: string } {
1512
- const raw = params.turnBudget ?? config.turnBudget;
1513
- if (raw === undefined) return {};
1514
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return { error: "turnBudget must be an object with maxTurns and optional graceTurns." };
1515
- if (typeof raw.maxTurns !== "number" || !Number.isInteger(raw.maxTurns) || raw.maxTurns < 1) {
1516
- return { error: "turnBudget.maxTurns must be an integer >= 1." };
1517
- }
1518
- const graceTurns = raw.graceTurns ?? DEFAULT_TURN_BUDGET_GRACE_TURNS;
1519
- if (typeof graceTurns !== "number" || !Number.isInteger(graceTurns) || graceTurns < 0) {
1520
- return { error: "turnBudget.graceTurns must be an integer >= 0." };
1521
- }
1522
- return { turnBudget: { maxTurns: raw.maxTurns, graceTurns } };
1523
- }
1524
-
1525
1620
  function resolveToolBudget(raw: unknown, label = "toolBudget"): { toolBudget?: ResolvedToolBudget; error?: string } {
1526
1621
  const resolved = validateToolBudgetConfig(raw, label);
1527
1622
  return { toolBudget: resolved.budget, error: resolved.error };
@@ -1607,6 +1702,19 @@ function withForkContext(
1607
1702
  };
1608
1703
  }
1609
1704
 
1705
+ function withForkThinkingNotes(
1706
+ result: AgentToolResult<Details>,
1707
+ downgrades: Map<number, string>,
1708
+ ): AgentToolResult<Details> {
1709
+ if (downgrades.size === 0) return result;
1710
+ const children = [...downgrades.entries()]
1711
+ .sort(([a], [b]) => a - b)
1712
+ .map(([index, agent]) => `${agent} (child ${index})`)
1713
+ .join(", ");
1714
+ const note = `Note: fork context forced thinking off for ${children}. The forked transcript contained signed Anthropic thinking blocks that were sanitized, and Anthropic children cannot resume such a transcript with thinking enabled. Use context: "fresh" when an Anthropic child needs thinking.`;
1715
+ return { ...result, content: [...result.content, { type: "text", text: note }] };
1716
+ }
1717
+
1610
1718
  function toExecutionErrorResult(params: SubagentParamsLike, error: unknown): AgentToolResult<Details> {
1611
1719
  const message = error instanceof Error ? error.message : String(error);
1612
1720
  return withForkContext(
@@ -1621,7 +1729,7 @@ function toExecutionErrorResult(params: SubagentParamsLike, error: unknown): Age
1621
1729
 
1622
1730
  function collectChainSessionFiles(
1623
1731
  chain: ChainStep[],
1624
- sessionFileForTask: (agentName: string, idx?: number) => string | undefined,
1732
+ sessionFileForTask: ForkSessionFileForTask,
1625
1733
  dynamicFanoutMaxItems?: number,
1626
1734
  ): (string | undefined)[] {
1627
1735
  const sessionFiles: (string | undefined)[] = [];
@@ -1629,7 +1737,7 @@ function collectChainSessionFiles(
1629
1737
  for (const step of chain) {
1630
1738
  if (isParallelStep(step)) {
1631
1739
  for (const task of step.parallel) {
1632
- sessionFiles.push(sessionFileForTask(task.agent, flatIndex));
1740
+ sessionFiles.push(sessionFileForTask(task.agent, flatIndex, task.model));
1633
1741
  flatIndex++;
1634
1742
  }
1635
1743
  continue;
@@ -1637,12 +1745,13 @@ function collectChainSessionFiles(
1637
1745
  if (isDynamicParallelStep(step)) {
1638
1746
  const maxItems = step.expand.maxItems ?? dynamicFanoutMaxItems ?? 0;
1639
1747
  for (let itemIndex = 0; itemIndex < maxItems; itemIndex++) {
1640
- sessionFiles.push(sessionFileForTask(step.parallel.agent, flatIndex));
1748
+ sessionFiles.push(sessionFileForTask(step.parallel.agent, flatIndex, step.parallel.model));
1641
1749
  flatIndex++;
1642
1750
  }
1643
1751
  continue;
1644
1752
  }
1645
- sessionFiles.push(sessionFileForTask((step as SequentialStep).agent, flatIndex));
1753
+ const sequential = step as SequentialStep;
1754
+ sessionFiles.push(sessionFileForTask(sequential.agent, flatIndex, sequential.model));
1646
1755
  flatIndex++;
1647
1756
  }
1648
1757
  return sessionFiles;
@@ -1650,7 +1759,7 @@ function collectChainSessionFiles(
1650
1759
 
1651
1760
  function collectChainThinkingOverrides(
1652
1761
  chain: ChainStep[],
1653
- thinkingOverrideForTask: (agentName: string, idx?: number) => AgentConfig["thinking"] | undefined,
1762
+ thinkingOverrideForTask: ForkThinkingOverrideForTask,
1654
1763
  dynamicFanoutMaxItems?: number,
1655
1764
  ): (AgentConfig["thinking"] | undefined)[] {
1656
1765
  const thinkingOverrides: (AgentConfig["thinking"] | undefined)[] = [];
@@ -1658,7 +1767,7 @@ function collectChainThinkingOverrides(
1658
1767
  for (const step of chain) {
1659
1768
  if (isParallelStep(step)) {
1660
1769
  for (const task of step.parallel) {
1661
- thinkingOverrides.push(thinkingOverrideForTask(task.agent, flatIndex));
1770
+ thinkingOverrides.push(thinkingOverrideForTask(task.agent, flatIndex, task.model));
1662
1771
  flatIndex++;
1663
1772
  }
1664
1773
  continue;
@@ -1666,17 +1775,42 @@ function collectChainThinkingOverrides(
1666
1775
  if (isDynamicParallelStep(step)) {
1667
1776
  const maxItems = step.expand.maxItems ?? dynamicFanoutMaxItems ?? 0;
1668
1777
  for (let itemIndex = 0; itemIndex < maxItems; itemIndex++) {
1669
- thinkingOverrides.push(thinkingOverrideForTask(step.parallel.agent, flatIndex));
1778
+ thinkingOverrides.push(thinkingOverrideForTask(step.parallel.agent, flatIndex, step.parallel.model));
1670
1779
  flatIndex++;
1671
1780
  }
1672
1781
  continue;
1673
1782
  }
1674
- thinkingOverrides.push(thinkingOverrideForTask((step as SequentialStep).agent, flatIndex));
1783
+ const sequential = step as SequentialStep;
1784
+ thinkingOverrides.push(thinkingOverrideForTask(sequential.agent, flatIndex, sequential.model));
1675
1785
  flatIndex++;
1676
1786
  }
1677
1787
  return thinkingOverrides;
1678
1788
  }
1679
1789
 
1790
+ function firstChainAgent(chain: ChainStep[]): string | undefined {
1791
+ const first = chain[0];
1792
+ if (!first) return undefined;
1793
+ if (isParallelStep(first)) return first.parallel[0]?.agent;
1794
+ if (isDynamicParallelStep(first)) return first.parallel.agent;
1795
+ return (first as SequentialStep).agent;
1796
+ }
1797
+
1798
+ function firstRawChainTask(chain: ChainStep[]): string | undefined {
1799
+ const first = chain[0];
1800
+ if (!first) return undefined;
1801
+ if (isParallelStep(first)) return first.parallel[0]?.task;
1802
+ if (isDynamicParallelStep(first)) return first.parallel.task;
1803
+ return (first as SequentialStep).task;
1804
+ }
1805
+
1806
+ function resolveAsyncEventGoal(workflowTask: string | undefined, rawChain: ChainStep[], unwrapForkFallback = false): string {
1807
+ if (workflowTask?.trim()) return workflowTask;
1808
+ const fallback = firstRawChainTask(rawChain) || "";
1809
+ if (!unwrapForkFallback) return fallback;
1810
+ const forkPrefix = `${DEFAULT_FORK_PREAMBLE}\n\nTask:\n`;
1811
+ return fallback.startsWith(forkPrefix) ? fallback.slice(forkPrefix.length) : fallback;
1812
+ }
1813
+
1680
1814
  function wrapChainTasksForFork(chain: ChainStep[], contextPolicy: AgentDefaultContextPolicy): ChainStep[] {
1681
1815
  return chain.map((step, stepIndex) => {
1682
1816
  if (isParallelStep(step)) {
@@ -1714,17 +1848,17 @@ function wrapChainTasksForFork(chain: ChainStep[], contextPolicy: AgentDefaultCo
1714
1848
  function preflightForkSessionsForStaticTasks(
1715
1849
  params: SubagentParamsLike,
1716
1850
  contextPolicy: AgentDefaultContextPolicy,
1717
- sessionFileForTask: (agentName: string, idx?: number) => string | undefined,
1851
+ sessionFileForTask: ForkSessionFileForTask,
1718
1852
  dynamicFanoutMaxItems?: number,
1719
1853
  ): void {
1720
1854
  if (!contextPolicy.usesFork) return;
1721
1855
  if (params.agent) {
1722
- if (shouldForkAgent(contextPolicy, params.agent)) sessionFileForTask(params.agent, 0);
1856
+ if (shouldForkAgent(contextPolicy, params.agent)) sessionFileForTask(params.agent, 0, params.model);
1723
1857
  return;
1724
1858
  }
1725
1859
  if (params.tasks) {
1726
1860
  params.tasks.forEach((task, index) => {
1727
- if (shouldForkAgent(contextPolicy, task.agent)) sessionFileForTask(task.agent, index);
1861
+ if (shouldForkAgent(contextPolicy, task.agent)) sessionFileForTask(task.agent, index, task.model);
1728
1862
  });
1729
1863
  return;
1730
1864
  }
@@ -1733,7 +1867,7 @@ function preflightForkSessionsForStaticTasks(
1733
1867
  for (const step of params.chain) {
1734
1868
  if (isParallelStep(step)) {
1735
1869
  for (const task of step.parallel) {
1736
- if (shouldForkAgent(contextPolicy, task.agent)) sessionFileForTask(task.agent, flatIndex);
1870
+ if (shouldForkAgent(contextPolicy, task.agent)) sessionFileForTask(task.agent, flatIndex, task.model);
1737
1871
  flatIndex++;
1738
1872
  }
1739
1873
  continue;
@@ -1741,13 +1875,13 @@ function preflightForkSessionsForStaticTasks(
1741
1875
  if (isDynamicParallelStep(step)) {
1742
1876
  const maxItems = step.expand.maxItems ?? dynamicFanoutMaxItems ?? 0;
1743
1877
  if (shouldForkAgent(contextPolicy, step.parallel.agent)) {
1744
- for (let itemIndex = 0; itemIndex < maxItems; itemIndex++) sessionFileForTask(step.parallel.agent, flatIndex + itemIndex);
1878
+ for (let itemIndex = 0; itemIndex < maxItems; itemIndex++) sessionFileForTask(step.parallel.agent, flatIndex + itemIndex, step.parallel.model);
1745
1879
  }
1746
1880
  flatIndex += maxItems;
1747
1881
  continue;
1748
1882
  }
1749
1883
  const sequential = step as SequentialStep;
1750
- if (shouldForkAgent(contextPolicy, sequential.agent)) sessionFileForTask(sequential.agent, flatIndex);
1884
+ if (shouldForkAgent(contextPolicy, sequential.agent)) sessionFileForTask(sequential.agent, flatIndex, sequential.model);
1751
1885
  flatIndex++;
1752
1886
  }
1753
1887
  }
@@ -1814,6 +1948,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1814
1948
  currentModelProvider: ctx.model?.provider,
1815
1949
  currentModel: ctx.model,
1816
1950
  modelScope: data.modelScope,
1951
+ interactive: ctx.hasUI,
1817
1952
  };
1818
1953
  const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo);
1819
1954
  const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
@@ -1822,18 +1957,14 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1822
1957
  const childIntercomTarget = intercomBridge.active ? (agent: string, index: number) => resolveSubagentIntercomTarget(id, agent, index) : undefined;
1823
1958
 
1824
1959
  if (hasTasks && params.tasks) {
1825
- const agentConfigs = params.tasks.map((task) => agents.find((agent) => agent.name === task.agent));
1826
- const modelOverrides = params.tasks.map((task, index) =>
1827
- resolveSubagentModelOverride(task.model ?? agentConfigs[index]?.model, ctx.model, availableModels, currentProvider, { scope: data.modelScope, source: task.model ? "explicit" : "inherited" }),
1828
- );
1829
1960
  const skillOverrides = params.tasks.map((task) => normalizeSkillInput(task.skill));
1830
1961
  const parallelTasks = params.tasks.map((task, index) => ({
1831
1962
  agent: task.agent,
1832
1963
  task: shouldForkAgent(contextPolicy, task.agent) ? wrapForkTask(task.task) : task.task,
1833
1964
  cwd: task.cwd,
1834
- ...(modelOverrides[index] ? { model: modelOverrides[index] } : {}),
1965
+ ...(task.model !== undefined ? { model: task.model } : {}),
1835
1966
  ...(skillOverrides[index] !== undefined ? { skill: skillOverrides[index] } : {}),
1836
- ...(task.output === true ? (agentConfigs[index]?.output ? { output: agentConfigs[index]!.output } : {}) : task.output !== undefined ? { output: task.output } : {}),
1967
+ ...(task.output !== undefined && task.output !== true ? { output: task.output } : {}),
1837
1968
  ...(task.outputMode !== undefined ? { outputMode: task.outputMode } : {}),
1838
1969
  ...(task.reads !== undefined && task.reads !== true ? { reads: task.reads } : {}),
1839
1970
  ...(task.progress !== undefined ? { progress: task.progress } : {}),
@@ -1847,6 +1978,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1847
1978
  worktree: params.worktree,
1848
1979
  }],
1849
1980
  resultMode: "parallel",
1981
+ goal: params.tasks[0]?.task ?? "",
1850
1982
  agents,
1851
1983
  ctx: asyncCtx,
1852
1984
  availableModels,
@@ -1857,9 +1989,10 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1857
1989
  shareEnabled,
1858
1990
  sessionRoot,
1859
1991
  chainSkills: [],
1860
- sessionFilesByFlatIndex: params.tasks.map((task, index) => sessionFileForTask(task.agent, index)),
1861
- thinkingOverridesByFlatIndex: params.tasks.map((task, index) => thinkingOverrideForTask(task.agent, index)),
1992
+ sessionFilesByFlatIndex: params.tasks.map((task, index) => sessionFileForTask(task.agent, index, task.model)),
1993
+ thinkingOverridesByFlatIndex: params.tasks.map((task, index) => thinkingOverrideForTask(task.agent, index, task.model)),
1862
1994
  maxSubagentDepth: currentMaxSubagentDepth,
1995
+ waitToolEnabled: deps.waitToolEnabled,
1863
1996
  worktreeSetupHook: deps.config.worktreeSetupHook,
1864
1997
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
1865
1998
  worktreeBaseDir: deps.config.worktreeBaseDir,
@@ -1878,10 +2011,12 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1878
2011
  if (hasChain && params.chain) {
1879
2012
  const normalized = normalizeSkillInput(params.skill);
1880
2013
  const chainSkills = normalized === false ? [] : (normalized ?? []);
1881
- const chain = wrapChainTasksForFork(params.chain as ChainStep[], contextPolicy);
2014
+ const rawChain = params.chain as ChainStep[];
2015
+ const chain = wrapChainTasksForFork(rawChain, contextPolicy);
1882
2016
  return executeAsyncChain(id, {
1883
2017
  chain,
1884
2018
  task: params.task,
2019
+ goal: resolveAsyncEventGoal(params.task, rawChain),
1885
2020
  agents,
1886
2021
  ctx: asyncCtx,
1887
2022
  availableModels,
@@ -1896,6 +2031,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1896
2031
  thinkingOverridesByFlatIndex: collectChainThinkingOverrides(chain, thinkingOverrideForTask, deps.config.chain?.dynamicFanout?.maxItems),
1897
2032
  dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
1898
2033
  maxSubagentDepth: currentMaxSubagentDepth,
2034
+ waitToolEnabled: deps.waitToolEnabled,
1899
2035
  worktreeSetupHook: deps.config.worktreeSetupHook,
1900
2036
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
1901
2037
  worktreeBaseDir: deps.config.worktreeBaseDir,
@@ -1926,10 +2062,11 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1926
2062
  const normalizedSkills = normalizeSkillInput(params.skill);
1927
2063
  const skills = normalizedSkills === false ? [] : normalizedSkills;
1928
2064
  const maxSubagentDepth = resolveChildMaxSubagentDepth(currentMaxSubagentDepth, a.maxSubagentDepth);
1929
- const modelOverride = resolveSubagentModelOverride((params.model as string | undefined) ?? a.model, ctx.model, availableModels, currentProvider, { scope: data.modelScope, source: (params.model as string | undefined) ? "explicit" : "inherited" });
2065
+ const modelOverride = resolveEffectiveSubagentModel(params.model as string | undefined, a.model, ctx.model, availableModels, currentProvider, { scope: data.modelScope });
1930
2066
  return executeAsyncSingle(id, {
1931
2067
  agent: params.agent!,
1932
2068
  task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(params.task ?? "") : (params.task ?? ""),
2069
+ goal: params.task ?? "",
1933
2070
  agentConfig: a,
1934
2071
  ctx: asyncCtx,
1935
2072
  availableModels,
@@ -1939,14 +2076,15 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1939
2076
  artifactConfig,
1940
2077
  shareEnabled,
1941
2078
  sessionRoot,
1942
- sessionFile: sessionFileForTask(params.agent!, 0),
2079
+ sessionFile: sessionFileForTask(params.agent!, 0, modelOverride),
1943
2080
  skills,
1944
2081
  output: effectiveOutput,
1945
2082
  outputMode: effectiveOutputMode,
1946
2083
  outputBaseDir: resolveSingleRunOutputBaseDir(deps, artifactsDir, id),
1947
2084
  modelOverride,
1948
- thinkingOverride: thinkingOverrideForTask(params.agent!, 0),
2085
+ thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride),
1949
2086
  maxSubagentDepth,
2087
+ waitToolEnabled: deps.waitToolEnabled,
1950
2088
  worktreeSetupHook: deps.config.worktreeSetupHook,
1951
2089
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
1952
2090
  worktreeBaseDir: deps.config.worktreeBaseDir,
@@ -2028,7 +2166,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2028
2166
  timeoutMs: data.timeoutMs,
2029
2167
  deadlineAt: data.deadlineAt,
2030
2168
  turnBudget: data.turnBudget,
2031
- onDetachedExit: (index, result) => updateRememberedForegroundChild(deps.state, { runId, mode: "chain", cwd: effectiveCwd, index, result }),
2169
+ onDetachedExit: (index, result) => updateRememberedForegroundChild(deps.state, { runId, mode: "chain", cwd: effectiveCwd, sessionId: data.parentSessionId, index, result, events: deps.pi.events }),
2032
2170
  toolBudget: data.toolBudget,
2033
2171
  configToolBudget: data.configToolBudget,
2034
2172
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
@@ -2051,11 +2189,15 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2051
2189
  currentModelProvider: ctx.model?.provider,
2052
2190
  currentModel: ctx.model,
2053
2191
  modelScope: data.modelScope,
2192
+ interactive: ctx.hasUI,
2054
2193
  };
2055
- const asyncChain = wrapChainTasksForFork(chainResult.requestedAsync.chain, contextPolicy);
2194
+ const rawAsyncChain = chainResult.requestedAsync.chain;
2195
+ const asyncChain = wrapChainTasksForFork(rawAsyncChain, contextPolicy);
2196
+ const firstAgent = firstChainAgent(rawAsyncChain);
2056
2197
  return executeAsyncChain(id, {
2057
2198
  chain: asyncChain,
2058
2199
  task: params.task,
2200
+ goal: resolveAsyncEventGoal(params.task, rawAsyncChain, firstAgent ? shouldForkAgent(contextPolicy, firstAgent) : false),
2059
2201
  agents,
2060
2202
  ctx: asyncCtx,
2061
2203
  availableModels: ctx.modelRegistry.getAvailable().map(toModelInfo),
@@ -2070,6 +2212,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2070
2212
  thinkingOverridesByFlatIndex: collectChainThinkingOverrides(asyncChain, thinkingOverrideForTask, deps.config.chain?.dynamicFanout?.maxItems),
2071
2213
  dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
2072
2214
  maxSubagentDepth: currentMaxSubagentDepth,
2215
+ waitToolEnabled: deps.waitToolEnabled,
2073
2216
  worktreeSetupHook: deps.config.worktreeSetupHook,
2074
2217
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
2075
2218
  worktreeBaseDir: deps.config.worktreeBaseDir,
@@ -2092,7 +2235,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2092
2235
  rawChainDetails.totalCost = sumResultsCost(rawChainDetails.results);
2093
2236
  }
2094
2237
  const chainDetails = rawChainDetails ? compactForegroundDetails(rawChainDetails) : undefined;
2095
- if (chainDetails) rememberForegroundRun(deps.state, { runId, mode: "chain", cwd: effectiveCwd, results: chainDetails.results });
2238
+ if (chainDetails) rememberForegroundRun(deps.state, { runId, mode: "chain", cwd: effectiveCwd, sessionId: data.parentSessionId, results: chainDetails.results });
2096
2239
  const intercomReceipt = chainDetails && !chainDetails.results.some((result) => result.interrupted || result.detached)
2097
2240
  ? await maybeBuildForegroundIntercomReceipt({
2098
2241
  pi: deps.pi,
@@ -2121,12 +2264,13 @@ interface ForegroundParallelRunInput {
2121
2264
  ctx: ExtensionContext;
2122
2265
  state: SubagentState;
2123
2266
  intercomEvents: IntercomEventBus;
2267
+ parentSessionId: string | null;
2124
2268
  signal: AbortSignal;
2125
2269
  runId: string;
2126
2270
  sessionDirForIndex: (idx?: number) => string | undefined;
2127
2271
  sessionFileForIndex: (idx?: number) => string | undefined;
2128
- sessionFileForTask: (agentName: string, idx?: number) => string | undefined;
2129
- thinkingOverrideForTask: (agentName: string, idx?: number) => AgentConfig["thinking"] | undefined;
2272
+ sessionFileForTask: ForkSessionFileForTask;
2273
+ thinkingOverrideForTask: ForkThinkingOverrideForTask;
2130
2274
  shareEnabled: boolean;
2131
2275
  artifactConfig: ArtifactConfig;
2132
2276
  artifactsDir: string;
@@ -2135,6 +2279,7 @@ interface ForegroundParallelRunInput {
2135
2279
  paramsCwd: string;
2136
2280
  progressDir: string;
2137
2281
  maxSubagentDepths: number[];
2282
+ waitToolEnabled?: boolean;
2138
2283
  availableModels: ModelInfo[];
2139
2284
  modelScope?: ModelScopeConfig;
2140
2285
  modelOverrides: (string | undefined)[];
@@ -2268,9 +2413,8 @@ function findDuplicateParallelOutputPath(input: {
2268
2413
  async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Promise<SingleResult[]> {
2269
2414
  // Pre-warm fork session files sequentially before concurrent dispatch to avoid
2270
2415
  // races where multiple workers simultaneously try to branch the same parent session.
2271
- // sessionFileForIndex caches results, so these calls return instantly inside mapConcurrent.
2272
2416
  for (let i = 0; i < input.tasks.length; i++) {
2273
- input.sessionFileForIndex(i);
2417
+ input.sessionFileForTask(input.tasks[i]!.agent, i, input.modelOverrides[i]);
2274
2418
  }
2275
2419
  return mapConcurrent(input.tasks, input.concurrencyLimit, async (task, index) => {
2276
2420
  const behavior = input.behaviors[index];
@@ -2283,9 +2427,11 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
2283
2427
  ? buildChainInstructions({ ...behavior, output: false, reads: false }, input.progressDir, index === input.firstProgressIndex)
2284
2428
  : { prefix: "", suffix: "" };
2285
2429
  const outputPath = resolveSingleOutputPath(behavior?.output, input.ctx.cwd, taskCwd, input.outputBaseDir);
2430
+ const agentConfig = input.agents.find((agent) => agent.name === task.agent);
2286
2431
  const taskText = injectSingleOutputInstruction(
2287
2432
  `${readInstructions.prefix}${input.taskTexts[index]!}${progressInstructions.suffix}`,
2288
2433
  outputPath,
2434
+ agentConfig,
2289
2435
  );
2290
2436
  const interruptController = new AbortController();
2291
2437
  if (input.foregroundControl) {
@@ -2301,7 +2447,6 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
2301
2447
  return true;
2302
2448
  };
2303
2449
  }
2304
- const agentConfig = input.agents.find((agent) => agent.name === task.agent);
2305
2450
  return runSync(input.ctx.cwd, input.agents, task.agent, taskText, {
2306
2451
  parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
2307
2452
  cwd: taskCwd,
@@ -2312,7 +2457,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
2312
2457
  runId: input.runId,
2313
2458
  index,
2314
2459
  sessionDir: input.sessionDirForIndex(index),
2315
- sessionFile: input.sessionFileForTask(task.agent, index),
2460
+ sessionFile: input.sessionFileForTask(task.agent, index, input.modelOverrides[index]),
2316
2461
  share: input.shareEnabled,
2317
2462
  artifactsDir: input.artifactConfig.enabled ? input.artifactsDir : undefined,
2318
2463
  artifactConfig: input.artifactConfig,
@@ -2320,14 +2465,15 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
2320
2465
  outputPath,
2321
2466
  outputMode: behavior?.outputMode,
2322
2467
  maxSubagentDepth: input.maxSubagentDepths[index],
2468
+ waitToolEnabled: input.waitToolEnabled,
2323
2469
  controlConfig: input.controlConfig,
2324
2470
  onControlEvent: input.onControlEvent,
2325
- onDetachedExit: (result) => updateRememberedForegroundChild(input.state, { runId: input.runId, mode: "parallel", cwd: taskCwd, index, result }),
2471
+ onDetachedExit: (result) => updateRememberedForegroundChild(input.state, { runId: input.runId, mode: "parallel", cwd: taskCwd, sessionId: input.parentSessionId, index, result, events: input.intercomEvents }),
2326
2472
  intercomSessionName: input.childIntercomTarget?.(task.agent, index),
2327
2473
  orchestratorIntercomTarget: input.orchestratorIntercomTarget,
2328
2474
  nestedRoute: input.foregroundControl?.nestedRoute,
2329
2475
  modelOverride: input.modelOverrides[index],
2330
- thinkingOverride: input.thinkingOverrideForTask(task.agent, index),
2476
+ thinkingOverride: input.thinkingOverrideForTask(task.agent, index, input.modelOverrides[index]),
2331
2477
  availableModels: input.availableModels,
2332
2478
  preferredModelProvider: input.ctx.model?.provider,
2333
2479
  modelScope: input.modelScope,
@@ -2453,15 +2599,15 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2453
2599
  normalizeSkillInput(t.skill),
2454
2600
  );
2455
2601
  const behaviorOverrides: StepOverrides[] = tasks.map((task, index) => ({
2456
- ...(task.output !== undefined ? { output: task.output === true ? agentConfigs[index]?.output ?? false : task.output } : {}),
2602
+ ...(task.output !== undefined && task.output !== true ? { output: task.output } : {}),
2457
2603
  ...(task.outputMode !== undefined ? { outputMode: task.outputMode } : {}),
2458
2604
  ...(task.reads !== undefined && task.reads !== true ? { reads: task.reads } : {}),
2459
2605
  ...(task.progress !== undefined ? { progress: task.progress } : {}),
2460
2606
  ...(skillOverrides[index] !== undefined ? { skills: skillOverrides[index] } : {}),
2461
- ...(task.model ? { model: task.model } : {}),
2607
+ ...(task.model !== undefined ? { model: task.model } : {}),
2462
2608
  }));
2463
2609
  const modelOverrides: (string | undefined)[] = tasks.map((_, i) =>
2464
- resolveSubagentModelOverride(behaviorOverrides[i]?.model ?? agentConfigs[i]?.model, ctx.model, availableModels, currentProvider, { scope: data.modelScope, source: behaviorOverrides[i]?.model ? "explicit" : "inherited" }),
2610
+ resolveEffectiveSubagentModel(behaviorOverrides[i]?.model, agentConfigs[i]?.model, ctx.model, availableModels, currentProvider, { scope: data.modelScope }),
2465
2611
  );
2466
2612
 
2467
2613
  if (params.clarify === true && ctx.hasUI) {
@@ -2495,8 +2641,8 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2495
2641
  taskTexts = result.templates;
2496
2642
  for (let i = 0; i < result.behaviorOverrides.length; i++) {
2497
2643
  const override = result.behaviorOverrides[i];
2498
- if (override?.model) {
2499
- modelOverrides[i] = resolveSubagentModelOverride(override.model, ctx.model, availableModels, currentProvider, { scope: data.modelScope, source: "explicit" });
2644
+ if (override?.model !== undefined) {
2645
+ modelOverrides[i] = resolveEffectiveSubagentModel(override.model, agentConfigs[i]?.model, ctx.model, availableModels, currentProvider, { scope: data.modelScope });
2500
2646
  behaviorOverrides[i]!.model = override.model;
2501
2647
  }
2502
2648
  if (override?.output !== undefined) behaviorOverrides[i]!.output = override.output;
@@ -2525,6 +2671,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2525
2671
  currentModelProvider: ctx.model?.provider,
2526
2672
  currentModel: ctx.model,
2527
2673
  modelScope: data.modelScope,
2674
+ interactive: ctx.hasUI,
2528
2675
  };
2529
2676
  const parallelTasks = tasks.map((t, i) => {
2530
2677
  const taskText = shouldForkAgent(contextPolicy, t.agent) ? wrapForkTask(taskTexts[i]!) : taskTexts[i]!;
@@ -2533,7 +2680,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2533
2680
  agent: t.agent,
2534
2681
  task: taskText,
2535
2682
  cwd: t.cwd,
2536
- ...(modelOverrides[i] ? { model: modelOverrides[i] } : {}),
2683
+ ...(behaviorOverrides[i]?.model !== undefined ? { model: behaviorOverrides[i]!.model } : {}),
2537
2684
  ...(skillOverrides[i] !== undefined ? { skill: skillOverrides[i] } : {}),
2538
2685
  ...(behaviorOverrides[i]?.output !== undefined ? { output: behaviorOverrides[i]!.output } : {}),
2539
2686
  ...(behaviorOverrides[i]?.outputMode !== undefined ? { outputMode: behaviorOverrides[i]!.outputMode } : {}),
@@ -2546,6 +2693,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2546
2693
  return executeAsyncChain(id, {
2547
2694
  chain: [{ parallel: parallelTasks, concurrency: parallelConcurrency, worktree: params.worktree }],
2548
2695
  resultMode: "parallel",
2696
+ goal: taskTexts[0] ?? "",
2549
2697
  agents,
2550
2698
  ctx: asyncCtx,
2551
2699
  availableModels,
@@ -2556,9 +2704,10 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2556
2704
  shareEnabled,
2557
2705
  sessionRoot,
2558
2706
  chainSkills: [],
2559
- sessionFilesByFlatIndex: tasks.map((task, index) => sessionFileForTask(task.agent, index)),
2560
- thinkingOverridesByFlatIndex: tasks.map((task, index) => thinkingOverrideForTask(task.agent, index)),
2707
+ sessionFilesByFlatIndex: tasks.map((task, index) => sessionFileForTask(task.agent, index, modelOverrides[index])),
2708
+ thinkingOverridesByFlatIndex: tasks.map((task, index) => thinkingOverrideForTask(task.agent, index, modelOverrides[index])),
2561
2709
  maxSubagentDepth: currentMaxSubagentDepth,
2710
+ waitToolEnabled: deps.waitToolEnabled,
2562
2711
  worktreeSetupHook: deps.config.worktreeSetupHook,
2563
2712
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
2564
2713
  worktreeBaseDir: deps.config.worktreeBaseDir,
@@ -2622,6 +2771,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2622
2771
  ctx,
2623
2772
  state: deps.state,
2624
2773
  intercomEvents: deps.pi.events,
2774
+ parentSessionId: data.parentSessionId,
2625
2775
  signal,
2626
2776
  runId,
2627
2777
  sessionDirForIndex,
@@ -2648,6 +2798,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2648
2798
  concurrencyLimit: parallelConcurrency,
2649
2799
  globalSemaphore: new Semaphore(deps.config.globalConcurrencyLimit ?? DEFAULT_GLOBAL_CONCURRENCY_LIMIT),
2650
2800
  maxSubagentDepths,
2801
+ waitToolEnabled: deps.waitToolEnabled,
2651
2802
  liveResults,
2652
2803
  liveProgress,
2653
2804
  onUpdate,
@@ -2681,7 +2832,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2681
2832
  totalChildUsage: sumResultsUsage(results),
2682
2833
  totalCost: sumResultsCost(results),
2683
2834
  });
2684
- rememberForegroundRun(deps.state, { runId, mode: "parallel", cwd: effectiveCwd, results: details.results });
2835
+ rememberForegroundRun(deps.state, { runId, mode: "parallel", cwd: effectiveCwd, sessionId: data.parentSessionId, results: details.results });
2685
2836
  if (interrupted) {
2686
2837
  return {
2687
2838
  content: [{ type: "text", text: `Parallel run paused after interrupt (${interrupted.agent}). Waiting for explicit next action.` }],
@@ -2692,7 +2843,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2692
2843
  const detached = detachedIndex >= 0 ? results[detachedIndex] : undefined;
2693
2844
  if (detached) {
2694
2845
  return {
2695
- content: [{ type: "text", text: `Parallel run detached for intercom coordination (${detached.agent}). Reply to the supervisor request first. After the child exits, start a fresh follow-up if needed.` }],
2846
+ content: [{ type: "text", text: `Parallel run detached for intercom coordination (${detached.agent}). Reply to the supervisor request first, then wait with subagent_wait({ id: "${runId}" }). Use subagent({ action: "status", id: "${runId}" }) to recover the result; do not resume or launch a replacement while it remains detached.` }],
2696
2847
  details,
2697
2848
  };
2698
2849
  }
@@ -2778,12 +2929,13 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2778
2929
  const currentProvider = ctx.model?.provider;
2779
2930
  const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo);
2780
2931
  let task = params.task ?? "";
2781
- let modelOverride: string | undefined = resolveSubagentModelOverride(
2782
- (params.model as string | undefined) ?? agentConfig.model,
2932
+ let modelOverride: string | undefined = resolveEffectiveSubagentModel(
2933
+ params.model as string | undefined,
2934
+ agentConfig.model,
2783
2935
  ctx.model,
2784
2936
  availableModels,
2785
2937
  currentProvider,
2786
- { scope: data.modelScope, source: (params.model as string | undefined) ? "explicit" : "inherited" },
2938
+ { scope: data.modelScope },
2787
2939
  );
2788
2940
  let skillOverride: string[] | false | undefined = normalizeSkillInput(params.skill);
2789
2941
  const rawOutput = params.output !== undefined ? params.output : agentConfig.output;
@@ -2820,7 +2972,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2820
2972
 
2821
2973
  task = result.templates[0]!;
2822
2974
  const override = result.behaviorOverrides[0];
2823
- if (override?.model) modelOverride = resolveSubagentModelOverride(override.model, ctx.model, availableModels, currentProvider, { scope: data.modelScope, source: "explicit" });
2975
+ if (override?.model !== undefined) modelOverride = resolveEffectiveSubagentModel(override.model, agentConfig.model, ctx.model, availableModels, currentProvider, { scope: data.modelScope });
2824
2976
  if (override?.output !== undefined) effectiveOutput = normalizeSingleOutputOverride(override.output, agentConfig.output);
2825
2977
  if (override?.skills !== undefined) skillOverride = override.skills;
2826
2978
 
@@ -2841,10 +2993,12 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2841
2993
  currentModelProvider: ctx.model?.provider,
2842
2994
  currentModel: ctx.model,
2843
2995
  modelScope: data.modelScope,
2996
+ interactive: ctx.hasUI,
2844
2997
  };
2845
2998
  return executeAsyncSingle(id, {
2846
2999
  agent: params.agent!,
2847
3000
  task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(task) : task,
3001
+ goal: task,
2848
3002
  agentConfig,
2849
3003
  ctx: asyncCtx,
2850
3004
  availableModels,
@@ -2854,14 +3008,15 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2854
3008
  artifactConfig,
2855
3009
  shareEnabled,
2856
3010
  sessionRoot,
2857
- sessionFile: sessionFileForTask(params.agent!, 0),
3011
+ sessionFile: sessionFileForTask(params.agent!, 0, modelOverride),
2858
3012
  skills: skillOverride === false ? [] : skillOverride,
2859
3013
  output: effectiveOutput,
2860
3014
  outputMode: effectiveOutputMode,
2861
3015
  outputBaseDir: resolveSingleRunOutputBaseDir(deps, artifactsDir, id),
2862
3016
  modelOverride,
2863
- thinkingOverride: thinkingOverrideForTask(params.agent!, 0),
3017
+ thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride),
2864
3018
  maxSubagentDepth,
3019
+ waitToolEnabled: deps.waitToolEnabled,
2865
3020
  worktreeSetupHook: deps.config.worktreeSetupHook,
2866
3021
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
2867
3022
  worktreeBaseDir: deps.config.worktreeBaseDir,
@@ -2884,7 +3039,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2884
3039
  if (validationError) {
2885
3040
  return { content: [{ type: "text", text: validationError }], isError: true, details: { mode: "single", results: [] } };
2886
3041
  }
2887
- task = injectSingleOutputInstruction(task, outputPath);
3042
+ task = injectSingleOutputInstruction(task, outputPath, agentConfig);
2888
3043
 
2889
3044
  let effectiveSkills: string[] | undefined;
2890
3045
  if (skillOverride === false) {
@@ -2938,7 +3093,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2938
3093
  intercomEvents: deps.pi.events,
2939
3094
  runId,
2940
3095
  sessionDir: sessionDirForIndex(0),
2941
- sessionFile: sessionFileForTask(params.agent!, 0),
3096
+ sessionFile: sessionFileForTask(params.agent!, 0, modelOverride),
2942
3097
  share: shareEnabled,
2943
3098
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
2944
3099
  artifactConfig,
@@ -2946,6 +3101,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2946
3101
  outputPath,
2947
3102
  outputMode: effectiveOutputMode,
2948
3103
  maxSubagentDepth,
3104
+ waitToolEnabled: deps.waitToolEnabled,
2949
3105
  onUpdate: forwardSingleUpdate,
2950
3106
  controlConfig,
2951
3107
  onControlEvent,
@@ -2954,14 +3110,14 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2954
3110
  nestedRoute: foregroundControl?.nestedRoute,
2955
3111
  index: 0,
2956
3112
  modelOverride,
2957
- thinkingOverride: thinkingOverrideForTask(params.agent!, 0),
3113
+ thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride),
2958
3114
  availableModels,
2959
3115
  preferredModelProvider: currentProvider,
2960
3116
  modelScope: data.modelScope,
2961
3117
  skills: effectiveSkills,
2962
3118
  acceptance: params.acceptance,
2963
3119
  acceptanceContext: { mode: "single" },
2964
- onDetachedExit: (result) => updateRememberedForegroundChild(deps.state, { runId, mode: "single", cwd: effectiveCwd, index: 0, result }),
3120
+ onDetachedExit: (result) => updateRememberedForegroundChild(deps.state, { runId, mode: "single", cwd: effectiveCwd, sessionId: data.parentSessionId, index: 0, result, events: deps.pi.events }),
2965
3121
  timeoutMs: data.timeoutMs,
2966
3122
  deadlineAt,
2967
3123
  turnBudget: data.turnBudget,
@@ -3011,7 +3167,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3011
3167
  totalChildUsage: sumResultsUsage([r]),
3012
3168
  totalCost: sumResultsCost([r]),
3013
3169
  });
3014
- rememberForegroundRun(deps.state, { runId, mode: "single", cwd: effectiveCwd, results: details.results });
3170
+ rememberForegroundRun(deps.state, { runId, mode: "single", cwd: effectiveCwd, sessionId: data.parentSessionId, results: details.results });
3015
3171
 
3016
3172
  if (!r.detached && !r.interrupted) {
3017
3173
  if (foregroundControl) updateForegroundNestedProjection(foregroundControl);
@@ -3034,7 +3190,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3034
3190
 
3035
3191
  if (r.detached) {
3036
3192
  return {
3037
- content: [{ type: "text", text: `Detached for intercom coordination: ${params.agent}. Reply to the supervisor request first. After the child exits, start a fresh follow-up if needed.` }],
3193
+ content: [{ type: "text", text: `Detached for intercom coordination: ${params.agent}. Reply to the supervisor request first, then wait with subagent_wait({ id: "${runId}" }). Use subagent({ action: "status", id: "${runId}" }) to recover the result; do not resume or launch a replacement while it remains detached.` }],
3038
3194
  details,
3039
3195
  };
3040
3196
  }
@@ -3115,6 +3271,80 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3115
3271
  const paramsWithResolvedCwd = requestParams.cwd === undefined ? requestParams : { ...requestParams, cwd: requestCwd };
3116
3272
  const action = paramsWithResolvedCwd.action;
3117
3273
  if (action) {
3274
+ if ((WATCHDOG_TOOL_ACTIONS as readonly string[]).includes(action)) {
3275
+ if (deps.allowMutatingManagementActions === false && MUTATING_MANAGEMENT_ACTIONS.has(action)) {
3276
+ return {
3277
+ content: [{ type: "text", text: `Action '${action}' is not available from child-safe subagent fanout mode.` }],
3278
+ isError: true,
3279
+ details: { mode: "management" as const, results: [] },
3280
+ };
3281
+ }
3282
+ return handleWatchdogToolAction(action, paramsWithResolvedCwd, ctx, deps.watchdog);
3283
+ }
3284
+ if (action === "grant-spawn-budget") {
3285
+ if (deps.allowMutatingManagementActions === false || !ctx.hasUI) {
3286
+ return {
3287
+ content: [{ type: "text", text: "Action 'grant-spawn-budget' is available only from the root interactive parent session." }],
3288
+ isError: true,
3289
+ details: { mode: "management", results: [] },
3290
+ };
3291
+ }
3292
+ deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
3293
+ if (!deps.state.currentSessionId) {
3294
+ return {
3295
+ content: [{ type: "text", text: "Action 'grant-spawn-budget' requires an active parent session id." }],
3296
+ isError: true,
3297
+ details: { mode: "management", results: [] },
3298
+ };
3299
+ }
3300
+ if (hasActiveSubagentChildren(deps.state)) {
3301
+ const spawnBudget = getSpawnBudgetSnapshot(deps.state, deps.config, deps.state.currentSessionId);
3302
+ return {
3303
+ content: [{ type: "text", text: "Spawn budget grants are rejected while current-session children are queued or running. Wait for them to settle, then retry the explicit grant." }],
3304
+ isError: true,
3305
+ details: { mode: "management", results: [], spawnBudget },
3306
+ };
3307
+ }
3308
+ const sessionId = deps.state.currentSessionId;
3309
+ const additional = paramsWithResolvedCwd.additional ?? Number.NaN;
3310
+ const preview = preflightSpawnBudgetGrant(deps.state, deps.config, sessionId, additional);
3311
+ if (preview.error) {
3312
+ return {
3313
+ content: [{ type: "text", text: preview.error }],
3314
+ isError: true,
3315
+ details: { mode: "management", results: [], spawnBudget: preview.snapshot },
3316
+ };
3317
+ }
3318
+ const confirmed = await ctx.ui.confirm(
3319
+ "Grant subagent spawn budget?",
3320
+ `Add ${additional} launches to this logical session?\n\n${formatSpawnBudget(preview.snapshot)}\n\nUsage is not reset. Compaction keeps the same budget; a new parent session starts a fresh one.`,
3321
+ );
3322
+ if (!confirmed) {
3323
+ return {
3324
+ content: [{ type: "text", text: "Spawn budget grant canceled; no capacity was added." }],
3325
+ details: { mode: "management", results: [], spawnBudget: preview.snapshot },
3326
+ };
3327
+ }
3328
+ const currentBudget = getSpawnBudgetSnapshot(deps.state, deps.config, deps.state.currentSessionId);
3329
+ if (
3330
+ resolveCurrentSessionId(ctx.sessionManager) !== sessionId
3331
+ || hasActiveSubagentChildren(deps.state)
3332
+ || currentBudget.used !== preview.snapshot.used
3333
+ || currentBudget.granted !== preview.snapshot.granted
3334
+ ) {
3335
+ return {
3336
+ content: [{ type: "text", text: "Spawn budget grant was not applied because the session, budget, or active-child state changed while confirmation was open." }],
3337
+ isError: true,
3338
+ details: { mode: "management", results: [], spawnBudget: currentBudget },
3339
+ };
3340
+ }
3341
+ const granted = grantSpawnBudget(deps.state, deps.config, sessionId, additional);
3342
+ return {
3343
+ content: [{ type: "text", text: granted.error ?? `Spawn budget grant applied: +${additional}. ${formatSpawnBudget(granted.snapshot)}` }],
3344
+ ...(granted.error ? { isError: true } : {}),
3345
+ details: { mode: "management", results: [], spawnBudget: granted.snapshot },
3346
+ };
3347
+ }
3118
3348
  if (action === "doctor") {
3119
3349
  let currentSessionFile: string | null = null;
3120
3350
  let currentSessionId = deps.state.currentSessionId;
@@ -3131,6 +3361,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3131
3361
  } catch (error) {
3132
3362
  if (!sessionError) sessionError = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
3133
3363
  }
3364
+ const spawnBudget = getSpawnBudgetSnapshot(deps.state, deps.config, currentSessionId);
3134
3365
  return {
3135
3366
  content: [{
3136
3367
  type: "text",
@@ -3147,15 +3378,22 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3147
3378
  expandTilde: deps.expandTilde,
3148
3379
  }),
3149
3380
  }],
3150
- details: { mode: "management", results: [] },
3381
+ details: { mode: "management", results: [], spawnBudget },
3151
3382
  };
3152
3383
  }
3153
3384
  if (action === "status") {
3385
+ deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
3386
+ const withBudget = (result: AgentToolResult<Details>) => withSpawnBudgetStatus(
3387
+ result,
3388
+ deps.state,
3389
+ deps.config,
3390
+ deps.state.currentSessionId,
3391
+ );
3154
3392
  const targetRunId = paramsWithResolvedCwd.id ?? paramsWithResolvedCwd.runId;
3155
3393
  const nestedScope = nestedResolutionScopeForExecutor(deps);
3156
3394
  const sessionRoots = trustedSessionRootsForStatus(ctx, deps);
3157
3395
  if (paramsWithResolvedCwd.view === "fleet") {
3158
- return inspectSubagentStatus(paramsWithResolvedCwd, { state: deps.state, nested: nestedScope, sessionRoots });
3396
+ return withBudget(inspectSubagentStatus(paramsWithResolvedCwd, { state: deps.state, nested: nestedScope, sessionRoots }));
3159
3397
  }
3160
3398
  if (targetRunId) {
3161
3399
  try {
@@ -3164,34 +3402,35 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3164
3402
  const foreground = getForegroundControl(deps.state, resolved.id);
3165
3403
  if (foreground) {
3166
3404
  if (paramsWithResolvedCwd.view === "transcript") {
3167
- return {
3405
+ return withBudget({
3168
3406
  content: [{ type: "text", text: "Live foreground transcript is already visible in the expanded running subagent result. Persisted session transcript becomes inspectable after the foreground run completes when sessions are enabled." }],
3169
3407
  details: { mode: "management", results: [] },
3170
- };
3408
+ });
3171
3409
  }
3172
- return foregroundStatusResult(foreground);
3410
+ return withBudget(foregroundStatusResult(foreground));
3173
3411
  }
3174
3412
  }
3175
3413
  } catch (error) {
3176
3414
  const message = error instanceof Error ? error.message : String(error);
3177
- return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
3415
+ return withBudget({ content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } });
3178
3416
  }
3179
3417
  } else {
3180
3418
  const foreground = getForegroundControl(deps.state, undefined);
3181
- if (foreground && paramsWithResolvedCwd.view !== "transcript") return foregroundStatusResult(foreground);
3419
+ if (foreground && paramsWithResolvedCwd.view !== "transcript") return withBudget(foregroundStatusResult(foreground));
3182
3420
  if (foreground && paramsWithResolvedCwd.view === "transcript") {
3183
- return {
3421
+ return withBudget({
3184
3422
  content: [{ type: "text", text: "Live foreground transcript is already visible in the expanded running subagent result. Pass an async run id to inspect a background transcript." }],
3185
3423
  details: { mode: "management", results: [] },
3186
- };
3424
+ });
3187
3425
  }
3188
3426
  }
3189
- return inspectSubagentStatus(paramsWithResolvedCwd, { state: deps.state, nested: nestedScope, sessionRoots });
3427
+ return withBudget(inspectSubagentStatus(paramsWithResolvedCwd, { state: deps.state, nested: nestedScope, sessionRoots }));
3190
3428
  }
3191
3429
  if (action === "resume") {
3192
3430
  return resumeAsyncRun({ params: paramsWithResolvedCwd, requestCwd, ctx, deps });
3193
3431
  }
3194
3432
  if (action === "steer") {
3433
+ deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
3195
3434
  const message = (paramsWithResolvedCwd.message ?? paramsWithResolvedCwd.task ?? "").trim();
3196
3435
  if (!message) return { content: [{ type: "text", text: "action='steer' requires message." }], isError: true, details: { mode: "management", results: [] } };
3197
3436
  const targetRunId = paramsWithResolvedCwd.runId ?? paramsWithResolvedCwd.id;
@@ -3199,7 +3438,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3199
3438
  try {
3200
3439
  const location = resolveAsyncRunLocation(paramsWithResolvedCwd, ASYNC_DIR, RESULTS_DIR);
3201
3440
  const runId = location.resolvedId ?? targetRunId ?? path.basename(location.asyncDir ?? paramsWithResolvedCwd.dir);
3202
- return steerAsyncRun({ state: deps.state, runId, message, index: paramsWithResolvedCwd.index, kill: deps.kill, location });
3441
+ return steerAsyncRun({ state: deps.state, runId, message, index: paramsWithResolvedCwd.index, kill: deps.kill, location, signal, recover: ({ absoluteDeadlineAt, ...limits }) => resumeAsyncRun({ params: { ...limits, action: "resume", id: runId, message }, requestCwd, ctx, deps, absoluteDeadlineAt }) });
3203
3442
  } catch (error) {
3204
3443
  const text = error instanceof Error ? error.message : String(error);
3205
3444
  return { content: [{ type: "text", text }], isError: true, details: { mode: "management", results: [] } };
@@ -3213,10 +3452,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3213
3452
  const text = error instanceof Error ? error.message : String(error);
3214
3453
  return { content: [{ type: "text", text }], isError: true, details: { mode: "management", results: [] } };
3215
3454
  }
3216
- if (resolved?.kind === "nested") return steerNestedRun({ target: resolved, message, index: paramsWithResolvedCwd.index });
3455
+ if (resolved?.kind === "nested") return steerNestedRun({ target: resolved, message, index: paramsWithResolvedCwd.index, signal });
3217
3456
  if (resolved?.kind === "foreground") return { content: [{ type: "text", text: "action='steer' currently supports live async Pi child sessions only; use action='interrupt' or action='resume' for foreground runs." }], isError: true, details: { mode: "management", results: [] } };
3218
3457
  if (resolved?.kind !== "async") return { content: [{ type: "text", text: `No async run found for '${targetRunId}'.` }], isError: true, details: { mode: "management", results: [] } };
3219
- return steerAsyncRun({ state: deps.state, runId: resolved.id, message, index: paramsWithResolvedCwd.index, kill: deps.kill, location: resolved.location });
3458
+ return steerAsyncRun({ state: deps.state, runId: resolved.id, message, index: paramsWithResolvedCwd.index, kill: deps.kill, location: resolved.location, signal, recover: ({ absoluteDeadlineAt, ...limits }) => resumeAsyncRun({ params: { ...limits, action: "resume", id: resolved!.id, message }, requestCwd, ctx, deps, absoluteDeadlineAt }) });
3220
3459
  }
3221
3460
  if (action === "append-step") {
3222
3461
  return appendStepToAsyncChain({ params: paramsWithResolvedCwd, requestCwd, ctx, deps });
@@ -3231,6 +3470,40 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3231
3470
  }
3232
3471
  return deps.handleScheduledRunAction(paramsWithResolvedCwd, ctx);
3233
3472
  }
3473
+ if (action === "stop") {
3474
+ const targetRunId = paramsWithResolvedCwd.runId ?? paramsWithResolvedCwd.id;
3475
+ let resolved: ResolvedSubagentRunId | undefined;
3476
+ if (paramsWithResolvedCwd.dir) {
3477
+ try {
3478
+ const location = resolveAsyncRunLocation(paramsWithResolvedCwd, ASYNC_DIR, RESULTS_DIR);
3479
+ return stopAsyncRun(deps.state, location.resolvedId ?? targetRunId ?? path.basename(location.asyncDir ?? paramsWithResolvedCwd.dir), deps.kill, location);
3480
+ } catch (error) {
3481
+ const text = error instanceof Error ? error.message : String(error);
3482
+ return { content: [{ type: "text", text }], isError: true, details: { mode: "management", results: [] } };
3483
+ }
3484
+ }
3485
+ if (!targetRunId) return { content: [{ type: "text", text: "action='stop' requires id or dir." }], isError: true, details: { mode: "management", results: [] } };
3486
+ try {
3487
+ resolved = resolveSubagentRunId(targetRunId, { state: deps.state, nested: nestedResolutionScopeForExecutor(deps) });
3488
+ } catch (error) {
3489
+ const message = error instanceof Error ? error.message : String(error);
3490
+ return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
3491
+ }
3492
+ if (resolved?.kind === "nested") return { content: [{ type: "text", text: "action='stop' supports current-session top-level async runs only." }], isError: true, details: { mode: "management", results: [] } };
3493
+ if (resolved?.kind === "foreground") return { content: [{ type: "text", text: "action='stop' supports async runs only. Use action='interrupt' for foreground runs." }], isError: true, details: { mode: "management", results: [] } };
3494
+ const stopResult = stopAsyncRun(
3495
+ deps.state,
3496
+ resolved?.kind === "async" ? resolved.id : targetRunId,
3497
+ deps.kill,
3498
+ resolved?.kind === "async" ? resolved.location : undefined,
3499
+ );
3500
+ if (stopResult) return stopResult;
3501
+ return {
3502
+ content: [{ type: "text", text: "No stoppable async run found in this session." }],
3503
+ isError: true,
3504
+ details: { mode: "management", results: [] },
3505
+ };
3506
+ }
3234
3507
  if (action === "interrupt") {
3235
3508
  const targetRunId = paramsWithResolvedCwd.runId ?? paramsWithResolvedCwd.id;
3236
3509
  let resolved: ResolvedSubagentRunId | undefined;
@@ -3320,10 +3593,6 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3320
3593
  depth,
3321
3594
  deps.config.forceTopLevelAsync === true,
3322
3595
  );
3323
- const foregroundTimeout = resolveForegroundTimeout(effectiveParams);
3324
- if (foregroundTimeout.error) return buildRequestedModeError(effectiveParams, foregroundTimeout.error);
3325
- const turnBudget = resolveTurnBudget(effectiveParams, deps.config);
3326
- if (turnBudget.error) return buildRequestedModeError(effectiveParams, turnBudget.error);
3327
3596
  const runToolBudget = resolveToolBudget(effectiveParams.toolBudget, "toolBudget");
3328
3597
  if (runToolBudget.error) return buildRequestedModeError(effectiveParams, runToolBudget.error);
3329
3598
  const configToolBudget = resolveToolBudget(deps.config.toolBudget, "config.toolBudget");
@@ -3336,6 +3605,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3336
3605
  const discovered = deps.discoverAgents(effectiveCwd, scope);
3337
3606
  const discoveredAgents = discovered.agents;
3338
3607
  const modelScope = discovered.modelScope;
3608
+ effectiveParams = applySingleAgentLaunchDefaults(effectiveParams, discoveredAgents);
3609
+ const foregroundTimeout = resolveForegroundTimeout(effectiveParams);
3610
+ if (foregroundTimeout.error) return buildRequestedModeError(effectiveParams, foregroundTimeout.error);
3611
+ const turnBudget = resolveTurnBudgetConfig(effectiveParams.turnBudget ?? deps.config.turnBudget);
3612
+ if (turnBudget.error) return buildRequestedModeError(effectiveParams, turnBudget.error);
3339
3613
  const contextPolicy = resolveAgentDefaultContextPolicy(effectiveParams, discoveredAgents);
3340
3614
  effectiveParams = contextPolicy.params;
3341
3615
  const sessionName = resolveIntercomSessionTarget(deps.pi.getSessionName(), ctx.sessionManager.getSessionId());
@@ -3370,10 +3644,47 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3370
3644
  );
3371
3645
  if (validationError) return validationError;
3372
3646
 
3647
+ const foregroundMode: "single" | "parallel" | "chain" = hasChain ? "chain" : hasTasks ? "parallel" : "single";
3648
+ const requestedSpawns = countRequestedSubagentSpawns(effectiveParams, deps.config);
3649
+ const spawnPreflight = preflightSpawnBudget(
3650
+ deps.state,
3651
+ deps.config,
3652
+ deps.state.currentSessionId,
3653
+ requestedSpawns,
3654
+ );
3655
+ if (spawnPreflight.error) return spawnBudgetErrorResult(spawnPreflight.error, foregroundMode);
3656
+
3373
3657
  let forkSessionFileForIndex: (idx?: number) => string | undefined = () => undefined;
3374
3658
  let forkThinkingOverrideForIndex: (idx?: number) => AgentConfig["thinking"] | undefined = () => undefined;
3659
+ let prepareForkThinking = (_agentName: string, _index: number, _modelOverride?: string): void => {};
3660
+ const forkThinkingRequirements = new Map<number, boolean>();
3661
+ const forkThinkingDowngrades = new Map<number, string>();
3375
3662
  try {
3376
- const forkContextResolver = createForkContextResolver(ctx.sessionManager, contextPolicy.usesFork ? "fork" : undefined);
3663
+ const forkAvailableModels = contextPolicy.usesFork ? ctx.modelRegistry.getAvailable().map(toModelInfo) : [];
3664
+ prepareForkThinking = (agentName, index, modelOverride) => {
3665
+ const agentConfig = agents.find((agent) => agent.name === agentName);
3666
+ const primaryModel = resolveEffectiveSubagentModel(
3667
+ modelOverride,
3668
+ agentConfig?.model,
3669
+ ctx.model,
3670
+ forkAvailableModels,
3671
+ ctx.model?.provider,
3672
+ );
3673
+ const candidates = buildModelCandidates(
3674
+ primaryModel,
3675
+ agentConfig?.fallbackModels,
3676
+ forkAvailableModels,
3677
+ ctx.model?.provider,
3678
+ );
3679
+ forkThinkingRequirements.set(
3680
+ index,
3681
+ candidates.length === 0
3682
+ || candidates.some((candidate) => forkedChildRequiresThinkingOff(candidate, forkAvailableModels, ctx.model?.provider)),
3683
+ );
3684
+ };
3685
+ const forkContextResolver = createForkContextResolver(ctx.sessionManager, contextPolicy.usesFork ? "fork" : undefined, {
3686
+ forceThinkingOffForIndex: (index) => forkThinkingRequirements.get(index) ?? true,
3687
+ });
3377
3688
  forkSessionFileForIndex = forkContextResolver.sessionFileForIndex;
3378
3689
  forkThinkingOverrideForIndex = forkContextResolver.thinkingOverrideForIndex;
3379
3690
  } catch (error) {
@@ -3410,16 +3721,26 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3410
3721
  }
3411
3722
  const sessionDirForIndex = (idx?: number) =>
3412
3723
  path.join(sessionRoot, `run-${idx ?? 0}`);
3413
- const forkSessionFileForTask = (agentName: string, idx?: number) =>
3414
- shouldForkAgent(contextPolicy, agentName) ? forkSessionFileForIndex(idx) : undefined;
3415
- const forkThinkingOverrideForTask = (agentName: string, idx?: number) =>
3416
- shouldForkAgent(contextPolicy, agentName) ? forkThinkingOverrideForIndex(idx) : undefined;
3417
- const childSessionFileForTask = (agentName: string, idx?: number) =>
3418
- forkSessionFileForTask(agentName, idx) ?? path.join(sessionDirForIndex(idx), "session.jsonl");
3724
+ const forkSessionFileForTask: ForkSessionFileForTask = (agentName, idx = 0, modelOverride) => {
3725
+ if (!shouldForkAgent(contextPolicy, agentName)) return undefined;
3726
+ prepareForkThinking(agentName, idx, modelOverride);
3727
+ return forkSessionFileForIndex(idx);
3728
+ };
3729
+ const forkThinkingOverrideForTask: ForkThinkingOverrideForTask = (agentName, idx = 0, modelOverride) => {
3730
+ if (!shouldForkAgent(contextPolicy, agentName)) return undefined;
3731
+ prepareForkThinking(agentName, idx, modelOverride);
3732
+ const override = forkThinkingOverrideForIndex(idx);
3733
+ if (override === "off") forkThinkingDowngrades.set(idx, agentName);
3734
+ return override;
3735
+ };
3736
+ const childSessionFileForTask: ForkSessionFileForTask = (agentName, idx, modelOverride) =>
3737
+ forkSessionFileForTask(agentName, idx, modelOverride) ?? path.join(sessionDirForIndex(idx), "session.jsonl");
3419
3738
  const childSessionFileForIndex = (idx?: number) =>
3420
3739
  path.join(sessionDirForIndex(idx), "session.jsonl");
3421
3740
  try {
3422
- preflightForkSessionsForStaticTasks(effectiveParams, contextPolicy, forkSessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems);
3741
+ if (!(effectiveParams.clarify === true && ctx.hasUI)) {
3742
+ preflightForkSessionsForStaticTasks(effectiveParams, contextPolicy, forkSessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems);
3743
+ }
3423
3744
  } catch (error) {
3424
3745
  return toExecutionErrorResult(effectiveParams, error);
3425
3746
  }
@@ -3430,15 +3751,13 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3430
3751
  ? (r: AgentToolResult<Details>) => onUpdate(withForkContext(r, effectiveParams.context))
3431
3752
  : undefined;
3432
3753
 
3433
- const foregroundMode: "single" | "parallel" | "chain" = hasChain ? "chain" : hasTasks ? "parallel" : "single";
3434
- const spawnLimitError = reserveSubagentSpawns({
3435
- state: deps.state,
3436
- config: deps.config,
3437
- sessionId: deps.state.currentSessionId,
3438
- requested: countRequestedSubagentSpawns(effectiveParams, deps.config),
3439
- mode: foregroundMode,
3440
- });
3441
- if (spawnLimitError) return spawnLimitError;
3754
+ const reservation = reserveSpawnBudget(
3755
+ deps.state,
3756
+ deps.config,
3757
+ deps.state.currentSessionId,
3758
+ requestedSpawns,
3759
+ );
3760
+ if (reservation.error) return spawnBudgetErrorResult(reservation.error, foregroundMode);
3442
3761
 
3443
3762
  const execData: ExecutionContextData = {
3444
3763
  params: effectiveParams,
@@ -3467,6 +3786,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3467
3786
  configToolBudget: configToolBudget.toolBudget,
3468
3787
  contextPolicy,
3469
3788
  modelScope,
3789
+ parentSessionId: deps.state.currentSessionId,
3470
3790
  };
3471
3791
 
3472
3792
  const foregroundControl = effectiveAsync
@@ -3493,10 +3813,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3493
3813
  const details = result?.details;
3494
3814
  const state = type === "subagent.nested.started"
3495
3815
  ? "running"
3496
- : result?.isError || details?.results.some((child) => child.exitCode !== 0)
3497
- ? "failed"
3498
- : details?.results.some((child) => child.interrupted)
3499
- ? "paused"
3816
+ : details?.results.some((child) => child.interrupted || child.detached)
3817
+ ? "paused"
3818
+ : result?.isError || details?.results.some((child) => child.exitCode !== 0)
3819
+ ? "failed"
3500
3820
  : "complete";
3501
3821
  const errorText = result?.isError
3502
3822
  ? result.content.find((item) => item.type === "text")?.text
@@ -3536,7 +3856,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3536
3856
  ...(errorText ? { error: errorText } : {}),
3537
3857
  ...(details?.results.length ? { steps: details.results.map((child) => ({
3538
3858
  agent: child.agent,
3539
- status: child.interrupted ? "paused" : child.exitCode === 0 ? "complete" : "failed",
3859
+ status: child.interrupted || child.detached ? "paused" : child.exitCode === 0 ? "complete" : "failed",
3540
3860
  ...(child.sessionFile ? { sessionFile: child.sessionFile } : {}),
3541
3861
  ...(child.error ? { error: child.error } : {}),
3542
3862
  })) } : {}),
@@ -3550,7 +3870,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3550
3870
  let nestedForegroundStarted = false;
3551
3871
  try {
3552
3872
  const asyncResult = runAsyncPath(execData, deps);
3553
- if (asyncResult) return withForkContext(asyncResult, effectiveParams.context);
3873
+ if (asyncResult) return withForkContext(withForkThinkingNotes(asyncResult, forkThinkingDowngrades), effectiveParams.context);
3554
3874
  if (foregroundControl) {
3555
3875
  writeNestedForegroundEvent("subagent.nested.started");
3556
3876
  nestedForegroundStarted = true;
@@ -3558,20 +3878,20 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3558
3878
  if (hasChain && effectiveParams.chain) {
3559
3879
  const result = await runChainPath(execData, deps);
3560
3880
  writeNestedForegroundEvent("subagent.nested.completed", result);
3561
- return withForkContext(result, effectiveParams.context);
3881
+ return withForkContext(withForkThinkingNotes(result, forkThinkingDowngrades), effectiveParams.context);
3562
3882
  }
3563
3883
  if (hasTasks && effectiveParams.tasks) {
3564
3884
  const result = await runParallelPath(execData, deps);
3565
3885
  writeNestedForegroundEvent("subagent.nested.completed", result);
3566
- return withForkContext(result, effectiveParams.context);
3886
+ return withForkContext(withForkThinkingNotes(result, forkThinkingDowngrades), effectiveParams.context);
3567
3887
  }
3568
3888
  if (hasSingle) {
3569
3889
  const result = await runSinglePath(execData, deps);
3570
3890
  writeNestedForegroundEvent("subagent.nested.completed", result);
3571
- return withForkContext(result, effectiveParams.context);
3891
+ return withForkContext(withForkThinkingNotes(result, forkThinkingDowngrades), effectiveParams.context);
3572
3892
  }
3573
3893
  } catch (error) {
3574
- const errorResult = toExecutionErrorResult(effectiveParams, error);
3894
+ const errorResult = withForkThinkingNotes(toExecutionErrorResult(effectiveParams, error), forkThinkingDowngrades);
3575
3895
  if (nestedForegroundStarted) writeNestedForegroundEvent("subagent.nested.completed", errorResult);
3576
3896
  return errorResult;
3577
3897
  } finally {
@@ -3600,6 +3920,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3600
3920
  ): Promise<AgentToolResult<Details>> => {
3601
3921
  const requestParams = omitExecutionModeActionAlias(params);
3602
3922
  if (requestParams.action) return execute(id, requestParams, signal, onUpdate, ctx);
3923
+ const { depth } = checkSubagentDepth(deps.config.maxSubagentDepth);
3924
+ const dispatchParams = applyForceTopLevelAsyncOverride(requestParams, depth, deps.config.forceTopLevelAsync === true);
3925
+ const runsForeground = dispatchParams.clarify === true || (dispatchParams.async ?? deps.asyncByDefault) !== true;
3926
+ if (!runsForeground) return execute(id, requestParams, signal, onUpdate, ctx);
3603
3927
  if (deps.state.subagentInProgress === true) return duplicateSubagentCallResult(requestParams);
3604
3928
  deps.state.subagentInProgress = true;
3605
3929
  try {