pi-subagents 0.36.0 → 0.37.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +111 -16
  3. package/agents/delegate.md +2 -0
  4. package/agents/planner.md +2 -1
  5. package/agents/worker.md +2 -0
  6. package/package.json +6 -4
  7. package/skills/pi-subagents/SKILL.md +18 -973
  8. package/skills/pi-subagents/references/constraints-and-recipes.md +256 -0
  9. package/skills/pi-subagents/references/execution-controls.md +411 -0
  10. package/skills/pi-subagents/references/management-authoring-rpc.md +140 -0
  11. package/skills/pi-subagents/references/prompting-and-roles.md +268 -0
  12. package/src/agents/agent-management.ts +7 -1
  13. package/src/agents/agents.ts +99 -12
  14. package/src/api/capability-ceiling.ts +17 -0
  15. package/src/api/delegation.ts +6 -1
  16. package/src/api/preflight.ts +399 -0
  17. package/src/extension/index.ts +16 -7
  18. package/src/extension/rpc.ts +29 -2
  19. package/src/extension/schemas.ts +10 -4
  20. package/src/extension/tool-description.ts +4 -2
  21. package/src/intercom/intercom-bridge.ts +5 -2
  22. package/src/runs/background/async-execution.ts +125 -14
  23. package/src/runs/background/async-job-tracker.ts +19 -12
  24. package/src/runs/background/async-resume.ts +27 -9
  25. package/src/runs/background/async-status.ts +18 -0
  26. package/src/runs/background/notify.ts +3 -0
  27. package/src/runs/background/process-terminal.ts +280 -0
  28. package/src/runs/background/result-watcher.ts +9 -3
  29. package/src/runs/background/run-status.ts +7 -1
  30. package/src/runs/background/scheduled-runs.ts +6 -1
  31. package/src/runs/background/stale-run-reconciler.ts +6 -0
  32. package/src/runs/background/subagent-runner.ts +182 -12
  33. package/src/runs/foreground/chain-execution.ts +5 -0
  34. package/src/runs/foreground/execution.ts +59 -13
  35. package/src/runs/foreground/subagent-executor.ts +142 -47
  36. package/src/runs/shared/acceptance.ts +41 -30
  37. package/src/runs/shared/capability-ceiling.ts +177 -0
  38. package/src/runs/shared/dynamic-fanout.ts +1 -1
  39. package/src/runs/shared/mcp-direct-tool-allowlist.ts +56 -17
  40. package/src/runs/shared/model-fallback.ts +8 -0
  41. package/src/runs/shared/nested-events.ts +8 -1
  42. package/src/runs/shared/parallel-utils.ts +5 -0
  43. package/src/runs/shared/pi-args.ts +141 -58
  44. package/src/runs/shared/session-lease.ts +25 -5
  45. package/src/runs/shared/subagent-prompt-runtime.ts +14 -1
  46. package/src/runs/shared/task-intent.ts +1 -1
  47. package/src/runs/shared/tool-availability.ts +18 -2
  48. package/src/shared/launch-contract.ts +123 -0
  49. package/src/shared/types.ts +118 -8
  50. package/src/shared/utils.ts +17 -42
  51. package/src/slash/delegation-adapters.ts +6 -1
  52. package/src/slash/slash-commands.ts +1 -1
  53. package/src/tui/fleet-status.ts +62 -16
  54. package/src/tui/fleet.ts +1 -1
@@ -16,7 +16,7 @@ import { clearPendingForegroundControlNotices } from "../../extension/control-no
16
16
  import { runSync } from "./execution.ts";
17
17
  import { handleWatchdogToolAction, WATCHDOG_TOOL_ACTIONS } from "../../watchdog/tool-actions.ts";
18
18
  import type { MainWatchdogRuntime } from "../../watchdog/runtime.ts";
19
- import { buildModelCandidates, resolveEffectiveSubagentModel, resolveModelCandidate } from "../shared/model-fallback.ts";
19
+ import { buildModelCandidates, normalizeParentModel, resolveEffectiveSubagentModel, resolveModelCandidate, type ParentModel } from "../shared/model-fallback.ts";
20
20
  import type { ModelScopeConfig } from "../shared/model-scope.ts";
21
21
  import { aggregateParallelOutputs } from "../shared/parallel-utils.ts";
22
22
  import { recordRun } from "../shared/run-history.ts";
@@ -47,6 +47,7 @@ import { formatControlIntercomMessage, formatControlNoticeMessage, resolveContro
47
47
  import { resolveTurnBudgetConfig } from "../shared/turn-budget.ts";
48
48
  import { formatSpawnBudget, getSpawnBudgetSnapshot, grantSpawnBudget, preflightSpawnBudget, preflightSpawnBudgetGrant, reserveSpawnBudget } from "../shared/spawn-budget.ts";
49
49
  import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
50
+ import { intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
50
51
  import { isAgentContractV1 } from "../shared/agent-contract.ts";
51
52
  import { finalizeSingleOutput, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
52
53
  import { cleanupStructuredOutputRuntime, createStructuredOutputRuntime } from "../shared/structured-output.ts";
@@ -191,6 +192,15 @@ export interface SubagentParamsLike {
191
192
  additional?: number;
192
193
  }
193
194
 
195
+ function rememberParentModel(state: { currentSessionId?: string | null; lastParentModel?: ParentModel }, sessionId: string | null, model: unknown): ParentModel | undefined {
196
+ if (state.currentSessionId !== sessionId) state.lastParentModel = undefined;
197
+ state.currentSessionId = sessionId;
198
+ const parentModel = normalizeParentModel(model);
199
+ if (!sessionId) return parentModel;
200
+ if (parentModel) state.lastParentModel = parentModel;
201
+ return parentModel ?? state.lastParentModel;
202
+ }
203
+
194
204
  interface ExecutorDeps {
195
205
  pi: ExtensionAPI;
196
206
  state: SubagentState;
@@ -239,7 +249,9 @@ interface ExecutionContextData {
239
249
  configToolBudget?: ResolvedToolBudget;
240
250
  contextPolicy: AgentDefaultContextPolicy;
241
251
  modelScope?: ModelScopeConfig;
252
+ parentModel?: ParentModel;
242
253
  parentSessionId: string | null;
254
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
243
255
  }
244
256
 
245
257
  function resolveRequestedCwd(runtimeCwd: string, requestedCwd: string | undefined): string {
@@ -405,11 +417,16 @@ function rememberForegroundRun(state: SubagentState, input: { runId: string; mod
405
417
  ...(result.savedOutputPath ? { savedOutputPath: result.savedOutputPath } : {}),
406
418
  ...(result.outputSaveError ? { outputSaveError: result.outputSaveError } : {}),
407
419
  ...(result.sessionFile ? { sessionFile: result.sessionFile } : {}),
420
+ ...(result.model ? { model: result.model } : {}),
421
+ ...(result.thinking ? { thinking: result.thinking } : {}),
408
422
  ...(result.artifactPaths ? { artifactPaths: result.artifactPaths } : {}),
409
423
  ...(result.transcriptPath ? { transcriptPath: result.transcriptPath } : {}),
410
424
  ...(result.transcriptError ? { transcriptError: result.transcriptError } : {}),
411
425
  ...(result.detachedReason ? { detachedReason: result.detachedReason } : {}),
412
426
  ...(result.acceptance ? { acceptance: result.acceptance } : {}),
427
+ ...(result.launchContractDigest ? { launchContractDigest: result.launchContractDigest } : {}),
428
+ ...(result.capabilityCeiling ? { capabilityCeiling: result.capabilityCeiling } : {}),
429
+ ...(result.capabilityAudit ? { capabilityAudit: result.capabilityAudit } : {}),
413
430
  };
414
431
  const recovered = previous?.children[index];
415
432
  return child.status === "detached" && recovered && recovered.status !== "detached" ? recovered : child;
@@ -468,11 +485,16 @@ function updateRememberedForegroundChild(state: SubagentState, input: { runId: s
468
485
  savedOutputPath: input.result.savedOutputPath,
469
486
  outputSaveError: input.result.outputSaveError,
470
487
  ...(input.result.sessionFile ? { sessionFile: input.result.sessionFile } : {}),
488
+ ...(input.result.model ? { model: input.result.model } : {}),
489
+ ...(input.result.thinking ? { thinking: input.result.thinking } : {}),
471
490
  ...(input.result.artifactPaths ? { artifactPaths: input.result.artifactPaths } : {}),
472
491
  ...(input.result.transcriptPath ? { transcriptPath: input.result.transcriptPath } : {}),
473
492
  ...(input.result.transcriptError ? { transcriptError: input.result.transcriptError } : {}),
474
493
  ...(input.result.detachedReason ? { detachedReason: input.result.detachedReason } : {}),
475
494
  ...(input.result.acceptance ? { acceptance: input.result.acceptance } : {}),
495
+ ...(input.result.launchContractDigest ? { launchContractDigest: input.result.launchContractDigest } : {}),
496
+ ...(input.result.capabilityCeiling ? { capabilityCeiling: input.result.capabilityCeiling } : {}),
497
+ ...(input.result.capabilityAudit ? { capabilityAudit: input.result.capabilityAudit } : {}),
476
498
  };
477
499
  trimRememberedForegroundRuns(state);
478
500
  const output = getSingleResultOutput(input.result).trim();
@@ -501,7 +523,7 @@ function updateRememberedForegroundChild(state: SubagentState, input: { runId: s
501
523
  });
502
524
  }
503
525
 
504
- function resolveForegroundResumeTarget(params: SubagentParamsLike, state: SubagentState): { runId: string; mode: "single" | "parallel" | "chain"; state: "complete"; agent: string; index: number; cwd: string; sessionFile: string } | undefined {
526
+ function resolveForegroundResumeTarget(params: SubagentParamsLike, state: SubagentState): { runId: string; mode: "single" | "parallel" | "chain"; state: "complete"; agent: string; index: number; cwd: string; sessionFile: string; model?: string; thinking?: string; launchContractDigest?: string; capabilityCeiling?: ResolvedSubagentCapabilityCeiling } | undefined {
505
527
  const requested = (params.id ?? params.runId)?.trim();
506
528
  if (!requested || !state.foregroundRuns?.size || !state.currentSessionId) return undefined;
507
529
  const sessionRuns = [...state.foregroundRuns.values()].filter((run) => run.sessionId === state.currentSessionId);
@@ -520,7 +542,19 @@ function resolveForegroundResumeTarget(params: SubagentParamsLike, state: Subage
520
542
  if (path.extname(child.sessionFile) !== ".jsonl") throw new Error(`Foreground run '${run.runId}' child ${index} session file must be a .jsonl file: ${child.sessionFile}`);
521
543
  const sessionFile = path.resolve(child.sessionFile);
522
544
  if (!fs.existsSync(sessionFile)) throw new Error(`Foreground run '${run.runId}' child ${index} session file does not exist: ${child.sessionFile}`);
523
- return { runId: run.runId, mode: run.mode, state: "complete", agent: child.agent, index, cwd: run.cwd, sessionFile };
545
+ return {
546
+ runId: run.runId,
547
+ mode: run.mode,
548
+ state: "complete",
549
+ agent: child.agent,
550
+ index,
551
+ cwd: run.cwd,
552
+ sessionFile,
553
+ ...(child.model ? { model: child.model } : {}),
554
+ ...(child.thinking ? { thinking: child.thinking } : {}),
555
+ ...(child.launchContractDigest ? { launchContractDigest: child.launchContractDigest } : {}),
556
+ ...(child.capabilityCeiling ? { capabilityCeiling: child.capabilityCeiling } : {}),
557
+ };
524
558
  }
525
559
 
526
560
  type AsyncResumeSourceTarget = ReturnType<typeof resolveAsyncResumeTarget> & { source: "async" };
@@ -534,6 +568,7 @@ type NestedResumeSourceTarget = {
534
568
  index: number;
535
569
  cwd?: string;
536
570
  sessionFile: string;
571
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
537
572
  };
538
573
  type ResumeSourceTarget = AsyncResumeSourceTarget | ForegroundResumeSourceTarget | NestedResumeSourceTarget;
539
574
 
@@ -757,6 +792,7 @@ function appendStepToAsyncChain(input: {
757
792
  requestCwd: string;
758
793
  ctx: ExtensionContext;
759
794
  deps: ExecutorDeps;
795
+ parentModel?: ParentModel;
760
796
  }): AgentToolResult<Details> {
761
797
  const targetRunId = input.params.id ?? input.params.runId;
762
798
  if (!targetRunId) {
@@ -861,13 +897,14 @@ function appendStepToAsyncChain(input: {
861
897
  const contextPolicy = resolveExplicitContextPolicy(input.params);
862
898
  const chainSkillInput = normalizeSkillInput(input.params.skill);
863
899
  const chainSkills = chainSkillInput === false ? [] : (chainSkillInput ?? []);
900
+ const parentModel = input.parentModel;
864
901
  const asyncCtx = {
865
902
  pi: input.deps.pi,
866
903
  cwd: input.ctx.cwd,
867
904
  currentSessionId: resolveCurrentSessionId(input.ctx.sessionManager),
868
905
  parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
869
- currentModelProvider: input.ctx.model?.provider,
870
- currentModel: input.ctx.model,
906
+ currentModelProvider: parentModel?.provider,
907
+ currentModel: parentModel,
871
908
  modelScope: discoveredForAppend.modelScope,
872
909
  interactive: input.ctx.hasUI,
873
910
  };
@@ -886,6 +923,7 @@ function appendStepToAsyncChain(input: {
886
923
  contextForAgent: contextPolicy.contextForAgent,
887
924
  asyncDir: resolved.location.asyncDir,
888
925
  validateOutputBindings: false,
926
+ capabilityCeiling: intersectSubagentCapabilityCeilings(status.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(asyncCtx.currentSessionId)),
889
927
  });
890
928
  if ("error" in built) {
891
929
  return {
@@ -990,6 +1028,7 @@ function resolveNestedResumeTarget(match: ResolvedSubagentRunId & { kind: "neste
990
1028
  index: 0,
991
1029
  cwd: asyncDir ? path.dirname(asyncDir) : undefined,
992
1030
  sessionFile: validateNestedSessionFile(run, trustedSessionRoots),
1031
+ ...(run.capabilityCeiling ? { capabilityCeiling: run.capabilityCeiling } : {}),
993
1032
  };
994
1033
  }
995
1034
 
@@ -1103,6 +1142,7 @@ async function resumeAsyncRun(input: {
1103
1142
  requestCwd: string;
1104
1143
  ctx: ExtensionContext;
1105
1144
  deps: ExecutorDeps;
1145
+ parentModel?: ParentModel;
1106
1146
  absoluteDeadlineAt?: number;
1107
1147
  }): Promise<AgentToolResult<Details>> {
1108
1148
  const followUp = (input.params.message ?? input.params.task ?? "").trim();
@@ -1114,6 +1154,13 @@ async function resumeAsyncRun(input: {
1114
1154
  details: { mode: "management", results: [] },
1115
1155
  };
1116
1156
  }
1157
+ if (input.params.model !== undefined) {
1158
+ return {
1159
+ content: [{ type: "text", text: "action='resume' reuses the persisted child model and does not accept a model override." }],
1160
+ isError: true,
1161
+ details: { mode: "management", results: [] },
1162
+ };
1163
+ }
1117
1164
  const acceptanceErrors = validateExecutionAcceptance(input.params);
1118
1165
  if (acceptanceErrors.length > 0) {
1119
1166
  return {
@@ -1241,6 +1288,7 @@ async function resumeAsyncRun(input: {
1241
1288
  const goal = resolveAsyncEventGoal(workflowTask, attachChain);
1242
1289
  const chain = wrapChainTasksForFork(attachChain, contextPolicy);
1243
1290
  const normalized = normalizeSkillInput(input.params.skill);
1291
+ const parentModel = input.parentModel;
1244
1292
  const result = executeAsyncChain(runId, {
1245
1293
  chain,
1246
1294
  task: workflowTask,
@@ -1259,8 +1307,8 @@ async function resumeAsyncRun(input: {
1259
1307
  cwd: input.requestCwd,
1260
1308
  currentSessionId: input.deps.state.currentSessionId,
1261
1309
  parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
1262
- currentModelProvider: input.ctx.model?.provider,
1263
- currentModel: input.ctx.model,
1310
+ currentModelProvider: parentModel?.provider,
1311
+ currentModel: parentModel,
1264
1312
  modelScope,
1265
1313
  interactive: input.ctx.hasUI,
1266
1314
  },
@@ -1283,6 +1331,7 @@ async function resumeAsyncRun(input: {
1283
1331
  controlIntercomTarget: intercomBridge.active ? intercomBridge.orchestratorTarget : undefined,
1284
1332
  childIntercomTarget: intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
1285
1333
  globalConcurrencyLimit: input.deps.config.globalConcurrencyLimit,
1334
+ capabilityCeiling: intersectSubagentCapabilityCeilings("capabilityCeiling" in target ? target.capabilityCeiling : undefined, resolveCurrentSubagentCapabilityCeiling(input.deps.state.currentSessionId)),
1286
1335
  });
1287
1336
  if (result.isError) return result;
1288
1337
  const attachedId = result.details.asyncId ?? runId;
@@ -1301,6 +1350,7 @@ async function resumeAsyncRun(input: {
1301
1350
  const artifactConfig: ArtifactConfig = recoveryDescriptor?.artifactConfig ?? { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false, dir: input.deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir };
1302
1351
  const artifactsDir = recoveryDescriptor?.artifactsDir ?? getArtifactsDir(parentSessionFile, effectiveCwd, artifactConfig.dir);
1303
1352
  const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo);
1353
+ const parentModel = input.parentModel;
1304
1354
  const result = executeAsyncSingle(runId, {
1305
1355
  agent: target.agent,
1306
1356
  task: buildRevivedAsyncTask(target, followUp),
@@ -1311,8 +1361,8 @@ async function resumeAsyncRun(input: {
1311
1361
  cwd: input.requestCwd,
1312
1362
  currentSessionId: input.deps.state.currentSessionId,
1313
1363
  parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
1314
- currentModelProvider: input.ctx.model?.provider,
1315
- currentModel: input.ctx.model,
1364
+ currentModelProvider: parentModel?.provider,
1365
+ currentModel: parentModel,
1316
1366
  modelScope,
1317
1367
  interactive: input.ctx.hasUI,
1318
1368
  },
@@ -1330,8 +1380,8 @@ async function resumeAsyncRun(input: {
1330
1380
  sourceRunId: target.runId,
1331
1381
  ...(input.deps.state.currentSessionId ? { parentSessionId: input.deps.state.currentSessionId } : {}),
1332
1382
  },
1333
- modelOverride: input.params.model ?? recoveryDescriptor?.model ?? target.model,
1334
- thinkingOverride: input.params.model ? undefined : recoveryDescriptor?.thinking ?? target.thinking,
1383
+ modelOverride: recoveryDescriptor?.model ?? target.model,
1384
+ thinkingOverride: recoveryDescriptor?.thinking ?? target.thinking,
1335
1385
  outputBaseDir: resolveSingleRunOutputBaseDir(input.deps, artifactsDir, runId),
1336
1386
  maxSubagentDepth: recoveryDescriptor?.maxSubagentDepth ?? resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth),
1337
1387
  waitToolEnabled: input.deps.waitToolEnabled,
@@ -1342,8 +1392,8 @@ async function resumeAsyncRun(input: {
1342
1392
  controlIntercomTarget: intercomBridge.active ? intercomBridge.orchestratorTarget : undefined,
1343
1393
  childIntercomTarget: intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
1344
1394
  availableModels,
1345
- output: recoveryDescriptor?.outputPath,
1346
- outputMode: recoveryDescriptor?.outputMode,
1395
+ output: typeof input.params.output === "string" ? input.params.output : recoveryDescriptor?.outputPath,
1396
+ outputMode: input.params.outputMode ?? recoveryDescriptor?.outputMode,
1347
1397
  ...(recoveryDescriptor?.agentContract ? { agentContract: recoveryDescriptor.agentContract } : {}),
1348
1398
  ...(recoveryDescriptor?.structuredOutputSchema ? { structuredOutputSchema: recoveryDescriptor.structuredOutputSchema } : {}),
1349
1399
  ...(recoveryDescriptor?.skills ? { skills: [...recoveryDescriptor.skills] } : {}),
@@ -1352,6 +1402,7 @@ async function resumeAsyncRun(input: {
1352
1402
  ...(input.absoluteDeadlineAt !== undefined ? { absoluteDeadlineAt: input.absoluteDeadlineAt } : {}),
1353
1403
  ...(input.params.turnBudget !== undefined ? { turnBudget: input.params.turnBudget } : {}),
1354
1404
  ...(input.params.toolBudget !== undefined ? { toolBudget: input.params.toolBudget } : {}),
1405
+ capabilityCeiling: intersectSubagentCapabilityCeilings("capabilityCeiling" in target ? target.capabilityCeiling : undefined, recoveryDescriptor?.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(input.deps.state.currentSessionId)),
1355
1406
  });
1356
1407
  if (result.isError) return result;
1357
1408
 
@@ -1367,7 +1418,13 @@ async function resumeAsyncRun(input: {
1367
1418
  revivedTarget ? `Intercom target: ${revivedTarget} (if registered)` : undefined,
1368
1419
  `Status if needed: subagent({ action: "status", id: "${revivedId}" })`,
1369
1420
  ].filter((line): line is string => Boolean(line));
1370
- return { content: [{ type: "text", text: formatAsyncStartedMessage(lines.join("\n"), input.ctx.hasUI) }], details: result.details };
1421
+ return {
1422
+ content: [{ type: "text", text: formatAsyncStartedMessage(lines.join("\n"), input.ctx.hasUI) }],
1423
+ details: {
1424
+ ...result.details,
1425
+ ...(target.launchContractDigest ? { sourceLaunchContractDigest: target.launchContractDigest } : {}),
1426
+ },
1427
+ };
1371
1428
  }
1372
1429
 
1373
1430
  function resultSummaryForIntercom(result: SingleResult): string {
@@ -1413,7 +1470,7 @@ async function emitForegroundResultIntercom(input: {
1413
1470
  nestedChildren?: NestedRunSummary[];
1414
1471
  parallelHandoff?: Details["parallelHandoff"];
1415
1472
  }): Promise<ReturnType<typeof buildSubagentResultIntercomPayload> | null> {
1416
- if (!input.intercomBridge.active || !input.intercomBridge.orchestratorTarget) return null;
1473
+ if (!input.intercomBridge.active || !input.intercomBridge.resultDelivery || !input.intercomBridge.orchestratorTarget) return null;
1417
1474
  const children = input.results.flatMap((result, index) => result.detached ? [] : [{
1418
1475
  agent: result.agent,
1419
1476
  status: resolveSubagentResultStatus({
@@ -1679,10 +1736,14 @@ function applySingleAgentLaunchDefaults(params: SubagentParamsLike, agents: Agen
1679
1736
  };
1680
1737
  }
1681
1738
 
1682
- function resolveForegroundTimeout(params: SubagentParamsLike): { timeoutMs?: number; error?: string } {
1739
+ export const DEFAULT_FOREGROUND_TIMEOUT_MS = 30 * 60 * 1000;
1740
+
1741
+ function resolveForegroundTimeout(params: SubagentParamsLike, defaultTimeoutMs?: number): { timeoutMs?: number; error?: string } {
1683
1742
  const rawTimeout = params.timeoutMs;
1684
1743
  const rawMaxRuntime = params.maxRuntimeMs;
1685
- if (rawTimeout === undefined && rawMaxRuntime === undefined) return {};
1744
+ if (rawTimeout === undefined && rawMaxRuntime === undefined) {
1745
+ return defaultTimeoutMs === undefined ? {} : { timeoutMs: defaultTimeoutMs };
1746
+ }
1686
1747
  for (const [name, value] of [["timeoutMs", rawTimeout], ["maxRuntimeMs", rawMaxRuntime]] as const) {
1687
1748
  if (value === undefined) continue;
1688
1749
  if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
@@ -2024,19 +2085,20 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2024
2085
  };
2025
2086
  }
2026
2087
  const id = randomUUID();
2088
+ const parentModel = data.parentModel;
2027
2089
  const asyncCtx = {
2028
2090
  pi: deps.pi,
2029
2091
  cwd: ctx.cwd,
2030
2092
  currentSessionId: deps.state.currentSessionId!,
2031
2093
  parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
2032
- currentModelProvider: ctx.model?.provider,
2033
- currentModel: ctx.model,
2094
+ currentModelProvider: parentModel?.provider,
2095
+ currentModel: parentModel,
2034
2096
  modelScope: data.modelScope,
2035
2097
  interactive: ctx.hasUI,
2036
2098
  };
2037
2099
  const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo);
2038
2100
  const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
2039
- const currentProvider = ctx.model?.provider;
2101
+ const currentProvider = parentModel?.provider;
2040
2102
  const controlIntercomTarget = intercomBridge.active ? intercomBridge.orchestratorTarget : undefined;
2041
2103
  const childIntercomTarget = intercomBridge.active ? (agent: string, index: number) => resolveSubagentIntercomTarget(id, agent, index) : undefined;
2042
2104
 
@@ -2092,6 +2154,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2092
2154
  turnBudget: data.turnBudget,
2093
2155
  toolBudget: data.toolBudget,
2094
2156
  configToolBudget: data.configToolBudget,
2157
+ capabilityCeiling: data.capabilityCeiling,
2095
2158
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2096
2159
  });
2097
2160
  }
@@ -2133,6 +2196,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2133
2196
  turnBudget: data.turnBudget,
2134
2197
  toolBudget: data.toolBudget,
2135
2198
  configToolBudget: data.configToolBudget,
2199
+ capabilityCeiling: data.capabilityCeiling,
2136
2200
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2137
2201
  });
2138
2202
  }
@@ -2152,7 +2216,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2152
2216
  const normalizedSkills = normalizeSkillInput(params.skill);
2153
2217
  const skills = normalizedSkills === false ? [] : normalizedSkills;
2154
2218
  const maxSubagentDepth = resolveChildMaxSubagentDepth(currentMaxSubagentDepth, a.maxSubagentDepth);
2155
- const modelOverride = resolveEffectiveSubagentModel(params.model as string | undefined, a.model, ctx.model, availableModels, currentProvider, { scope: data.modelScope });
2219
+ const modelOverride = resolveEffectiveSubagentModel(params.model as string | undefined, a.model, parentModel, availableModels, currentProvider, { scope: data.modelScope });
2156
2220
  return executeAsyncSingle(id, {
2157
2221
  agent: params.agent!,
2158
2222
  task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(params.task ?? "") : (params.task ?? ""),
@@ -2190,6 +2254,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2190
2254
  turnBudget: data.turnBudget,
2191
2255
  toolBudget: data.toolBudget,
2192
2256
  configToolBudget: data.configToolBudget,
2257
+ capabilityCeiling: data.capabilityCeiling,
2193
2258
  });
2194
2259
  }
2195
2260
 
@@ -2223,11 +2288,12 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2223
2288
  const chainSkills = normalized === false ? [] : (normalized ?? []);
2224
2289
  const chain = wrapChainTasksForFork(params.chain as ChainStep[], contextPolicy);
2225
2290
  const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
2291
+ const chainCtx = normalizeParentModel(ctx.model) || !data.parentModel ? ctx : { ...ctx, model: data.parentModel };
2226
2292
  const chainResult = await executeChain({
2227
2293
  chain,
2228
2294
  task: params.task,
2229
2295
  agents,
2230
- ctx,
2296
+ ctx: chainCtx,
2231
2297
  modelScope: data.modelScope,
2232
2298
  intercomEvents: deps.pi.events,
2233
2299
  signal,
@@ -2265,6 +2331,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2265
2331
  toolBudget: data.toolBudget,
2266
2332
  configToolBudget: data.configToolBudget,
2267
2333
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2334
+ capabilityCeiling: data.capabilityCeiling,
2268
2335
  });
2269
2336
 
2270
2337
  if (chainResult.requestedAsync) {
@@ -2276,13 +2343,14 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2276
2343
  };
2277
2344
  }
2278
2345
  const id = randomUUID();
2346
+ const parentModel = data.parentModel;
2279
2347
  const asyncCtx = {
2280
2348
  pi: deps.pi,
2281
2349
  cwd: ctx.cwd,
2282
2350
  currentSessionId: deps.state.currentSessionId!,
2283
2351
  parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
2284
- currentModelProvider: ctx.model?.provider,
2285
- currentModel: ctx.model,
2352
+ currentModelProvider: parentModel?.provider,
2353
+ currentModel: parentModel,
2286
2354
  modelScope: data.modelScope,
2287
2355
  interactive: ctx.hasUI,
2288
2356
  };
@@ -2321,11 +2389,12 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2321
2389
  turnBudget: data.turnBudget,
2322
2390
  toolBudget: data.toolBudget,
2323
2391
  configToolBudget: data.configToolBudget,
2392
+ capabilityCeiling: data.capabilityCeiling,
2324
2393
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2325
2394
  });
2326
2395
  }
2327
2396
 
2328
- const rawChainDetails = chainResult.details ? { ...chainResult.details, runId } : undefined;
2397
+ const rawChainDetails = chainResult.details ? { ...chainResult.details, runId, timeoutMs: data.timeoutMs } : undefined;
2329
2398
  if (foregroundControl && rawChainDetails) {
2330
2399
  updateForegroundNestedProjection(foregroundControl);
2331
2400
  attachRootChildrenToSteps(runId, rawChainDetails.results, foregroundControl.nestedChildren);
@@ -2363,6 +2432,7 @@ interface ForegroundParallelRunInput {
2363
2432
  state: SubagentState;
2364
2433
  intercomEvents: IntercomEventBus;
2365
2434
  parentSessionId: string | null;
2435
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
2366
2436
  signal: AbortSignal;
2367
2437
  runId: string;
2368
2438
  sessionDirForIndex: (idx?: number) => string | undefined;
@@ -2380,6 +2450,7 @@ interface ForegroundParallelRunInput {
2380
2450
  waitToolEnabled?: boolean;
2381
2451
  availableModels: ModelInfo[];
2382
2452
  modelScope?: ModelScopeConfig;
2453
+ parentModel?: ParentModel;
2383
2454
  modelOverrides: (string | undefined)[];
2384
2455
  behaviors: Array<ReturnType<typeof resolveStepBehavior>>;
2385
2456
  firstProgressIndex: number;
@@ -2605,6 +2676,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
2605
2676
  outputMode: behavior?.outputMode,
2606
2677
  maxSubagentDepth: input.maxSubagentDepths[index],
2607
2678
  waitToolEnabled: input.waitToolEnabled,
2679
+ capabilityCeiling: input.capabilityCeiling,
2608
2680
  controlConfig: input.controlConfig,
2609
2681
  onControlEvent: input.onControlEvent,
2610
2682
  onDetachedExit: (result) => updateRememberedForegroundChild(input.state, { runId: input.runId, mode: "parallel", cwd: taskCwd, sessionId: input.parentSessionId, index, result, events: input.intercomEvents }),
@@ -2614,7 +2686,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
2614
2686
  modelOverride: input.modelOverrides[index],
2615
2687
  thinkingOverride: input.thinkingOverrideForTask(task.agent, index, input.modelOverrides[index]),
2616
2688
  availableModels: input.availableModels,
2617
- preferredModelProvider: input.ctx.model?.provider,
2689
+ preferredModelProvider: input.parentModel?.provider,
2618
2690
  modelScope: input.modelScope,
2619
2691
  skills: effectiveSkills === false ? [] : effectiveSkills,
2620
2692
  structuredOutput: structuredRuntime,
@@ -2719,7 +2791,8 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2719
2791
  if (worktreeTaskCwdError) return buildParallelModeError(worktreeTaskCwdError);
2720
2792
  }
2721
2793
 
2722
- const currentProvider = ctx.model?.provider;
2794
+ const parentModel = data.parentModel;
2795
+ const currentProvider = parentModel?.provider;
2723
2796
  const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo);
2724
2797
  let taskTexts = tasks.map((t) => t.task);
2725
2798
  const skillOverrides: (string[] | false | undefined)[] = tasks.map((t) =>
@@ -2734,7 +2807,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2734
2807
  ...(task.model !== undefined ? { model: task.model } : {}),
2735
2808
  }));
2736
2809
  const modelOverrides: (string | undefined)[] = tasks.map((_, i) =>
2737
- resolveEffectiveSubagentModel(behaviorOverrides[i]?.model, agentConfigs[i]?.model, ctx.model, availableModels, currentProvider, { scope: data.modelScope }),
2810
+ resolveEffectiveSubagentModel(behaviorOverrides[i]?.model, agentConfigs[i]?.model, parentModel, availableModels, currentProvider, { scope: data.modelScope }),
2738
2811
  );
2739
2812
 
2740
2813
  if (params.clarify === true && ctx.hasUI) {
@@ -2769,7 +2842,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2769
2842
  for (let i = 0; i < result.behaviorOverrides.length; i++) {
2770
2843
  const override = result.behaviorOverrides[i];
2771
2844
  if (override?.model !== undefined) {
2772
- modelOverrides[i] = resolveEffectiveSubagentModel(override.model, agentConfigs[i]?.model, ctx.model, availableModels, currentProvider, { scope: data.modelScope });
2845
+ modelOverrides[i] = resolveEffectiveSubagentModel(override.model, agentConfigs[i]?.model, parentModel, availableModels, currentProvider, { scope: data.modelScope });
2773
2846
  behaviorOverrides[i]!.model = override.model;
2774
2847
  }
2775
2848
  if (override?.output !== undefined) behaviorOverrides[i]!.output = override.output;
@@ -2795,8 +2868,8 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2795
2868
  cwd: ctx.cwd,
2796
2869
  currentSessionId: deps.state.currentSessionId!,
2797
2870
  parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
2798
- currentModelProvider: ctx.model?.provider,
2799
- currentModel: ctx.model,
2871
+ currentModelProvider: parentModel?.provider,
2872
+ currentModel: parentModel,
2800
2873
  modelScope: data.modelScope,
2801
2874
  interactive: ctx.hasUI,
2802
2875
  };
@@ -2915,6 +2988,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2915
2988
  state: deps.state,
2916
2989
  intercomEvents: deps.pi.events,
2917
2990
  parentSessionId: data.parentSessionId,
2991
+ capabilityCeiling: data.capabilityCeiling,
2918
2992
  signal,
2919
2993
  runId,
2920
2994
  sessionDirForIndex,
@@ -2930,6 +3004,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2930
3004
  progressDir: parallelProgressDir,
2931
3005
  availableModels,
2932
3006
  modelScope: data.modelScope,
3007
+ parentModel,
2933
3008
  modelOverrides,
2934
3009
  behaviors,
2935
3010
  firstProgressIndex: parallelProgressPrecreated ? -1 : firstProgressIndex,
@@ -2976,6 +3051,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2976
3051
  const details = compactForegroundDetails({
2977
3052
  mode: "parallel",
2978
3053
  runId,
3054
+ timeoutMs: data.timeoutMs,
2979
3055
  results,
2980
3056
  progress: params.includeProgress ? allProgress : undefined,
2981
3057
  artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
@@ -3077,13 +3153,14 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3077
3153
  const effectiveToolBudget = resolveEffectiveToolBudget({ runBudget: data.toolBudget, agentBudget: agentConfig.toolBudget, configBudget: data.configToolBudget });
3078
3154
  if (effectiveToolBudget.error) return toExecutionErrorResult(params, new Error(effectiveToolBudget.error), data.contextPolicy.contextSummary);
3079
3155
 
3080
- const currentProvider = ctx.model?.provider;
3156
+ const parentModel = data.parentModel;
3157
+ const currentProvider = parentModel?.provider;
3081
3158
  const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo);
3082
3159
  let task = params.task ?? "";
3083
3160
  let modelOverride: string | undefined = resolveEffectiveSubagentModel(
3084
3161
  params.model as string | undefined,
3085
3162
  agentConfig.model,
3086
- ctx.model,
3163
+ parentModel,
3087
3164
  availableModels,
3088
3165
  currentProvider,
3089
3166
  { scope: data.modelScope },
@@ -3123,7 +3200,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3123
3200
 
3124
3201
  task = result.templates[0]!;
3125
3202
  const override = result.behaviorOverrides[0];
3126
- if (override?.model !== undefined) modelOverride = resolveEffectiveSubagentModel(override.model, agentConfig.model, ctx.model, availableModels, currentProvider, { scope: data.modelScope });
3203
+ if (override?.model !== undefined) modelOverride = resolveEffectiveSubagentModel(override.model, agentConfig.model, parentModel, availableModels, currentProvider, { scope: data.modelScope });
3127
3204
  if (override?.output !== undefined) effectiveOutput = normalizeSingleOutputOverride(override.output, agentConfig.output);
3128
3205
  if (override?.skills !== undefined) skillOverride = override.skills;
3129
3206
 
@@ -3141,8 +3218,8 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3141
3218
  cwd: ctx.cwd,
3142
3219
  currentSessionId: deps.state.currentSessionId!,
3143
3220
  parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
3144
- currentModelProvider: ctx.model?.provider,
3145
- currentModel: ctx.model,
3221
+ currentModelProvider: parentModel?.provider,
3222
+ currentModel: parentModel,
3146
3223
  modelScope: data.modelScope,
3147
3224
  interactive: ctx.hasUI,
3148
3225
  };
@@ -3274,6 +3351,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3274
3351
  turnBudget: data.turnBudget,
3275
3352
  enforceHardTurnLimit: params.enforceHardTurnLimit,
3276
3353
  toolBudget: effectiveToolBudget.toolBudget,
3354
+ capabilityCeiling: data.capabilityCeiling,
3277
3355
  allowZeroToolBudget: data.allowZeroToolBudget && effectiveToolBudget.toolBudget === data.toolBudget,
3278
3356
  });
3279
3357
  } finally {
@@ -3303,6 +3381,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3303
3381
  const details = compactForegroundDetails({
3304
3382
  mode: "single",
3305
3383
  runId,
3384
+ timeoutMs: data.timeoutMs,
3306
3385
  results: [r],
3307
3386
  ...(data.turnBudget ? { turnBudget: data.turnBudget } : {}),
3308
3387
  ...(effectiveToolBudget.toolBudget ? { toolBudget: effectiveToolBudget.toolBudget } : {}),
@@ -3431,6 +3510,13 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3431
3510
  const requestCwd = resolveRequestedCwd(ctx.cwd, requestParams.cwd);
3432
3511
  const paramsWithResolvedCwd = requestParams.cwd === undefined ? requestParams : { ...requestParams, cwd: requestCwd };
3433
3512
  const action = paramsWithResolvedCwd.action;
3513
+ let requestParentModel: ParentModel | undefined;
3514
+ try {
3515
+ requestParentModel = rememberParentModel(deps.state, resolveCurrentSessionId(ctx.sessionManager), ctx.model);
3516
+ } catch (error) {
3517
+ if (action?.toLowerCase() !== "doctor") throw error;
3518
+ requestParentModel = normalizeParentModel(ctx.model);
3519
+ }
3434
3520
  if (action) {
3435
3521
  if ((WATCHDOG_TOOL_ACTIONS as readonly string[]).includes(action)) {
3436
3522
  if (deps.allowMutatingManagementActions === false && MUTATING_MANAGEMENT_ACTIONS.has(action)) {
@@ -3588,7 +3674,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3588
3674
  return withBudget(inspectSubagentStatus(paramsWithResolvedCwd, { state: deps.state, nested: nestedScope, sessionRoots }));
3589
3675
  }
3590
3676
  if (action === "resume") {
3591
- return resumeAsyncRun({ params: paramsWithResolvedCwd, requestCwd, ctx, deps });
3677
+ return resumeAsyncRun({ params: paramsWithResolvedCwd, requestCwd, ctx, deps, parentModel: requestParentModel });
3592
3678
  }
3593
3679
  if (action === "steer") {
3594
3680
  deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
@@ -3611,7 +3697,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3611
3697
  ? {}
3612
3698
  : {
3613
3699
  recover: ({ absoluteDeadlineAt, ...limits }) =>
3614
- resumeAsyncRun({ params: { ...limits, action: "resume", id: runId, message }, requestCwd, ctx, deps, absoluteDeadlineAt }),
3700
+ resumeAsyncRun({ params: { ...limits, action: "resume", id: runId, message }, requestCwd, ctx, deps, parentModel: requestParentModel, absoluteDeadlineAt }),
3615
3701
  }
3616
3702
  ),
3617
3703
  });
@@ -3648,6 +3734,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3648
3734
  requestCwd,
3649
3735
  ctx,
3650
3736
  deps,
3737
+ parentModel: requestParentModel,
3651
3738
  absoluteDeadlineAt,
3652
3739
  }),
3653
3740
  }
@@ -3655,7 +3742,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3655
3742
  });
3656
3743
  }
3657
3744
  if (action === "append-step") {
3658
- return appendStepToAsyncChain({ params: paramsWithResolvedCwd, requestCwd, ctx, deps });
3745
+ return appendStepToAsyncChain({ params: paramsWithResolvedCwd, requestCwd, ctx, deps, parentModel: requestParentModel });
3659
3746
  }
3660
3747
  if (action === "schedule" || action === "schedule-list" || action === "schedule-status" || action === "schedule-cancel") {
3661
3748
  if (!deps.handleScheduledRunAction) {
@@ -3802,13 +3889,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3802
3889
  const scope: AgentScope = resolveExecutionAgentScope(effectiveParams.agentScope);
3803
3890
  const effectiveCwd = effectiveParams.cwd ?? ctx.cwd;
3804
3891
  const parentSessionFile = ctx.sessionManager.getSessionFile() ?? null;
3805
- deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
3806
3892
  const discovered = deps.discoverAgents(effectiveCwd, scope);
3807
3893
  const discoveredAgents = discovered.agents;
3808
3894
  const modelScope = discovered.modelScope;
3809
3895
  effectiveParams = applySingleAgentLaunchDefaults(effectiveParams, discoveredAgents);
3810
- const foregroundTimeout = resolveForegroundTimeout(effectiveParams);
3811
- if (foregroundTimeout.error) return buildRequestedModeError(effectiveParams, foregroundTimeout.error);
3812
3896
  const turnBudget = resolveTurnBudgetConfig(effectiveParams.turnBudget ?? deps.config.turnBudget);
3813
3897
  if (turnBudget.error) return buildRequestedModeError(effectiveParams, turnBudget.error);
3814
3898
  const contextPolicy = resolveAgentDefaultContextPolicy(effectiveParams, discoveredAgents);
@@ -3862,25 +3946,26 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3862
3946
  const forkThinkingDowngrades = new Map<number, string>();
3863
3947
  try {
3864
3948
  const forkAvailableModels = contextPolicy.usesFork ? ctx.modelRegistry.getAvailable().map(toModelInfo) : [];
3949
+ const parentModel = requestParentModel;
3865
3950
  prepareForkThinking = (agentName, index, modelOverride) => {
3866
3951
  const agentConfig = agents.find((agent) => agent.name === agentName);
3867
3952
  const primaryModel = resolveEffectiveSubagentModel(
3868
3953
  modelOverride,
3869
3954
  agentConfig?.model,
3870
- ctx.model,
3955
+ parentModel,
3871
3956
  forkAvailableModels,
3872
- ctx.model?.provider,
3957
+ parentModel?.provider,
3873
3958
  );
3874
3959
  const candidates = buildModelCandidates(
3875
3960
  primaryModel,
3876
3961
  agentConfig?.fallbackModels,
3877
3962
  forkAvailableModels,
3878
- ctx.model?.provider,
3963
+ parentModel?.provider,
3879
3964
  );
3880
3965
  forkThinkingRequirements.set(
3881
3966
  index,
3882
3967
  candidates.length === 0
3883
- || candidates.some((candidate) => forkedChildRequiresThinkingOff(candidate, forkAvailableModels, ctx.model?.provider)),
3968
+ || candidates.some((candidate) => forkedChildRequiresThinkingOff(candidate, forkAvailableModels, parentModel?.provider)),
3884
3969
  );
3885
3970
  };
3886
3971
  const forkContextResolver = createForkContextResolver(ctx.sessionManager, contextPolicy.usesFork ? "fork" : undefined, {
@@ -3894,6 +3979,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3894
3979
  const requestedAsync = effectiveParams.async ?? deps.asyncByDefault;
3895
3980
  const backgroundRequestedWhileClarifying = (hasChain || hasTasks) && requestedAsync && effectiveParams.clarify === true;
3896
3981
  const effectiveAsync = requestedAsync && effectiveParams.clarify !== true;
3982
+ const foregroundTimeout = resolveForegroundTimeout(
3983
+ effectiveParams,
3984
+ effectiveAsync ? undefined : DEFAULT_FOREGROUND_TIMEOUT_MS,
3985
+ );
3986
+ if (foregroundTimeout.error) return buildRequestedModeError(effectiveParams, foregroundTimeout.error);
3897
3987
  const controlConfig = resolveControlConfig(deps.config.control, effectiveParams.control);
3898
3988
 
3899
3989
  const artifactConfig: ArtifactConfig = {
@@ -3951,7 +4041,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3951
4041
  if (chainBindingsError) return withResolvedContext(chainBindingsError, contextPolicy.contextSummary);
3952
4042
 
3953
4043
  const onUpdateWithContext = onUpdate
3954
- ? (r: AgentToolResult<Details>) => onUpdate(withResolvedContext(r, contextPolicy.contextSummary))
4044
+ ? (r: AgentToolResult<Details>) => onUpdate(withResolvedContext({
4045
+ ...r,
4046
+ details: { ...r.details, runId },
4047
+ }, contextPolicy.contextSummary))
3955
4048
  : undefined;
3956
4049
 
3957
4050
  const reservation = reserveSpawnBudget(
@@ -3990,7 +4083,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3990
4083
  configToolBudget: configToolBudget.toolBudget,
3991
4084
  contextPolicy,
3992
4085
  modelScope,
4086
+ parentModel: requestParentModel,
3993
4087
  parentSessionId: deps.state.currentSessionId,
4088
+ capabilityCeiling: resolveCurrentSubagentCapabilityCeiling(deps.state.currentSessionId ?? undefined),
3994
4089
  };
3995
4090
 
3996
4091
  const foregroundDescription = effectiveParams.task?.trim()