pi-subagents 0.38.0 → 0.40.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 (59) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +85 -13
  3. package/agents/worker.md +1 -0
  4. package/package.json +1 -1
  5. package/prompts/review-loop.md +1 -1
  6. package/skills/pi-subagents/SKILL.md +4 -3
  7. package/skills/pi-subagents/references/execution-controls.md +22 -3
  8. package/skills/pi-subagents/references/management-authoring-rpc.md +5 -1
  9. package/skills/pi-subagents/references/prompting-and-roles.md +17 -3
  10. package/src/agents/agent-management.ts +71 -30
  11. package/src/agents/agent-serializer.ts +4 -0
  12. package/src/agents/agents.ts +60 -1
  13. package/src/api/preflight.ts +16 -12
  14. package/src/extension/chain-validation.ts +17 -1
  15. package/src/extension/rpc.ts +2 -0
  16. package/src/extension/schemas.ts +16 -3
  17. package/src/extension/tool-description.ts +6 -5
  18. package/src/intercom/result-intercom.ts +33 -4
  19. package/src/runs/background/async-execution.ts +63 -16
  20. package/src/runs/background/async-job-tracker.ts +5 -1
  21. package/src/runs/background/async-resume.ts +1 -0
  22. package/src/runs/background/async-status.ts +18 -2
  23. package/src/runs/background/chain-append.ts +48 -5
  24. package/src/runs/background/control-channel.ts +60 -0
  25. package/src/runs/background/notify.ts +26 -4
  26. package/src/runs/background/result-watcher.ts +20 -3
  27. package/src/runs/background/run-status.ts +29 -3
  28. package/src/runs/background/subagent-runner.ts +302 -18
  29. package/src/runs/foreground/async-stop-action.ts +65 -0
  30. package/src/runs/foreground/chain-execution.ts +143 -43
  31. package/src/runs/foreground/execution.ts +384 -202
  32. package/src/runs/foreground/foreground-control.ts +30 -0
  33. package/src/runs/foreground/subagent-executor.ts +314 -77
  34. package/src/runs/shared/capability-ceiling.ts +51 -19
  35. package/src/runs/shared/chain-outputs.ts +3 -1
  36. package/src/runs/shared/dynamic-fanout.ts +1 -1
  37. package/src/runs/shared/nested-events.ts +97 -20
  38. package/src/runs/shared/parallel-utils.ts +43 -12
  39. package/src/runs/shared/pi-args.ts +45 -3
  40. package/src/runs/shared/process-signal.ts +19 -0
  41. package/src/runs/shared/run-history.ts +45 -9
  42. package/src/runs/shared/runtime-acknowledged-extensions.ts +71 -0
  43. package/src/runs/shared/subagent-prompt-runtime.ts +30 -0
  44. package/src/runs/shared/usage-budget.ts +65 -0
  45. package/src/runs/shared/workflow-graph.ts +26 -1
  46. package/src/shared/settings.ts +17 -1
  47. package/src/shared/types.ts +124 -8
  48. package/src/shared/utils.ts +12 -1
  49. package/src/slash/slash-commands.ts +43 -1
  50. package/src/slash/slash-live-state.ts +5 -3
  51. package/src/tui/fleet-status.ts +12 -2
  52. package/src/tui/fleet.ts +181 -8
  53. package/src/tui/render.ts +4 -2
  54. package/src/watchdog/register-main.ts +14 -7
  55. package/src/watchdog/review.ts +4 -3
  56. package/src/watchdog/runtime.ts +170 -17
  57. package/src/watchdog/scope.ts +62 -0
  58. package/src/watchdog/settings.ts +41 -1
  59. package/src/watchdog/types.ts +10 -0
@@ -3,12 +3,19 @@ import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
5
5
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
6
- import type { AgentConfig, AgentScope } from "../../agents/agents.ts";
6
+ import { resolveAgentName, type AgentConfig, type AgentScope } from "../../agents/agents.ts";
7
7
  import { getArtifactsDir, getProjectChainRunsDir } from "../../shared/artifacts.ts";
8
8
  import { ChainClarifyComponent, type ChainClarifyResult } from "./chain-clarify.ts";
9
- import { toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
9
+ import { resolveEffectiveThinking, toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
10
10
  import { executeChain } from "./chain-execution.ts";
11
- import { beginForegroundChild, finishForegroundChild, updateForegroundChild } from "./foreground-control.ts";
11
+ import {
12
+ beginForegroundChild,
13
+ finishForegroundChild,
14
+ foregroundSchedulingSettled,
15
+ retainForegroundSchedulingOwner,
16
+ settleForegroundSchedulingOwner,
17
+ updateForegroundChild,
18
+ } from "./foreground-control.ts";
12
19
  import { resolveExecutionAgentScope } from "../../agents/agent-scope.ts";
13
20
  import { handleManagementAction } from "../../agents/agent-management.ts";
14
21
  import { buildDoctorReport } from "../../extension/doctor.ts";
@@ -47,6 +54,7 @@ import { formatControlIntercomMessage, formatControlNoticeMessage, resolveContro
47
54
  import { resolveTurnBudgetConfig } from "../shared/turn-budget.ts";
48
55
  import { formatSpawnBudget, getSpawnBudgetSnapshot, grantSpawnBudget, preflightSpawnBudget, preflightSpawnBudgetGrant, reserveSpawnBudget } from "../shared/spawn-budget.ts";
49
56
  import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
57
+ import { usageBudgetExceededMessage, usageBudgetState, validateUsageBudgetConfig } from "../shared/usage-budget.ts";
50
58
  import { intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
51
59
  import { isAgentContractV1 } from "../shared/agent-contract.ts";
52
60
  import { finalizeSingleOutput, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
@@ -64,12 +72,13 @@ import {
64
72
  stripDetailsOutputsForIntercomReceipt,
65
73
  } from "../../intercom/result-intercom.ts";
66
74
  import { applySteeringRecoveryAgentConfig, buildRevivedAsyncTask, resolveAsyncResumeTarget, resolveAsyncRunLocation } from "../background/async-resume.ts";
67
- import { deliverInterruptRequest, deliverStopRequest, requestAsyncSteer } from "../background/control-channel.ts";
75
+ import { deliverCheckpointDecisionRequest, deliverInterruptRequest, requestAsyncSteer } from "../background/control-channel.ts";
68
76
  import { waitForSteeringAction } from "../background/steering.ts";
69
77
  import { steerAsyncRun } from "./async-steering-action.ts";
78
+ import { stopAsyncRun } from "./async-stop-action.ts";
70
79
  import { reconcileAsyncRun } from "../background/stale-run-reconciler.ts";
71
80
  import { resolveAsyncRootResultPath } from "../background/chain-root-attachment.ts";
72
- import { attachRootChildrenToSteps, createNestedRoute, readNestedControlResults, resolveInheritedNestedRouteFromEnv, resolveNestedAsyncDir, resolveNestedParentAddressFromEnv, updateForegroundNestedProjection, writeNestedControlRequest, writeNestedEvent, type NestedRunResolutionScope } from "../shared/nested-events.ts";
81
+ import { attachRootChildrenToSteps, createNestedRoute, findNestedControlResult, resolveInheritedNestedRouteFromEnv, resolveNestedAsyncDir, resolveNestedParentAddressFromEnv, snapshotNestedEventFiles, updateForegroundNestedProjection, writeNestedControlRequest, writeNestedEvent, type NestedRunResolutionScope } from "../shared/nested-events.ts";
73
82
  import { resolveSubagentRunId, type ResolvedSubagentRunId } from "../background/run-id-resolver.ts";
74
83
  import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
75
84
  import { inspectSubagentStatus } from "../background/run-status.ts";
@@ -106,6 +115,7 @@ import {
106
115
  type SingleResult,
107
116
  type ToolBudgetConfig,
108
117
  type TurnBudgetConfig,
118
+ type UsageBudgetConfig,
109
119
  type SubagentRunMode,
110
120
  type SubagentState,
111
121
  ASYNC_DIR,
@@ -167,6 +177,7 @@ export interface SubagentParamsLike {
167
177
  /** Internal-only strict turn-boundary enforcement for versioned foreground delegation. */
168
178
  enforceHardTurnLimit?: boolean;
169
179
  toolBudget?: ToolBudgetConfig;
180
+ usageBudget?: UsageBudgetConfig;
170
181
  clarify?: boolean;
171
182
  share?: boolean;
172
183
  control?: ControlConfig;
@@ -245,6 +256,7 @@ interface ExecutionContextData {
245
256
  deadlineAt?: number;
246
257
  turnBudget?: ResolvedTurnBudget;
247
258
  toolBudget?: ResolvedToolBudget;
259
+ usageBudget?: UsageBudgetConfig;
248
260
  allowZeroToolBudget?: boolean;
249
261
  configToolBudget?: ResolvedToolBudget;
250
262
  contextPolicy: AgentDefaultContextPolicy;
@@ -258,6 +270,15 @@ function resolveRequestedCwd(runtimeCwd: string, requestedCwd: string | undefine
258
270
  return requestedCwd ? path.resolve(runtimeCwd, requestedCwd) : runtimeCwd;
259
271
  }
260
272
 
273
+ function removeForegroundControlIfIdle(state: SubagentState, runId: string): boolean {
274
+ const control = state.foregroundControls.get(runId);
275
+ if (control && (!foregroundSchedulingSettled(control) || (control.activeChildren?.size ?? 0) > 0)) return false;
276
+ clearPendingForegroundControlNotices(state, runId);
277
+ state.foregroundControls.delete(runId);
278
+ if (state.lastForegroundControlId === runId) state.lastForegroundControlId = null;
279
+ return true;
280
+ }
281
+
261
282
  function getForegroundControl(state: SubagentState, runId: string | undefined) {
262
283
  if (runId) return state.foregroundControls.get(runId);
263
284
  if (state.lastForegroundControlId) {
@@ -392,7 +413,7 @@ function foregroundChildActivityFromProgress(progress: SingleResult["progress"]
392
413
  };
393
414
  }
394
415
 
395
- function rememberForegroundRun(state: SubagentState, input: { runId: string; mode: "single" | "parallel" | "chain"; cwd: string; sessionId: string | null; results: SingleResult[] }): void {
416
+ function rememberForegroundRun(state: SubagentState, input: { runId: string; mode: "single" | "parallel" | "chain"; cwd: string; sessionId: string | null; results: SingleResult[]; checkpoint?: Details["checkpoint"] }): void {
396
417
  state.foregroundRuns ??= new Map();
397
418
  const previous = state.foregroundRuns.get(input.runId);
398
419
  const updatedAt = Date.now();
@@ -402,17 +423,27 @@ function rememberForegroundRun(state: SubagentState, input: { runId: string; mod
402
423
  cwd: input.cwd,
403
424
  ...(input.sessionId ? { sessionId: input.sessionId } : {}),
404
425
  updatedAt,
426
+ ...(input.checkpoint ? { checkpoint: input.checkpoint } : {}),
405
427
  children: input.results.map((result, index) => {
406
428
  const child = {
407
429
  agent: result.agent,
408
430
  index,
409
431
  ...(result.context ? { context: result.context } : {}),
410
- status: resolveSubagentResultStatus({ exitCode: result.exitCode, interrupted: result.interrupted, detached: result.detached }),
432
+ status: resolveSubagentResultStatus({
433
+ exitCode: result.exitCode,
434
+ interrupted: result.interrupted,
435
+ detached: result.detached,
436
+ processSignal: result.processSignal,
437
+ timedOut: result.timedOut,
438
+ stopped: result.stopped,
439
+ turnBudgetExceeded: result.turnBudgetExceeded,
440
+ }),
411
441
  ...foregroundChildActivityFromProgress(result.progress),
412
442
  updatedAt,
413
443
  ...(result.exitCode !== undefined ? { exitCode: result.exitCode } : {}),
414
444
  ...(result.error ? { error: result.error } : {}),
415
445
  ...(result.finalOutput ? { finalOutput: result.finalOutput } : {}),
446
+ ...(result.outputState ? { outputState: result.outputState } : {}),
416
447
  ...(result.outputMode ? { outputMode: result.outputMode } : {}),
417
448
  ...(result.savedOutputPath ? { savedOutputPath: result.savedOutputPath } : {}),
418
449
  ...(result.outputSaveError ? { outputSaveError: result.outputSaveError } : {}),
@@ -425,6 +456,8 @@ function rememberForegroundRun(state: SubagentState, input: { runId: string; mod
425
456
  ...(result.detachedReason ? { detachedReason: result.detachedReason } : {}),
426
457
  ...(result.acceptance ? { acceptance: result.acceptance } : {}),
427
458
  ...(result.launchContractDigest ? { launchContractDigest: result.launchContractDigest } : {}),
459
+ ...(result.launchResolvedExtensions ? { launchResolvedExtensions: result.launchResolvedExtensions } : {}),
460
+ ...(result.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: result.runtimeAcknowledgedExtensions } : {}),
428
461
  ...(result.capabilityCeiling ? { capabilityCeiling: result.capabilityCeiling } : {}),
429
462
  ...(result.capabilityAudit ? { capabilityAudit: result.capabilityAudit } : {}),
430
463
  };
@@ -467,20 +500,29 @@ function updateRememberedForegroundChild(state: SubagentState, input: { runId: s
467
500
  state.foregroundRuns.set(input.runId, run);
468
501
  }
469
502
  run.updatedAt = updatedAt;
503
+ const terminalStatus = resolveSubagentResultStatus({
504
+ exitCode: input.result.exitCode,
505
+ ...(input.result.acceptance?.status === "rejected" ? { success: false } : {}),
506
+ interrupted: input.result.interrupted,
507
+ detached: false,
508
+ processSignal: input.result.processSignal,
509
+ timedOut: input.result.timedOut,
510
+ stopped: input.result.stopped,
511
+ turnBudgetExceeded: input.result.turnBudgetExceeded,
512
+ });
470
513
  const child = run.children[input.index] ?? { agent: input.result.agent, index: input.index, status: "detached" as const };
471
514
  run.children[input.index] = {
472
515
  ...child,
473
516
  agent: input.result.agent,
474
517
  index: input.index,
475
518
  ...(input.result.context ? { context: input.result.context } : {}),
476
- status: input.result.acceptance?.status === "rejected"
477
- ? "failed"
478
- : resolveSubagentResultStatus({ exitCode: input.result.exitCode, interrupted: input.result.interrupted, detached: false }),
519
+ status: terminalStatus,
479
520
  ...foregroundChildActivityFromProgress(input.result.progress),
480
521
  updatedAt,
481
522
  ...(input.result.exitCode !== undefined ? { exitCode: input.result.exitCode } : {}),
482
523
  ...(input.result.error ? { error: input.result.error } : {}),
483
524
  ...(input.result.finalOutput ? { finalOutput: input.result.finalOutput } : {}),
525
+ outputState: input.result.outputState,
484
526
  outputMode: input.result.outputMode,
485
527
  savedOutputPath: input.result.savedOutputPath,
486
528
  outputSaveError: input.result.outputSaveError,
@@ -493,12 +535,14 @@ function updateRememberedForegroundChild(state: SubagentState, input: { runId: s
493
535
  ...(input.result.detachedReason ? { detachedReason: input.result.detachedReason } : {}),
494
536
  ...(input.result.acceptance ? { acceptance: input.result.acceptance } : {}),
495
537
  ...(input.result.launchContractDigest ? { launchContractDigest: input.result.launchContractDigest } : {}),
538
+ ...(input.result.launchResolvedExtensions ? { launchResolvedExtensions: input.result.launchResolvedExtensions } : {}),
539
+ ...(input.result.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: input.result.runtimeAcknowledgedExtensions } : {}),
496
540
  ...(input.result.capabilityCeiling ? { capabilityCeiling: input.result.capabilityCeiling } : {}),
497
541
  ...(input.result.capabilityAudit ? { capabilityAudit: input.result.capabilityAudit } : {}),
498
542
  };
499
543
  trimRememberedForegroundRuns(state);
500
544
  const output = getSingleResultOutput(input.result).trim();
501
- const success = input.result.exitCode === 0 && input.result.acceptance?.status !== "rejected";
545
+ const success = terminalStatus === "completed";
502
546
  const summary = !success && input.result.error
503
547
  ? `${input.result.error}${output ? `\n\nOutput:\n${output}` : ""}`
504
548
  : output || input.result.error || "Detached child exited without final output.";
@@ -514,7 +558,12 @@ function updateRememberedForegroundChild(state: SubagentState, input: { runId: s
514
558
  success,
515
559
  summary,
516
560
  exitCode: input.result.exitCode,
517
- state: success ? "complete" : "failed",
561
+ state: terminalStatus === "completed" ? "complete" : terminalStatus,
562
+ ...(input.result.interrupted !== undefined ? { interrupted: input.result.interrupted } : {}),
563
+ ...(input.result.stopped !== undefined ? { stopped: input.result.stopped } : {}),
564
+ ...(input.result.processSignal !== undefined ? { processSignal: input.result.processSignal } : {}),
565
+ ...(input.result.timedOut !== undefined ? { timedOut: input.result.timedOut } : {}),
566
+ ...(input.result.turnBudgetExceeded !== undefined ? { turnBudgetExceeded: input.result.turnBudgetExceeded } : {}),
518
567
  timestamp: updatedAt,
519
568
  cwd: input.cwd,
520
569
  sessionFile: input.result.sessionFile,
@@ -733,50 +782,6 @@ function interruptAsyncRun(
733
782
  }
734
783
  }
735
784
 
736
- function stopAsyncRun(
737
- state: SubagentState,
738
- runId: string | undefined,
739
- kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean,
740
- location?: { asyncDir: string | null; resolvedId?: string },
741
- ): AgentToolResult<Details> | null {
742
- const target = getAsyncInterruptTarget(state, runId, location, { fallbackToNewest: false });
743
- if (!target) return null;
744
- const status = reconcileAsyncRun(target.asyncDir, { kill }).status;
745
- if (state.currentSessionId && status?.sessionId !== state.currentSessionId) {
746
- return {
747
- content: [{ type: "text", text: `Async run '${target.asyncId}' was not found in the active session.` }],
748
- isError: true,
749
- details: { mode: "management", results: [] },
750
- };
751
- }
752
- if (!status || (status.state !== "running" && status.state !== "queued")) {
753
- return {
754
- content: [{ type: "text", text: `No running or queued async run was found for '${runId ?? "current"}'.` }],
755
- isError: true,
756
- details: { mode: "management", results: [] },
757
- };
758
- }
759
- try {
760
- deliverStopRequest({ asyncDir: target.asyncDir, pid: typeof status.pid === "number" ? status.pid : undefined, kill, source: "stop-action" });
761
- const tracked = state.asyncJobs.get(target.asyncId);
762
- if (tracked) {
763
- tracked.activityState = undefined;
764
- tracked.updatedAt = Date.now();
765
- }
766
- return {
767
- content: [{ type: "text", text: `Stop requested for async run ${target.asyncId}.` }],
768
- details: { mode: "management", results: [] },
769
- };
770
- } catch (error) {
771
- const message = error instanceof Error ? error.message : String(error);
772
- return {
773
- content: [{ type: "text", text: `Failed to stop async run ${target.asyncId}: ${message}` }],
774
- isError: true,
775
- details: { mode: "management", results: [] },
776
- };
777
- }
778
- }
779
-
780
785
  function duplicateNames(names: string[]): string[] {
781
786
  const seen = new Set<string>();
782
787
  const duplicates = new Set<string>();
@@ -1032,10 +1037,10 @@ function resolveNestedResumeTarget(match: ResolvedSubagentRunId & { kind: "neste
1032
1037
  };
1033
1038
  }
1034
1039
 
1035
- async function waitForNestedControlResult(target: ResolvedSubagentRunId & { kind: "nested" }, requestId: string, timeoutMs = 1_000) {
1040
+ async function waitForNestedControlResult(target: ResolvedSubagentRunId & { kind: "nested" }, requestId: string, ignoredFiles: ReadonlySet<string>, timeoutMs = 1_000) {
1036
1041
  const deadline = Date.now() + timeoutMs;
1037
1042
  while (Date.now() < deadline) {
1038
- const result = readNestedControlResults(target.match.route).find((candidate) => candidate.requestId === requestId && candidate.targetRunId === target.match.run.id);
1043
+ const result = findNestedControlResult(target.match.route, requestId, target.match.run.id, ignoredFiles);
1039
1044
  if (result) return result;
1040
1045
  await new Promise((resolve) => setTimeout(resolve, 50));
1041
1046
  }
@@ -1044,14 +1049,16 @@ async function waitForNestedControlResult(target: ResolvedSubagentRunId & { kind
1044
1049
 
1045
1050
  async function sendNestedControlRequest(target: ResolvedSubagentRunId & { kind: "nested" }, action: "interrupt" | "resume", message?: string) {
1046
1051
  const requestId = randomUUID();
1052
+ const ignoredFiles = snapshotNestedEventFiles(target.match.route);
1053
+ const requestedAt = Date.now();
1047
1054
  writeNestedControlRequest(target.match.route, {
1048
- ts: Date.now(),
1055
+ ts: requestedAt,
1049
1056
  requestId,
1050
1057
  targetRunId: target.match.run.id,
1051
1058
  action,
1052
1059
  ...(message ? { message } : {}),
1053
1060
  });
1054
- return waitForNestedControlResult(target, requestId);
1061
+ return waitForNestedControlResult(target, requestId, ignoredFiles);
1055
1062
  }
1056
1063
 
1057
1064
  function directNestedAsyncInterrupt(target: ResolvedSubagentRunId & { kind: "nested" }): AgentToolResult<Details> | undefined {
@@ -1477,7 +1484,12 @@ async function emitForegroundResultIntercom(input: {
1477
1484
  exitCode: result.exitCode,
1478
1485
  interrupted: result.interrupted,
1479
1486
  detached: result.detached,
1487
+ processSignal: result.processSignal,
1488
+ timedOut: result.timedOut,
1489
+ stopped: result.stopped,
1490
+ turnBudgetExceeded: result.turnBudgetExceeded,
1480
1491
  }),
1492
+ outputState: result.outputState ?? "unknown",
1481
1493
  summary: resultSummaryForIntercom(result),
1482
1494
  index,
1483
1495
  artifactPath: result.artifactPaths?.outputPath,
@@ -1524,6 +1536,67 @@ async function maybeBuildForegroundIntercomReceipt(input: {
1524
1536
  };
1525
1537
  }
1526
1538
 
1539
+ function canonicalizeAgentName(name: string, agents: AgentConfig[]): { name?: string; error?: string } {
1540
+ const resolved = resolveAgentName(name, agents);
1541
+ if (resolved.error) return { error: resolved.error };
1542
+ if (!resolved.agent) return { error: `Unknown agent: ${name}` };
1543
+ return { name: resolved.agent.name };
1544
+ }
1545
+
1546
+ function canonicalizeExecutionParams(params: SubagentParamsLike, agents: AgentConfig[]): { params?: SubagentParamsLike; error?: string } {
1547
+ const resolve = (name: string, location?: string): { name?: string; error?: string } => {
1548
+ const result = canonicalizeAgentName(name, agents);
1549
+ return result.error && location ? { error: `${result.error} (${location})` } : result;
1550
+ };
1551
+ if (params.agent) {
1552
+ const result = resolve(params.agent);
1553
+ if (result.error) return { error: result.error };
1554
+ params = { ...params, agent: result.name };
1555
+ }
1556
+ if (params.tasks) {
1557
+ const tasks: TaskParam[] = [];
1558
+ for (let index = 0; index < params.tasks.length; index++) {
1559
+ const task = params.tasks[index]!;
1560
+ const result = resolve(task.agent, `task ${index + 1}`);
1561
+ if (result.error) return { error: result.error };
1562
+ tasks.push({ ...task, agent: result.name! });
1563
+ }
1564
+ params = { ...params, tasks };
1565
+ }
1566
+ if (params.chain) {
1567
+ const chain: ChainStep[] = [];
1568
+ for (let index = 0; index < params.chain.length; index++) {
1569
+ const step = params.chain[index]!;
1570
+ if (isParallelStep(step)) {
1571
+ const parallel: typeof step.parallel = [];
1572
+ for (let taskIndex = 0; taskIndex < step.parallel.length; taskIndex++) {
1573
+ const task = step.parallel[taskIndex]!;
1574
+ const result = resolve(task.agent, `step ${index + 1}, task ${taskIndex + 1}`);
1575
+ if (result.error) return { error: result.error };
1576
+ parallel.push({ ...task, agent: result.name! });
1577
+ }
1578
+ chain.push({ ...step, parallel });
1579
+ continue;
1580
+ }
1581
+ if (isDynamicParallelStep(step)) {
1582
+ const result = resolve(step.parallel.agent, `step ${index + 1}`);
1583
+ if (result.error) return { error: result.error };
1584
+ chain.push({ ...step, parallel: { ...step.parallel, agent: result.name! } });
1585
+ continue;
1586
+ }
1587
+ if ("agent" in step && typeof step.agent === "string") {
1588
+ const result = resolve(step.agent, `step ${index + 1}`);
1589
+ if (result.error) return { error: result.error };
1590
+ chain.push({ ...step, agent: result.name! });
1591
+ continue;
1592
+ }
1593
+ chain.push(step);
1594
+ }
1595
+ params = { ...params, chain };
1596
+ }
1597
+ return { params };
1598
+ }
1599
+
1527
1600
  function validateExecutionInput(
1528
1601
  params: SubagentParamsLike,
1529
1602
  agents: AgentConfig[],
@@ -2153,6 +2226,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2153
2226
  timeoutMs: data.timeoutMs,
2154
2227
  turnBudget: data.turnBudget,
2155
2228
  toolBudget: data.toolBudget,
2229
+ usageBudget: data.usageBudget,
2156
2230
  configToolBudget: data.configToolBudget,
2157
2231
  capabilityCeiling: data.capabilityCeiling,
2158
2232
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
@@ -2195,6 +2269,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2195
2269
  timeoutMs: data.timeoutMs,
2196
2270
  turnBudget: data.turnBudget,
2197
2271
  toolBudget: data.toolBudget,
2272
+ usageBudget: data.usageBudget,
2198
2273
  configToolBudget: data.configToolBudget,
2199
2274
  capabilityCeiling: data.capabilityCeiling,
2200
2275
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
@@ -2253,6 +2328,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2253
2328
  timeoutMs: data.timeoutMs,
2254
2329
  turnBudget: data.turnBudget,
2255
2330
  toolBudget: data.toolBudget,
2331
+ usageBudget: data.usageBudget,
2256
2332
  configToolBudget: data.configToolBudget,
2257
2333
  capabilityCeiling: data.capabilityCeiling,
2258
2334
  });
@@ -2327,8 +2403,18 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2327
2403
  timeoutMs: data.timeoutMs,
2328
2404
  deadlineAt: data.deadlineAt,
2329
2405
  turnBudget: data.turnBudget,
2330
- onDetachedExit: (index, result) => updateRememberedForegroundChild(deps.state, { runId, mode: "chain", cwd: effectiveCwd, sessionId: data.parentSessionId, index, result, events: deps.pi.events }),
2406
+ onDetachedExit: (index, result) => {
2407
+ try {
2408
+ updateRememberedForegroundChild(deps.state, { runId, mode: "chain", cwd: effectiveCwd, sessionId: data.parentSessionId, index, result, events: deps.pi.events });
2409
+ } finally {
2410
+ removeForegroundControlIfIdle(deps.state, runId);
2411
+ }
2412
+ },
2413
+ onForegroundChildSettled: () => {
2414
+ removeForegroundControlIfIdle(deps.state, runId);
2415
+ },
2331
2416
  toolBudget: data.toolBudget,
2417
+ usageBudget: data.usageBudget,
2332
2418
  configToolBudget: data.configToolBudget,
2333
2419
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2334
2420
  capabilityCeiling: data.capabilityCeiling,
@@ -2388,6 +2474,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2388
2474
  timeoutMs: data.timeoutMs,
2389
2475
  turnBudget: data.turnBudget,
2390
2476
  toolBudget: data.toolBudget,
2477
+ usageBudget: data.usageBudget,
2391
2478
  configToolBudget: data.configToolBudget,
2392
2479
  capabilityCeiling: data.capabilityCeiling,
2393
2480
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
@@ -2399,9 +2486,10 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2399
2486
  updateForegroundNestedProjection(foregroundControl);
2400
2487
  attachRootChildrenToSteps(runId, rawChainDetails.results, foregroundControl.nestedChildren);
2401
2488
  rawChainDetails.totalCost = sumResultsCost(rawChainDetails.results);
2489
+ rawChainDetails.usageBudget = usageBudgetState(data.usageBudget, rawChainDetails.totalCost);
2402
2490
  }
2403
2491
  const chainDetails = rawChainDetails ? compactForegroundDetails(rawChainDetails) : undefined;
2404
- if (chainDetails) rememberForegroundRun(deps.state, { runId, mode: "chain", cwd: effectiveCwd, sessionId: data.parentSessionId, results: chainDetails.results });
2492
+ if (chainDetails) rememberForegroundRun(deps.state, { runId, mode: "chain", cwd: effectiveCwd, sessionId: data.parentSessionId, results: chainDetails.results, checkpoint: chainDetails.checkpoint });
2405
2493
  const intercomReceipt = chainDetails && !chainDetails.results.some((result) => result.interrupted || result.detached)
2406
2494
  ? await maybeBuildForegroundIntercomReceipt({
2407
2495
  pi: deps.pi,
@@ -2469,6 +2557,7 @@ interface ForegroundParallelRunInput {
2469
2557
  timeoutMs?: number;
2470
2558
  deadlineAt?: number;
2471
2559
  turnBudget?: ResolvedTurnBudget;
2560
+ usageBudget?: UsageBudgetConfig;
2472
2561
  toolBudgets: (ResolvedToolBudget | undefined)[];
2473
2562
  agentContract?: AgentContract;
2474
2563
  }
@@ -2575,6 +2664,10 @@ function finalizeParallelWorktreeHandoff(input: {
2575
2664
  interrupted: result.interrupted,
2576
2665
  detached: result.detached,
2577
2666
  state: result.stopped ? "stopped" : undefined,
2667
+ processSignal: result.processSignal,
2668
+ timedOut: result.timedOut,
2669
+ stopped: result.stopped,
2670
+ turnBudgetExceeded: result.turnBudgetExceeded,
2578
2671
  }),
2579
2672
  summary: resultSummaryForIntercom(result),
2580
2673
  ...(result.artifactPaths?.outputPath ? { outputPath: result.artifactPaths.outputPath } : {}),
@@ -2623,7 +2716,22 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
2623
2716
  for (let i = 0; i < input.tasks.length; i++) {
2624
2717
  input.sessionFileForTask(input.tasks[i]!.agent, i, input.modelOverrides[i]);
2625
2718
  }
2719
+ const completedResults: SingleResult[] = [];
2720
+ if (input.foregroundControl) retainForegroundSchedulingOwner(input.foregroundControl);
2626
2721
  return mapConcurrent(input.tasks, input.concurrencyLimit, async (task, index) => {
2722
+ const budgetState = usageBudgetState(input.usageBudget, sumResultsCost(completedResults));
2723
+ if (budgetState?.exhausted) {
2724
+ return {
2725
+ index,
2726
+ agent: task.agent,
2727
+ task: input.taskTexts[index] ?? "(skipped)",
2728
+ exitCode: 1,
2729
+ messages: [],
2730
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
2731
+ error: usageBudgetExceededMessage(budgetState),
2732
+ skipped: true,
2733
+ } as SingleResult;
2734
+ }
2627
2735
  const behavior = input.behaviors[index];
2628
2736
  const effectiveSkills = behavior?.skills;
2629
2737
  const taskCwd = resolveParallelTaskCwd(task, input.paramsCwd, input.worktreeSetup, index);
@@ -2642,10 +2750,14 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
2642
2750
  );
2643
2751
  const interruptController = new AbortController();
2644
2752
  if (input.foregroundControl) {
2753
+ const model = input.modelOverrides[index];
2754
+ const thinking = resolveEffectiveThinking(model, input.thinkingOverrideForTask(task.agent, index, model));
2645
2755
  beginForegroundChild(input.foregroundControl, {
2646
2756
  index,
2647
2757
  agent: task.agent,
2648
2758
  description: input.taskDescriptions[index],
2759
+ ...(model ? { model } : {}),
2760
+ ...(thinking ? { thinking } : {}),
2649
2761
  interrupt: () => {
2650
2762
  if (interruptController.signal.aborted) return false;
2651
2763
  interruptController.abort();
@@ -2656,7 +2768,8 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
2656
2768
  const structuredRuntime = task.outputSchema
2657
2769
  ? createStructuredOutputRuntime(task.outputSchema, path.join(input.artifactsDir, "structured-output", input.runId))
2658
2770
  : undefined;
2659
- return runSync(input.ctx.cwd, input.agents, task.agent, taskText, {
2771
+ let detachedReceipt = false;
2772
+ const result = await runSync(input.ctx.cwd, input.agents, task.agent, taskText, {
2660
2773
  parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
2661
2774
  context: input.contextPolicy.contextForAgent(task.agent),
2662
2775
  cwd: taskCwd,
@@ -2679,7 +2792,17 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
2679
2792
  capabilityCeiling: input.capabilityCeiling,
2680
2793
  controlConfig: input.controlConfig,
2681
2794
  onControlEvent: input.onControlEvent,
2682
- onDetachedExit: (result) => updateRememberedForegroundChild(input.state, { runId: input.runId, mode: "parallel", cwd: taskCwd, sessionId: input.parentSessionId, index, result, events: input.intercomEvents }),
2795
+ onDetachedExit: (result) => {
2796
+ try {
2797
+ updateRememberedForegroundChild(input.state, { runId: input.runId, mode: "parallel", cwd: taskCwd, sessionId: input.parentSessionId, index, result, events: input.intercomEvents });
2798
+ } finally {
2799
+ try {
2800
+ if (input.foregroundControl) finishForegroundChild(input.foregroundControl, index);
2801
+ } finally {
2802
+ removeForegroundControlIfIdle(input.state, input.runId);
2803
+ }
2804
+ }
2805
+ },
2683
2806
  intercomSessionName: input.childIntercomTarget?.(task.agent, index),
2684
2807
  orchestratorIntercomTarget: input.orchestratorIntercomTarget,
2685
2808
  nestedRoute: input.foregroundControl?.nestedRoute,
@@ -2720,10 +2843,24 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
2720
2843
  });
2721
2844
  }
2722
2845
  : undefined,
2846
+ }).then((result) => {
2847
+ detachedReceipt = result.detached === true;
2848
+ return result;
2723
2849
  }).finally(() => {
2724
- if (input.foregroundControl) finishForegroundChild(input.foregroundControl, index);
2850
+ // mapConcurrent rejects before siblings settle, so every attached child
2851
+ // attempts idle removal after releasing its own control. Detached receipts
2852
+ // transfer both responsibilities to the authoritative exit callback.
2853
+ if (!detachedReceipt) {
2854
+ if (input.foregroundControl) finishForegroundChild(input.foregroundControl, index);
2855
+ removeForegroundControlIfIdle(input.state, input.runId);
2856
+ }
2725
2857
  });
2726
- }, input.globalSemaphore);
2858
+ completedResults.push(result);
2859
+ return result;
2860
+ }, input.globalSemaphore, () => {
2861
+ if (input.foregroundControl) settleForegroundSchedulingOwner(input.foregroundControl);
2862
+ removeForegroundControlIfIdle(input.state, input.runId);
2863
+ });
2727
2864
  }
2728
2865
 
2729
2866
  async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps): Promise<AgentToolResult<Details>> {
@@ -2921,6 +3058,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2921
3058
  nestedRoute: data.nestedRoute,
2922
3059
  timeoutMs: data.timeoutMs,
2923
3060
  turnBudget: data.turnBudget,
3061
+ usageBudget: data.usageBudget,
2924
3062
  toolBudget: data.toolBudget,
2925
3063
  configToolBudget: data.configToolBudget,
2926
3064
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
@@ -3025,6 +3163,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3025
3163
  timeoutMs: data.timeoutMs,
3026
3164
  deadlineAt,
3027
3165
  turnBudget: data.turnBudget,
3166
+ usageBudget: data.usageBudget,
3028
3167
  toolBudgets,
3029
3168
  agentContract: params.agentContract,
3030
3169
  });
@@ -3048,6 +3187,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3048
3187
  handoff = finalizeParallelWorktreeHandoff({ worktreeSetup, artifactsDir, runId, cwd: effectiveCwd, tasks, results });
3049
3188
  }
3050
3189
  const interrupted = results.find((result) => result.interrupted);
3190
+ const totalCost = sumResultsCost(results);
3051
3191
  const details = compactForegroundDetails({
3052
3192
  mode: "parallel",
3053
3193
  runId,
@@ -3056,7 +3196,8 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3056
3196
  progress: params.includeProgress ? allProgress : undefined,
3057
3197
  artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
3058
3198
  totalChildUsage: sumResultsUsage(results),
3059
- totalCost: sumResultsCost(results),
3199
+ totalCost,
3200
+ usageBudget: usageBudgetState(data.usageBudget, totalCost),
3060
3201
  ...(handoff?.reference ? { parallelHandoff: handoff.reference } : {}),
3061
3202
  });
3062
3203
  rememberForegroundRun(deps.state, { runId, mode: "parallel", cwd: effectiveCwd, sessionId: data.parentSessionId, results: details.results });
@@ -3259,6 +3400,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3259
3400
  timeoutMs: data.timeoutMs,
3260
3401
  turnBudget: data.turnBudget,
3261
3402
  toolBudget: effectiveToolBudget.toolBudget,
3403
+ usageBudget: data.usageBudget,
3262
3404
  allowZeroToolBudget: data.allowZeroToolBudget && effectiveToolBudget.toolBudget === data.toolBudget,
3263
3405
  });
3264
3406
  }
@@ -3285,17 +3427,22 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3285
3427
  effectiveSkills = skillOverride;
3286
3428
  }
3287
3429
  const interruptController = new AbortController();
3430
+ let detachForeground: ((reason?: string) => boolean) | undefined;
3288
3431
  const foregroundControl = deps.state.foregroundControls.get(runId);
3289
3432
  if (foregroundControl) {
3433
+ const thinking = resolveEffectiveThinking(modelOverride, thinkingOverrideForTask(params.agent!, 0, modelOverride));
3290
3434
  beginForegroundChild(foregroundControl, {
3291
3435
  index: 0,
3292
3436
  agent: params.agent!,
3293
3437
  description: foregroundControl.description,
3438
+ ...(modelOverride ? { model: modelOverride } : {}),
3439
+ ...(thinking ? { thinking } : {}),
3294
3440
  interrupt: () => {
3295
3441
  if (interruptController.signal.aborted) return false;
3296
3442
  interruptController.abort();
3297
3443
  return true;
3298
3444
  },
3445
+ detach: () => detachForeground?.("user request") === true,
3299
3446
  });
3300
3447
  }
3301
3448
 
@@ -3345,7 +3492,29 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3345
3492
  agentContract: params.agentContract,
3346
3493
  acceptance: params.acceptance,
3347
3494
  acceptanceContext: { mode: "single" },
3348
- onDetachedExit: (result) => updateRememberedForegroundChild(deps.state, { runId, mode: "single", cwd: effectiveCwd, sessionId: data.parentSessionId, index: 0, result, events: deps.pi.events }),
3495
+ onDetachReady: (detach) => {
3496
+ detachForeground = detach;
3497
+ },
3498
+ onDetachedExit: (result) => {
3499
+ try {
3500
+ try {
3501
+ updateRememberedForegroundChild(deps.state, { runId, mode: "single", cwd: effectiveCwd, sessionId: data.parentSessionId, index: 0, result, events: deps.pi.events });
3502
+ } catch {
3503
+ // Remembered foreground state is best-effort; run history and cleanup must still complete.
3504
+ }
3505
+ } finally {
3506
+ try {
3507
+ if (!artifactConfig.enabled) cleanupStructuredOutputRuntime(structuredRuntime);
3508
+ } finally {
3509
+ try {
3510
+ if (foregroundControl) finishForegroundChild(foregroundControl, 0);
3511
+ } finally {
3512
+ removeForegroundControlIfIdle(deps.state, runId);
3513
+ }
3514
+ }
3515
+ }
3516
+ recordRun(params.agent!, cleanTask, result.exitCode, result.progressSummary?.durationMs ?? 0);
3517
+ },
3349
3518
  timeoutMs: data.timeoutMs,
3350
3519
  deadlineAt,
3351
3520
  turnBudget: data.turnBudget,
@@ -3355,10 +3524,17 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3355
3524
  allowZeroToolBudget: data.allowZeroToolBudget && effectiveToolBudget.toolBudget === data.toolBudget,
3356
3525
  });
3357
3526
  } finally {
3358
- if (!artifactConfig.enabled) cleanupStructuredOutputRuntime(structuredRuntime);
3527
+ // An attached runSync rejection still owns its child and structured runtime.
3528
+ // A successful detached receipt transfers both to onDetachedExit while the
3529
+ // authoritative completion remains live.
3530
+ if (!r?.detached) {
3531
+ if (!artifactConfig.enabled) cleanupStructuredOutputRuntime(structuredRuntime);
3532
+ if (foregroundControl) finishForegroundChild(foregroundControl, 0);
3533
+ }
3534
+ }
3535
+ if (!r.detached) {
3536
+ recordRun(params.agent!, cleanTask, r.exitCode, r.progressSummary?.durationMs ?? 0);
3359
3537
  }
3360
- if (foregroundControl) finishForegroundChild(foregroundControl, 0);
3361
- recordRun(params.agent!, cleanTask, r.exitCode, r.progressSummary?.durationMs ?? 0);
3362
3538
 
3363
3539
  if (r.progress) allProgress.push(r.progress);
3364
3540
  if (r.artifactPaths) allArtifactPaths.push(r.artifactPaths);
@@ -3378,6 +3554,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3378
3554
  updateForegroundNestedProjection(foregroundControl);
3379
3555
  attachRootChildrenToSteps(runId, [r], foregroundControl.nestedChildren);
3380
3556
  }
3557
+ const totalCost = sumResultsCost([r]);
3381
3558
  const details = compactForegroundDetails({
3382
3559
  mode: "single",
3383
3560
  runId,
@@ -3389,7 +3566,8 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3389
3566
  artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
3390
3567
  truncation: r.truncation,
3391
3568
  totalChildUsage: sumResultsUsage([r]),
3392
- totalCost: sumResultsCost([r]),
3569
+ totalCost,
3570
+ usageBudget: usageBudgetState(data.usageBudget, totalCost),
3393
3571
  });
3394
3572
  rememberForegroundRun(deps.state, { runId, mode: "single", cwd: effectiveCwd, sessionId: data.parentSessionId, results: details.results });
3395
3573
 
@@ -3673,6 +3851,59 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3673
3851
  }
3674
3852
  return withBudget(inspectSubagentStatus(paramsWithResolvedCwd, { state: deps.state, nested: nestedScope, sessionRoots }));
3675
3853
  }
3854
+
3855
+ if (action === "approve-checkpoint" || action === "reject-checkpoint") {
3856
+ deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
3857
+ const targetRunId = paramsWithResolvedCwd.runId ?? paramsWithResolvedCwd.id;
3858
+ if (!targetRunId && !paramsWithResolvedCwd.dir) {
3859
+ return { content: [{ type: "text", text: `action='${action}' requires id or dir.` }], isError: true, details: { mode: "management", results: [] } };
3860
+ }
3861
+ try {
3862
+ const resolved = targetRunId ? resolveSubagentRunId(targetRunId, { state: deps.state, nested: nestedResolutionScopeForExecutor(deps) }) : undefined;
3863
+ const decision = action === "approve-checkpoint" ? "approved" : "rejected";
3864
+ if (resolved?.kind === "foreground") {
3865
+ const run = deps.state.foregroundRuns?.get(resolved.id);
3866
+ if (!run?.checkpoint || run.checkpoint.status !== "pending") {
3867
+ return { content: [{ type: "text", text: `Run '${resolved.id}' is not paused at an approval checkpoint.` }], isError: true, details: { mode: "management", results: [] } };
3868
+ }
3869
+ if (deps.state.currentSessionId && run.sessionId !== deps.state.currentSessionId) {
3870
+ return { content: [{ type: "text", text: `Run '${resolved.id}' was not found in the active session.` }], isError: true, details: { mode: "management", results: [] } };
3871
+ }
3872
+ const decidedAt = Date.now();
3873
+ const checkpoint = { ...run.checkpoint, status: decision, ...(decision === "approved" ? { approvedAt: decidedAt } : { rejectedAt: decidedAt }) };
3874
+ run.checkpoint = checkpoint;
3875
+ run.updatedAt = decidedAt;
3876
+ return {
3877
+ content: [{ type: "text", text: `Checkpoint '${checkpoint.name}' ${decision} for foreground run ${resolved.id}.` }],
3878
+ details: { mode: "management", results: [], checkpoint },
3879
+ };
3880
+ }
3881
+ const location = paramsWithResolvedCwd.dir
3882
+ ? resolveAsyncRunLocation(paramsWithResolvedCwd, ASYNC_DIR, RESULTS_DIR)
3883
+ : resolved?.kind === "async"
3884
+ ? resolved.location
3885
+ : undefined;
3886
+ if (!location?.asyncDir) {
3887
+ return { content: [{ type: "text", text: `action='${action}' targets paused foreground or async checkpoints. No matching run found.` }], isError: true, details: { mode: "management", results: [] } };
3888
+ }
3889
+ const status = readStatus(location.asyncDir);
3890
+ const runId = status?.runId ?? location.resolvedId ?? targetRunId ?? path.basename(location.asyncDir);
3891
+ if (!status?.checkpoint || status.state !== "paused") {
3892
+ return { content: [{ type: "text", text: `Run '${runId}' is not paused at an approval checkpoint.` }], isError: true, details: { mode: "management", results: [] } };
3893
+ }
3894
+ if (deps.state.currentSessionId && status.sessionId !== deps.state.currentSessionId) {
3895
+ return { content: [{ type: "text", text: `Run '${runId}' was not found in the active session.` }], isError: true, details: { mode: "management", results: [] } };
3896
+ }
3897
+ deliverCheckpointDecisionRequest({ asyncDir: location.asyncDir, decision, source: "subagent-action", ...(paramsWithResolvedCwd.message ? { reason: paramsWithResolvedCwd.message } : {}) });
3898
+ return {
3899
+ content: [{ type: "text", text: `Checkpoint '${status.checkpoint.name}' ${decision} for run ${runId}.` }],
3900
+ details: { mode: "management", results: [], checkpoint: { ...status.checkpoint, status: decision } },
3901
+ };
3902
+ } catch (error) {
3903
+ const text = error instanceof Error ? error.message : String(error);
3904
+ return { content: [{ type: "text", text }], isError: true, details: { mode: "management", results: [] } };
3905
+ }
3906
+ }
3676
3907
  if (action === "resume") {
3677
3908
  return resumeAsyncRun({ params: paramsWithResolvedCwd, requestCwd, ctx, deps, parentModel: requestParentModel });
3678
3909
  }
@@ -3848,6 +4079,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3848
4079
  ...ctx,
3849
4080
  cwd: requestCwd,
3850
4081
  config: deps.config,
4082
+ currentSessionId: deps.state.currentSessionId ?? ctx.sessionManager.getSessionId() ?? undefined,
3851
4083
  });
3852
4084
  }
3853
4085
 
@@ -3885,12 +4117,17 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3885
4117
  if (runToolBudget.error) return buildRequestedModeError(effectiveParams, runToolBudget.error);
3886
4118
  const configToolBudget = resolveToolBudget(deps.config.toolBudget, "config.toolBudget");
3887
4119
  if (configToolBudget.error) return buildRequestedModeError(effectiveParams, configToolBudget.error);
4120
+ const usageBudget = validateUsageBudgetConfig(effectiveParams.usageBudget ?? deps.config.usageBudget, effectiveParams.usageBudget ? "usageBudget" : "config.usageBudget");
4121
+ if (usageBudget.error) return buildRequestedModeError(effectiveParams, usageBudget.error);
3888
4122
 
3889
4123
  const scope: AgentScope = resolveExecutionAgentScope(effectiveParams.agentScope);
3890
4124
  const effectiveCwd = effectiveParams.cwd ?? ctx.cwd;
3891
4125
  const parentSessionFile = ctx.sessionManager.getSessionFile() ?? null;
3892
4126
  const discovered = deps.discoverAgents(effectiveCwd, scope);
3893
4127
  const discoveredAgents = discovered.agents;
4128
+ const canonicalParams = canonicalizeExecutionParams(effectiveParams, discoveredAgents);
4129
+ if (canonicalParams.error) return buildRequestedModeError(effectiveParams, canonicalParams.error);
4130
+ effectiveParams = canonicalParams.params!;
3894
4131
  const modelScope = discovered.modelScope;
3895
4132
  effectiveParams = applySingleAgentLaunchDefaults(effectiveParams, discoveredAgents);
3896
4133
  const turnBudget = resolveTurnBudgetConfig(effectiveParams.turnBudget ?? deps.config.turnBudget);
@@ -4079,6 +4316,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4079
4316
  timeoutMs: foregroundTimeout.timeoutMs,
4080
4317
  turnBudget: turnBudget.turnBudget,
4081
4318
  toolBudget: runToolBudget.toolBudget,
4319
+ usageBudget: usageBudget.budget,
4082
4320
  allowZeroToolBudget,
4083
4321
  configToolBudget: configToolBudget.toolBudget,
4084
4322
  contextPolicy,
@@ -4105,6 +4343,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4105
4343
  description: foregroundDescription,
4106
4344
  currentActivityState: undefined,
4107
4345
  activeChildren: new Map(),
4346
+ // The outer executor owns scheduling until its finally block settles.
4347
+ schedulingOwners: 1,
4108
4348
  nestedRoute,
4109
4349
  interrupt: undefined,
4110
4350
  };
@@ -4202,11 +4442,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4202
4442
  return errorResult;
4203
4443
  } finally {
4204
4444
  if (foregroundControl) {
4205
- clearPendingForegroundControlNotices(deps.state, runId);
4206
- deps.state.foregroundControls.delete(runId);
4207
- if (deps.state.lastForegroundControlId === runId) {
4208
- deps.state.lastForegroundControlId = null;
4209
- }
4445
+ settleForegroundSchedulingOwner(foregroundControl);
4446
+ removeForegroundControlIfIdle(deps.state, runId);
4210
4447
  }
4211
4448
  }
4212
4449