pi-subagents 0.24.4 → 0.27.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 (48) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +145 -27
  3. package/package.json +1 -1
  4. package/prompts/parallel-context-build.md +3 -1
  5. package/prompts/parallel-handoff-plan.md +3 -1
  6. package/prompts/review-loop.md +1 -1
  7. package/skills/pi-subagents/SKILL.md +71 -20
  8. package/src/agents/agent-management.ts +57 -15
  9. package/src/agents/agent-serializer.ts +3 -2
  10. package/src/agents/agents.ts +47 -16
  11. package/src/agents/chain-serializer.ts +120 -0
  12. package/src/extension/fanout-child.ts +171 -0
  13. package/src/extension/index.ts +7 -2
  14. package/src/extension/schemas.ts +138 -5
  15. package/src/intercom/result-intercom.ts +108 -0
  16. package/src/runs/background/async-execution.ts +185 -10
  17. package/src/runs/background/async-job-tracker.ts +41 -6
  18. package/src/runs/background/async-resume.ts +28 -15
  19. package/src/runs/background/async-status.ts +71 -31
  20. package/src/runs/background/result-watcher.ts +111 -54
  21. package/src/runs/background/run-id-resolver.ts +83 -0
  22. package/src/runs/background/run-status.ts +89 -4
  23. package/src/runs/background/stale-run-reconciler.ts +46 -1
  24. package/src/runs/background/subagent-runner.ts +648 -42
  25. package/src/runs/foreground/chain-execution.ts +331 -118
  26. package/src/runs/foreground/execution.ts +226 -10
  27. package/src/runs/foreground/subagent-executor.ts +377 -14
  28. package/src/runs/shared/acceptance-contract.ts +291 -0
  29. package/src/runs/shared/acceptance-evaluation.ts +221 -0
  30. package/src/runs/shared/acceptance-finalization.ts +161 -0
  31. package/src/runs/shared/acceptance-reports.ts +127 -0
  32. package/src/runs/shared/acceptance.ts +22 -0
  33. package/src/runs/shared/chain-outputs.ts +101 -0
  34. package/src/runs/shared/completion-guard.ts +26 -3
  35. package/src/runs/shared/dynamic-fanout.ts +293 -0
  36. package/src/runs/shared/nested-events.ts +819 -0
  37. package/src/runs/shared/nested-path.ts +52 -0
  38. package/src/runs/shared/nested-render.ts +115 -0
  39. package/src/runs/shared/parallel-utils.ts +31 -1
  40. package/src/runs/shared/pi-args.ts +73 -5
  41. package/src/runs/shared/structured-output.ts +77 -0
  42. package/src/runs/shared/subagent-prompt-runtime.ts +77 -7
  43. package/src/runs/shared/workflow-graph.ts +206 -0
  44. package/src/shared/formatters.ts +2 -2
  45. package/src/shared/settings.ts +53 -4
  46. package/src/shared/types.ts +345 -0
  47. package/src/slash/slash-commands.ts +41 -3
  48. package/src/tui/render.ts +268 -43
@@ -21,6 +21,7 @@ import {
21
21
  writeInitialProgressFile,
22
22
  getStepAgents,
23
23
  isParallelStep,
24
+ isDynamicParallelStep,
24
25
  resolveStepBehavior,
25
26
  suppressProgressForReadOnlyTask,
26
27
  taskDisallowsFileUpdates,
@@ -38,6 +39,7 @@ import { formatControlIntercomMessage, formatControlNoticeMessage, resolveContro
38
39
  import { finalizeSingleOutput, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
39
40
  import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, readStatus, resolveChildCwd } from "../../shared/utils.ts";
40
41
  import {
42
+ attachNestedChildrenToResultChildren,
41
43
  buildSubagentResultIntercomPayload,
42
44
  deliverSubagentIntercomMessageEvent,
43
45
  deliverSubagentResultIntercomEvent,
@@ -46,8 +48,12 @@ import {
46
48
  stripDetailsOutputsForIntercomReceipt,
47
49
  } from "../../intercom/result-intercom.ts";
48
50
  import { buildRevivedAsyncTask, resolveAsyncResumeTarget } from "../background/async-resume.ts";
51
+ import { createNestedRoute, readNestedControlResults, resolveInheritedNestedRouteFromEnv, resolveNestedAsyncDir, resolveNestedParentAddressFromEnv, updateForegroundNestedProjection, writeNestedControlRequest, writeNestedEvent, type NestedRunResolutionScope } from "../shared/nested-events.ts";
52
+ import { resolveSubagentRunId, type ResolvedSubagentRunId } from "../background/run-id-resolver.ts";
53
+ import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
49
54
  import { inspectSubagentStatus } from "../background/run-status.ts";
50
55
  import { applyForceTopLevelAsyncOverride } from "../background/top-level-async.ts";
56
+ import { validateAcceptanceInput } from "../shared/acceptance.ts";
51
57
  import {
52
58
  cleanupWorktrees,
53
59
  createWorktrees,
@@ -59,6 +65,7 @@ import {
59
65
  } from "../shared/worktree.ts";
60
66
  import {
61
67
  type AgentProgress,
68
+ type AcceptanceInput,
62
69
  type ArtifactConfig,
63
70
  type ArtifactPaths,
64
71
  type ControlConfig,
@@ -67,6 +74,8 @@ import {
67
74
  type ExtensionConfig,
68
75
  type IntercomEventBus,
69
76
  type MaxOutputConfig,
77
+ type NestedRouteInfo,
78
+ type NestedRunSummary,
70
79
  type ResolvedControlConfig,
71
80
  type SingleResult,
72
81
  type SubagentRunMode,
@@ -84,6 +93,7 @@ import {
84
93
  } from "../../shared/types.ts";
85
94
 
86
95
  const ASYNC_INTERRUPT_SIGNAL: NodeJS.Signals = process.platform === "win32" ? "SIGBREAK" : "SIGUSR2";
96
+ const MUTATING_MANAGEMENT_ACTIONS = new Set(["create", "update", "delete"]);
87
97
 
88
98
  interface TaskParam {
89
99
  agent: string;
@@ -96,6 +106,7 @@ interface TaskParam {
96
106
  progress?: boolean;
97
107
  model?: string;
98
108
  skill?: string | string[] | boolean;
109
+ acceptance?: AcceptanceInput;
99
110
  }
100
111
 
101
112
  export interface SubagentParamsLike {
@@ -127,6 +138,7 @@ export interface SubagentParamsLike {
127
138
  outputMode?: "inline" | "file-only";
128
139
  agentScope?: unknown;
129
140
  chainDir?: string;
141
+ acceptance?: AcceptanceInput;
130
142
  }
131
143
 
132
144
  interface ExecutorDeps {
@@ -138,6 +150,7 @@ interface ExecutorDeps {
138
150
  getSubagentSessionRoot: (parentSessionFile: string | null) => string;
139
151
  expandTilde: (p: string) => string;
140
152
  discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[] };
153
+ allowMutatingManagementActions?: boolean;
141
154
  }
142
155
 
143
156
  interface ExecutionContextData {
@@ -158,6 +171,7 @@ interface ExecutionContextData {
158
171
  effectiveAsync: boolean;
159
172
  controlConfig: ResolvedControlConfig;
160
173
  intercomBridge: IntercomBridgeState;
174
+ nestedRoute?: NestedRouteInfo;
161
175
  }
162
176
 
163
177
  function resolveRequestedCwd(runtimeCwd: string, requestedCwd: string | undefined): string {
@@ -196,7 +210,23 @@ function formatForegroundActivity(control: SubagentState["foregroundControls"] e
196
210
  return [`active ${seconds}s ago`, ...facts].join(" | ");
197
211
  }
198
212
 
213
+ function nestedResolutionScopeForExecutor(deps: ExecutorDeps): NestedRunResolutionScope | undefined {
214
+ if (deps.allowMutatingManagementActions !== false) return undefined;
215
+ const route = resolveInheritedNestedRouteFromEnv();
216
+ const address = route ? resolveNestedParentAddressFromEnv() : undefined;
217
+ return {
218
+ routes: route ? [route] : [],
219
+ ...(address ? { descendantOf: { parentRunId: address.parentRunId, ...(address.parentStepIndex !== undefined ? { parentStepIndex: address.parentStepIndex } : {}) } } : {}),
220
+ };
221
+ }
222
+
199
223
  function foregroundStatusResult(control: SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never): AgentToolResult<Details> {
224
+ let nestedWarning: string | undefined;
225
+ try {
226
+ updateForegroundNestedProjection(control);
227
+ } catch (error) {
228
+ nestedWarning = `Nested status unavailable: ${error instanceof Error ? error.message : String(error)}`;
229
+ }
200
230
  const activity = formatForegroundActivity(control);
201
231
  const lines = [
202
232
  `Run: ${control.runId}`,
@@ -205,6 +235,8 @@ function foregroundStatusResult(control: SubagentState["foregroundControls"] ext
205
235
  control.currentAgent ? `Current: ${control.currentAgent}${control.currentIndex !== undefined ? ` step ${control.currentIndex + 1}` : ""}` : undefined,
206
236
  activity ? `Activity: ${activity}` : undefined,
207
237
  ].filter((line): line is string => Boolean(line));
238
+ lines.push(...formatNestedRunStatusLines(control.nestedChildren, { indent: "", commandHints: true, maxLines: 20 }));
239
+ if (nestedWarning) lines.push(`Warning: ${nestedWarning}`);
208
240
  return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "management", results: [] } };
209
241
  }
210
242
 
@@ -252,7 +284,18 @@ function resolveForegroundResumeTarget(params: SubagentParamsLike, state: Subage
252
284
 
253
285
  type AsyncResumeSourceTarget = ReturnType<typeof resolveAsyncResumeTarget> & { source: "async" };
254
286
  type ForegroundResumeSourceTarget = NonNullable<ReturnType<typeof resolveForegroundResumeTarget>> & { kind: "revive"; source: "foreground" };
255
- type ResumeSourceTarget = AsyncResumeSourceTarget | ForegroundResumeSourceTarget;
287
+ type NestedResumeSourceTarget = {
288
+ kind: "revive";
289
+ source: "nested";
290
+ runId: string;
291
+ state: "complete" | "failed" | "paused";
292
+ agent: string;
293
+ index: number;
294
+ intercomTarget: string;
295
+ cwd?: string;
296
+ sessionFile: string;
297
+ };
298
+ type ResumeSourceTarget = AsyncResumeSourceTarget | ForegroundResumeSourceTarget | NestedResumeSourceTarget;
256
299
 
257
300
  function isAsyncRunNotFound(error: unknown): boolean {
258
301
  return error instanceof Error && error.message.startsWith("Async run not found.");
@@ -392,6 +435,119 @@ function interruptAsyncRun(state: SubagentState, runId: string | undefined): Age
392
435
  }
393
436
  }
394
437
 
438
+ function nestedRunSessionFile(run: NestedRunSummary): string | undefined {
439
+ return run.sessionFile ?? (run.steps?.length === 1 ? run.steps[0]?.sessionFile : undefined);
440
+ }
441
+
442
+ function nestedRunAgent(run: NestedRunSummary): string | undefined {
443
+ return run.agent ?? run.agents?.[0] ?? (run.steps?.length === 1 ? run.steps[0]?.agent : undefined);
444
+ }
445
+
446
+ function pathWithin(base: string, candidate: string): boolean {
447
+ const resolvedBase = path.resolve(base);
448
+ const resolvedCandidate = path.resolve(candidate);
449
+ return resolvedCandidate === resolvedBase || resolvedCandidate.startsWith(`${resolvedBase}${path.sep}`);
450
+ }
451
+
452
+ function validateNestedSessionFile(run: NestedRunSummary, trustedSessionRoots: string[]): string {
453
+ const sessionFile = nestedRunSessionFile(run);
454
+ if (!sessionFile) throw new Error(`Nested run '${run.id}' does not have a persisted session file to resume from.`);
455
+ if (path.extname(sessionFile) !== ".jsonl") throw new Error(`Nested run '${run.id}' session file must be a .jsonl file: ${sessionFile}`);
456
+ const resolved = path.resolve(sessionFile);
457
+ if (!path.isAbsolute(sessionFile)) throw new Error(`Nested run '${run.id}' session file must be absolute: ${sessionFile}`);
458
+ if (!fs.existsSync(resolved)) throw new Error(`Nested run '${run.id}' session file does not exist: ${sessionFile}`);
459
+ const stat = fs.lstatSync(resolved);
460
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`Nested run '${run.id}' session file is not a regular file: ${sessionFile}`);
461
+ const realSessionFile = fs.realpathSync(resolved);
462
+ const trustedRoots = trustedSessionRoots
463
+ .filter((root) => fs.existsSync(root))
464
+ .map((root) => fs.realpathSync(root));
465
+ if (!trustedRoots.some((root) => pathWithin(root, realSessionFile))) {
466
+ throw new Error(`Nested run '${run.id}' session file is outside trusted nested session roots: ${sessionFile}`);
467
+ }
468
+ if (!realSessionFile.split(path.sep).includes(run.id)) {
469
+ throw new Error(`Nested run '${run.id}' session file is not under that nested run's session directory: ${sessionFile}`);
470
+ }
471
+ return realSessionFile;
472
+ }
473
+
474
+ function resolveNestedResumeTarget(match: ResolvedSubagentRunId & { kind: "nested" }, trustedSessionRoots: string[]): NestedResumeSourceTarget {
475
+ const run = match.match.run;
476
+ if (run.state === "running" || run.state === "queued") throw new Error(`Nested run '${run.id}' is live; route the follow-up to the owner process instead.`);
477
+ const agent = nestedRunAgent(run);
478
+ if (!agent) throw new Error(`Could not determine child agent for nested run '${run.id}'.`);
479
+ const state = run.state === "complete" || run.state === "failed" || run.state === "paused" ? run.state : "failed";
480
+ const asyncDir = resolveNestedAsyncDir(match.match.rootRunId, run);
481
+ return {
482
+ kind: "revive",
483
+ source: "nested",
484
+ runId: run.id,
485
+ state,
486
+ agent,
487
+ index: 0,
488
+ intercomTarget: resolveSubagentIntercomTarget(run.id, agent, 0),
489
+ cwd: asyncDir ? path.dirname(asyncDir) : undefined,
490
+ sessionFile: validateNestedSessionFile(run, trustedSessionRoots),
491
+ };
492
+ }
493
+
494
+ async function waitForNestedControlResult(target: ResolvedSubagentRunId & { kind: "nested" }, requestId: string, timeoutMs = 1_000) {
495
+ const deadline = Date.now() + timeoutMs;
496
+ while (Date.now() < deadline) {
497
+ const result = readNestedControlResults(target.match.route).find((candidate) => candidate.requestId === requestId && candidate.targetRunId === target.match.run.id);
498
+ if (result) return result;
499
+ await new Promise((resolve) => setTimeout(resolve, 50));
500
+ }
501
+ return undefined;
502
+ }
503
+
504
+ async function sendNestedControlRequest(target: ResolvedSubagentRunId & { kind: "nested" }, action: "interrupt" | "resume", message?: string) {
505
+ const requestId = randomUUID();
506
+ writeNestedControlRequest(target.match.route, {
507
+ ts: Date.now(),
508
+ requestId,
509
+ targetRunId: target.match.run.id,
510
+ action,
511
+ ...(message ? { message } : {}),
512
+ });
513
+ return waitForNestedControlResult(target, requestId);
514
+ }
515
+
516
+ function directNestedAsyncInterrupt(target: ResolvedSubagentRunId & { kind: "nested" }): AgentToolResult<Details> | undefined {
517
+ const run = target.match.run;
518
+ const asyncDir = resolveNestedAsyncDir(target.match.rootRunId, run);
519
+ if (!asyncDir) return undefined;
520
+ const status = readStatus(asyncDir);
521
+ const pid = typeof status?.pid === "number" && status.pid > 0 ? status.pid : run.pid;
522
+ if (!status || status.state !== "running" || typeof pid !== "number" || pid <= 0) return undefined;
523
+ try {
524
+ process.kill(pid, ASYNC_INTERRUPT_SIGNAL);
525
+ return { content: [{ type: "text", text: `Interrupt requested for nested async run ${run.id}.` }], details: { mode: "management", results: [] } };
526
+ } catch (error) {
527
+ const message = error instanceof Error ? error.message : String(error);
528
+ return { content: [{ type: "text", text: `Failed to interrupt nested async run ${run.id}: ${message}` }], isError: true, details: { mode: "management", results: [] } };
529
+ }
530
+ }
531
+
532
+ async function interruptNestedRun(target: ResolvedSubagentRunId & { kind: "nested" }): Promise<AgentToolResult<Details>> {
533
+ const run = target.match.run;
534
+ if (run.state === "complete") return { content: [{ type: "text", text: `Nested run ${run.id} is already complete and cannot be interrupted.` }], isError: true, details: { mode: "management", results: [] } };
535
+ if (run.state === "failed") return { content: [{ type: "text", text: `Nested run ${run.id} has failed and cannot be interrupted.` }], isError: true, details: { mode: "management", results: [] } };
536
+ if (run.state === "paused") return { content: [{ type: "text", text: `Nested run ${run.id} is already paused.` }], isError: true, details: { mode: "management", results: [] } };
537
+ const result = await sendNestedControlRequest(target, "interrupt");
538
+ if (result) return { content: [{ type: "text", text: result.message }], isError: result.ok ? undefined : true, details: { mode: "management", results: [] } };
539
+ const direct = directNestedAsyncInterrupt(target);
540
+ if (direct) return direct;
541
+ return { content: [{ type: "text", text: `Nested run ${run.id} owner is not reachable and no safe direct async interrupt fallback is available.` }], isError: true, details: { mode: "management", results: [] } };
542
+ }
543
+
544
+ async function resumeLiveNestedRun(input: { target: ResolvedSubagentRunId & { kind: "nested" }; message: string }): Promise<AgentToolResult<Details>> {
545
+ const run = input.target.match.run;
546
+ const result = await sendNestedControlRequest(input.target, "resume", input.message);
547
+ if (result) return { content: [{ type: "text", text: result.message }], isError: result.ok ? undefined : true, details: { mode: "management", results: [] } };
548
+ return { content: [{ type: "text", text: `Nested run ${run.id} appears live but its owner route is not reachable. Wait for completion, then retry action='resume'.` }], isError: true, details: { mode: "management", results: [] } };
549
+ }
550
+
395
551
  async function resumeAsyncRun(input: {
396
552
  params: SubagentParamsLike;
397
553
  requestCwd: string;
@@ -408,8 +564,22 @@ async function resumeAsyncRun(input: {
408
564
  }
409
565
 
410
566
  let target: ResumeSourceTarget;
567
+ const parentSessionFile = input.ctx.sessionManager.getSessionFile() ?? null;
411
568
  try {
412
- target = resolveResumeTarget(input.params, input.deps.state);
569
+ const requestedId = input.params.id ?? input.params.runId;
570
+ const resolved = requestedId ? resolveSubagentRunId(requestedId, { state: input.deps.state, nested: nestedResolutionScopeForExecutor(input.deps) }) : undefined;
571
+ if (resolved?.kind === "nested") {
572
+ if (resolved.match.run.state === "running" || resolved.match.run.state === "queued") {
573
+ return resumeLiveNestedRun({ target: resolved, message: followUp });
574
+ }
575
+ const trustedSessionRoots = [
576
+ ...(input.deps.config.defaultSessionDir ? [path.resolve(input.deps.expandTilde(input.deps.config.defaultSessionDir))] : []),
577
+ ...(parentSessionFile ? [input.deps.getSubagentSessionRoot(parentSessionFile)] : []),
578
+ ];
579
+ target = resolveNestedResumeTarget(resolved, trustedSessionRoots);
580
+ } else {
581
+ target = resolveResumeTarget(input.params, input.deps.state);
582
+ }
413
583
  } catch (error) {
414
584
  const message = error instanceof Error ? error.message : String(error);
415
585
  return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
@@ -445,7 +615,6 @@ async function resumeAsyncRun(input: {
445
615
  };
446
616
  }
447
617
 
448
- const parentSessionFile = input.ctx.sessionManager.getSessionFile() ?? null;
449
618
  input.deps.state.currentSessionId = resolveCurrentSessionId(input.ctx.sessionManager);
450
619
  const effectiveCwd = target.cwd ?? input.requestCwd;
451
620
  const scope: AgentScope = resolveExecutionAgentScope(input.params.agentScope);
@@ -501,7 +670,7 @@ async function resumeAsyncRun(input: {
501
670
 
502
671
  const revivedId = result.details.asyncId ?? runId;
503
672
  const revivedTarget = intercomBridge.active ? resolveSubagentIntercomTarget(revivedId, target.agent, 0) : undefined;
504
- const sourceLabel = target.source === "foreground" ? "foreground" : "async";
673
+ const sourceLabel = target.source;
505
674
  const lines = [
506
675
  `Revived ${sourceLabel} subagent from ${target.runId}.`,
507
676
  `Revived run: ${revivedId}`,
@@ -538,6 +707,7 @@ async function emitForegroundResultIntercom(input: {
538
707
  mode: SubagentRunMode;
539
708
  results: SingleResult[];
540
709
  chainSteps?: number;
710
+ nestedChildren?: NestedRunSummary[];
541
711
  }): Promise<ReturnType<typeof buildSubagentResultIntercomPayload> | null> {
542
712
  if (!input.intercomBridge.active || !input.intercomBridge.orchestratorTarget) return null;
543
713
  const children = input.results.flatMap((result, index) => result.detached ? [] : [{
@@ -559,7 +729,7 @@ async function emitForegroundResultIntercom(input: {
559
729
  runId: input.runId,
560
730
  mode: input.mode,
561
731
  source: "foreground",
562
- children,
732
+ children: attachNestedChildrenToResultChildren(input.runId, children, input.nestedChildren),
563
733
  ...(typeof input.chainSteps === "number" ? { chainSteps: input.chainSteps } : {}),
564
734
  });
565
735
  const delivered = await deliverSubagentResultIntercomEvent(input.pi.events, payload);
@@ -573,6 +743,7 @@ async function maybeBuildForegroundIntercomReceipt(input: {
573
743
  runId: string;
574
744
  mode: SubagentRunMode;
575
745
  details: Details;
746
+ nestedChildren?: NestedRunSummary[];
576
747
  }): Promise<{ text: string; details: Details } | null> {
577
748
  const payload = await emitForegroundResultIntercom({
578
749
  pi: input.pi,
@@ -581,6 +752,7 @@ async function maybeBuildForegroundIntercomReceipt(input: {
581
752
  mode: input.mode,
582
753
  results: input.details.results,
583
754
  ...(typeof input.details.totalSteps === "number" ? { chainSteps: input.details.totalSteps } : {}),
755
+ ...(input.nestedChildren?.length ? { nestedChildren: input.nestedChildren } : {}),
584
756
  });
585
757
  if (!payload) return null;
586
758
  return {
@@ -589,6 +761,36 @@ async function maybeBuildForegroundIntercomReceipt(input: {
589
761
  };
590
762
  }
591
763
 
764
+ function validationErrorResult(mode: Details["mode"], text: string): AgentToolResult<Details> {
765
+ return { content: [{ type: "text", text }], isError: true, details: { mode, results: [] } };
766
+ }
767
+
768
+ function validateAcceptanceForExecution(params: SubagentParamsLike): AgentToolResult<Details> | null {
769
+ const topLevelErrors = validateAcceptanceInput(params.acceptance);
770
+ if (topLevelErrors.length > 0) return validationErrorResult("single", topLevelErrors.join(" "));
771
+ for (const [index, task] of (params.tasks ?? []).entries()) {
772
+ const errors = validateAcceptanceInput(task.acceptance, `tasks[${index}].acceptance`);
773
+ if (errors.length > 0) return validationErrorResult("parallel", errors.join(" "));
774
+ }
775
+ for (const [stepIndex, step] of (params.chain ?? []).entries()) {
776
+ if (isParallelStep(step)) {
777
+ if (Object.hasOwn(step, "acceptance")) return validationErrorResult("chain", `chain[${stepIndex}].acceptance is not supported on static parallel groups; set acceptance on each parallel task.`);
778
+ for (const [taskIndex, task] of step.parallel.entries()) {
779
+ const errors = validateAcceptanceInput(task.acceptance, `chain[${stepIndex}].parallel[${taskIndex}].acceptance`);
780
+ if (errors.length > 0) return validationErrorResult("chain", errors.join(" "));
781
+ }
782
+ } else if (isDynamicParallelStep(step)) {
783
+ if (Object.hasOwn(step, "acceptance")) return validationErrorResult("chain", `chain[${stepIndex}].acceptance is not supported on dynamic fanout groups; set acceptance on chain[${stepIndex}].parallel.acceptance for each materialized child.`);
784
+ const errors = validateAcceptanceInput(step.parallel.acceptance, `chain[${stepIndex}].parallel.acceptance`);
785
+ if (errors.length > 0) return validationErrorResult("chain", errors.join(" "));
786
+ } else {
787
+ const stepErrors = validateAcceptanceInput(step.acceptance, `chain[${stepIndex}].acceptance`);
788
+ if (stepErrors.length > 0) return validationErrorResult("chain", stepErrors.join(" "));
789
+ }
790
+ }
791
+ return null;
792
+ }
793
+
592
794
  function validateExecutionInput(
593
795
  params: SubagentParamsLike,
594
796
  agents: AgentConfig[],
@@ -597,6 +799,9 @@ function validateExecutionInput(
597
799
  hasSingle: boolean,
598
800
  allowClarifyTaskPrompt: boolean,
599
801
  ): AgentToolResult<Details> | null {
802
+ const acceptanceError = validateAcceptanceForExecution(params);
803
+ if (acceptanceError) return acceptanceError;
804
+
600
805
  if (Number(hasChain) + Number(hasTasks) + Number(hasSingle) !== 1) {
601
806
  return {
602
807
  content: [
@@ -649,6 +854,12 @@ function validateExecutionInput(
649
854
  details: { mode: "chain" as const, results: [] },
650
855
  };
651
856
  }
857
+ } else if (isDynamicParallelStep(firstStep)) {
858
+ return {
859
+ content: [{ type: "text", text: "First step in chain cannot be dynamic fanout; expand.from requires a prior structured named output" }],
860
+ isError: true,
861
+ details: { mode: "chain" as const, results: [] },
862
+ };
652
863
  } else if (!(firstStep as SequentialStep).task && !params.task && !allowClarifyTaskPrompt) {
653
864
  return {
654
865
  content: [{ type: "text", text: "First step in chain must have a task" }],
@@ -810,6 +1021,10 @@ function collectChainSessionFiles(
810
1021
  }
811
1022
  continue;
812
1023
  }
1024
+ if (isDynamicParallelStep(step)) {
1025
+ sessionFiles.push(undefined);
1026
+ continue;
1027
+ }
813
1028
  sessionFiles.push(sessionFileForIndex(flatIndex));
814
1029
  flatIndex++;
815
1030
  }
@@ -828,6 +1043,15 @@ function wrapChainTasksForFork(chain: ChainStep[], context: SubagentParamsLike["
828
1043
  })),
829
1044
  };
830
1045
  }
1046
+ if (isDynamicParallelStep(step)) {
1047
+ return {
1048
+ ...step,
1049
+ parallel: {
1050
+ ...step.parallel,
1051
+ task: wrapForkTask(step.parallel.task ?? "{previous}"),
1052
+ },
1053
+ };
1054
+ }
831
1055
  const sequential = step as SequentialStep;
832
1056
  return {
833
1057
  ...sequential,
@@ -850,6 +1074,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
850
1074
  effectiveAsync,
851
1075
  controlConfig,
852
1076
  intercomBridge,
1077
+ nestedRoute,
853
1078
  } = data;
854
1079
  const hasChain = (params.chain?.length ?? 0) > 0;
855
1080
  const hasTasks = (params.tasks?.length ?? 0) > 0;
@@ -914,6 +1139,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
914
1139
  ...(task.outputMode !== undefined ? { outputMode: task.outputMode } : {}),
915
1140
  ...(task.reads !== undefined && task.reads !== true ? { reads: task.reads } : {}),
916
1141
  ...(task.progress !== undefined ? { progress: task.progress } : {}),
1142
+ ...(task.acceptance !== undefined ? { acceptance: task.acceptance } : {}),
917
1143
  }));
918
1144
  return executeAsyncChain(id, {
919
1145
  chain: [{
@@ -939,6 +1165,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
939
1165
  controlConfig,
940
1166
  controlIntercomTarget,
941
1167
  childIntercomTarget,
1168
+ nestedRoute,
942
1169
  });
943
1170
  }
944
1171
 
@@ -960,12 +1187,14 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
960
1187
  sessionRoot,
961
1188
  chainSkills,
962
1189
  sessionFilesByFlatIndex: collectChainSessionFiles(chain, sessionFileForIndex),
1190
+ dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
963
1191
  maxSubagentDepth: currentMaxSubagentDepth,
964
1192
  worktreeSetupHook: deps.config.worktreeSetupHook,
965
1193
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
966
1194
  controlConfig,
967
1195
  controlIntercomTarget,
968
1196
  childIntercomTarget,
1197
+ nestedRoute,
969
1198
  });
970
1199
  }
971
1200
 
@@ -1008,6 +1237,8 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1008
1237
  controlConfig,
1009
1238
  controlIntercomTarget,
1010
1239
  childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(agent, index) : undefined,
1240
+ nestedRoute,
1241
+ acceptance: params.acceptance,
1011
1242
  });
1012
1243
  }
1013
1244
 
@@ -1060,8 +1291,10 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1060
1291
  childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(runId, agent, index) : undefined,
1061
1292
  orchestratorIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
1062
1293
  foregroundControl,
1294
+ nestedRoute: foregroundControl?.nestedRoute,
1063
1295
  chainSkills,
1064
1296
  chainDir: params.chainDir,
1297
+ dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
1065
1298
  maxSubagentDepth: currentMaxSubagentDepth,
1066
1299
  worktreeSetupHook: deps.config.worktreeSetupHook,
1067
1300
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
@@ -1097,16 +1330,19 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1097
1330
  sessionRoot,
1098
1331
  chainSkills: chainResult.requestedAsync.chainSkills,
1099
1332
  sessionFilesByFlatIndex: collectChainSessionFiles(asyncChain, sessionFileForIndex),
1333
+ dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
1100
1334
  maxSubagentDepth: currentMaxSubagentDepth,
1101
1335
  worktreeSetupHook: deps.config.worktreeSetupHook,
1102
1336
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
1103
1337
  controlConfig,
1104
1338
  controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
1105
1339
  childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
1340
+ nestedRoute: data.nestedRoute,
1106
1341
  });
1107
1342
  }
1108
1343
 
1109
1344
  const chainDetails = chainResult.details ? compactForegroundDetails({ ...chainResult.details, runId }) : undefined;
1345
+ if (foregroundControl) updateForegroundNestedProjection(foregroundControl);
1110
1346
  if (chainDetails) rememberForegroundRun(deps.state, { runId, mode: "chain", cwd: effectiveCwd, results: chainDetails.results });
1111
1347
  const intercomReceipt = chainDetails && !chainDetails.results.some((result) => result.interrupted || result.detached)
1112
1348
  ? await maybeBuildForegroundIntercomReceipt({
@@ -1115,6 +1351,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1115
1351
  runId,
1116
1352
  mode: "chain",
1117
1353
  details: chainDetails,
1354
+ ...(foregroundControl?.nestedChildren?.length ? { nestedChildren: foregroundControl.nestedChildren } : {}),
1118
1355
  })
1119
1356
  : null;
1120
1357
  if (intercomReceipt) {
@@ -1311,10 +1548,13 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
1311
1548
  onControlEvent: input.onControlEvent,
1312
1549
  intercomSessionName: input.childIntercomTarget?.(task.agent, index),
1313
1550
  orchestratorIntercomTarget: input.orchestratorIntercomTarget,
1551
+ nestedRoute: input.foregroundControl?.nestedRoute,
1314
1552
  modelOverride: input.modelOverrides[index],
1315
1553
  availableModels: input.availableModels,
1316
1554
  preferredModelProvider: input.ctx.model?.provider,
1317
1555
  skills: effectiveSkills === false ? [] : effectiveSkills,
1556
+ acceptance: task.acceptance,
1557
+ acceptanceContext: { mode: "parallel" },
1318
1558
  onUpdate: input.onUpdate
1319
1559
  ? (progressUpdate) => {
1320
1560
  const stepResults = progressUpdate.details?.results || [];
@@ -1504,6 +1744,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1504
1744
  ...(behaviorOverrides[i]?.outputMode !== undefined ? { outputMode: behaviorOverrides[i]!.outputMode } : {}),
1505
1745
  ...(behaviorOverrides[i]?.reads !== undefined ? { reads: behaviorOverrides[i]!.reads } : {}),
1506
1746
  ...(progress !== undefined ? { progress } : {}),
1747
+ ...(t.acceptance !== undefined ? { acceptance: t.acceptance } : {}),
1507
1748
  };
1508
1749
  });
1509
1750
  return executeAsyncChain(id, {
@@ -1635,12 +1876,14 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1635
1876
  };
1636
1877
  }
1637
1878
 
1879
+ if (foregroundControl) updateForegroundNestedProjection(foregroundControl);
1638
1880
  const intercomReceipt = await maybeBuildForegroundIntercomReceipt({
1639
1881
  pi: deps.pi,
1640
1882
  intercomBridge: data.intercomBridge,
1641
1883
  runId,
1642
1884
  mode: "parallel",
1643
1885
  details,
1886
+ ...(foregroundControl?.nestedChildren?.length ? { nestedChildren: foregroundControl.nestedChildren } : {}),
1644
1887
  });
1645
1888
  if (intercomReceipt) {
1646
1889
  return {
@@ -1869,11 +2112,14 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1869
2112
  onControlEvent,
1870
2113
  intercomSessionName: childIntercomTarget,
1871
2114
  orchestratorIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
2115
+ nestedRoute: foregroundControl?.nestedRoute,
1872
2116
  index: 0,
1873
2117
  modelOverride,
1874
2118
  availableModels,
1875
2119
  preferredModelProvider: currentProvider,
1876
2120
  skills: effectiveSkills,
2121
+ acceptance: params.acceptance,
2122
+ acceptanceContext: { mode: "single" },
1877
2123
  });
1878
2124
  if (foregroundControl?.currentIndex === 0) {
1879
2125
  foregroundControl.interrupt = undefined;
@@ -1914,12 +2160,14 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1914
2160
  rememberForegroundRun(deps.state, { runId, mode: "single", cwd: effectiveCwd, results: details.results });
1915
2161
 
1916
2162
  if (!r.detached && !r.interrupted) {
2163
+ if (foregroundControl) updateForegroundNestedProjection(foregroundControl);
1917
2164
  const intercomReceipt = await maybeBuildForegroundIntercomReceipt({
1918
2165
  pi: deps.pi,
1919
2166
  intercomBridge: data.intercomBridge,
1920
2167
  runId,
1921
2168
  mode: "single",
1922
2169
  details,
2170
+ ...(foregroundControl?.nestedChildren?.length ? { nestedChildren: foregroundControl.nestedChildren } : {}),
1923
2171
  });
1924
2172
  if (intercomReceipt) {
1925
2173
  return {
@@ -2013,16 +2261,41 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2013
2261
  };
2014
2262
  }
2015
2263
  if (params.action === "status") {
2016
- const foreground = getForegroundControl(deps.state, paramsWithResolvedCwd.id ?? paramsWithResolvedCwd.runId);
2017
- if (foreground) return foregroundStatusResult(foreground);
2018
- return inspectSubagentStatus(paramsWithResolvedCwd);
2264
+ const targetRunId = paramsWithResolvedCwd.id ?? paramsWithResolvedCwd.runId;
2265
+ if (targetRunId) {
2266
+ try {
2267
+ const nestedScope = nestedResolutionScopeForExecutor(deps);
2268
+ const resolved = resolveSubagentRunId(targetRunId, { state: deps.state, nested: nestedScope });
2269
+ if (resolved?.kind === "foreground") {
2270
+ const foreground = getForegroundControl(deps.state, resolved.id);
2271
+ if (foreground) return foregroundStatusResult(foreground);
2272
+ }
2273
+ } catch (error) {
2274
+ const message = error instanceof Error ? error.message : String(error);
2275
+ return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
2276
+ }
2277
+ } else {
2278
+ const foreground = getForegroundControl(deps.state, undefined);
2279
+ if (foreground) return foregroundStatusResult(foreground);
2280
+ }
2281
+ return inspectSubagentStatus(paramsWithResolvedCwd, { state: deps.state, nested: nestedResolutionScopeForExecutor(deps) });
2019
2282
  }
2020
2283
  if (params.action === "resume") {
2021
2284
  return resumeAsyncRun({ params: paramsWithResolvedCwd, requestCwd, ctx, deps });
2022
2285
  }
2023
2286
  if (params.action === "interrupt") {
2024
2287
  const targetRunId = paramsWithResolvedCwd.runId ?? paramsWithResolvedCwd.id;
2025
- const foreground = getForegroundControl(deps.state, targetRunId);
2288
+ let resolved: ResolvedSubagentRunId | undefined;
2289
+ if (targetRunId) {
2290
+ try {
2291
+ resolved = resolveSubagentRunId(targetRunId, { state: deps.state, nested: nestedResolutionScopeForExecutor(deps) });
2292
+ } catch (error) {
2293
+ const message = error instanceof Error ? error.message : String(error);
2294
+ return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
2295
+ }
2296
+ }
2297
+ if (resolved?.kind === "nested") return interruptNestedRun(resolved);
2298
+ const foreground = getForegroundControl(deps.state, resolved?.kind === "foreground" ? resolved.id : targetRunId);
2026
2299
  if (foreground?.interrupt) {
2027
2300
  const interrupted = foreground.interrupt();
2028
2301
  if (interrupted) {
@@ -2039,7 +2312,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2039
2312
  details: { mode: "management", results: [] },
2040
2313
  };
2041
2314
  }
2042
- const asyncInterruptResult = interruptAsyncRun(deps.state, targetRunId);
2315
+ const asyncInterruptResult = interruptAsyncRun(deps.state, resolved?.kind === "async" ? resolved.id : targetRunId);
2043
2316
  if (asyncInterruptResult) return asyncInterruptResult;
2044
2317
  return {
2045
2318
  content: [{ type: "text", text: "No interrupt-capable run found in this session." }],
@@ -2054,6 +2327,13 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2054
2327
  details: { mode: "management" as const, results: [] },
2055
2328
  };
2056
2329
  }
2330
+ if (deps.allowMutatingManagementActions === false && MUTATING_MANAGEMENT_ACTIONS.has(params.action)) {
2331
+ return {
2332
+ content: [{ type: "text", text: `Action '${params.action}' is not available from child-safe subagent fanout mode.` }],
2333
+ isError: true,
2334
+ details: { mode: "management" as const, results: [] },
2335
+ };
2336
+ }
2057
2337
  return handleManagementAction(params.action, paramsWithResolvedCwd, { ...ctx, cwd: requestCwd });
2058
2338
  }
2059
2339
 
@@ -2101,6 +2381,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2101
2381
  ? discoveredAgents.map((agent) => applyIntercomBridgeToAgent(agent, intercomBridge))
2102
2382
  : discoveredAgents;
2103
2383
  const runId = randomUUID().slice(0, 8);
2384
+ const inheritedNestedRoute = resolveInheritedNestedRouteFromEnv();
2385
+ const nestedParentAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
2386
+ const nestedRoute = inheritedNestedRoute ?? createNestedRoute(runId);
2104
2387
  const shareEnabled = effectiveParams.share === true;
2105
2388
  const hasChain = (effectiveParams.chain?.length ?? 0) > 0;
2106
2389
  const hasTasks = (effectiveParams.tasks?.length ?? 0) > 0;
@@ -2182,6 +2465,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2182
2465
  effectiveAsync,
2183
2466
  controlConfig,
2184
2467
  intercomBridge,
2468
+ nestedRoute,
2185
2469
  };
2186
2470
 
2187
2471
  const foregroundMode: "single" | "parallel" | "chain" = hasChain ? "chain" : hasTasks ? "parallel" : "single";
@@ -2195,6 +2479,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2195
2479
  currentAgent: undefined,
2196
2480
  currentIndex: undefined,
2197
2481
  currentActivityState: undefined,
2482
+ nestedRoute,
2198
2483
  interrupt: undefined,
2199
2484
  };
2200
2485
  if (foregroundControl) {
@@ -2202,14 +2487,92 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2202
2487
  deps.state.lastForegroundControlId = runId;
2203
2488
  }
2204
2489
 
2490
+ const writeNestedForegroundEvent = (type: "subagent.nested.started" | "subagent.nested.completed", result?: AgentToolResult<Details>): void => {
2491
+ if (!inheritedNestedRoute || !nestedParentAddress) return;
2492
+ const now = Date.now();
2493
+ const details = result?.details;
2494
+ const state = type === "subagent.nested.started"
2495
+ ? "running"
2496
+ : result?.isError || details?.results.some((child) => child.exitCode !== 0)
2497
+ ? "failed"
2498
+ : details?.results.some((child) => child.interrupted)
2499
+ ? "paused"
2500
+ : "complete";
2501
+ const errorText = result?.isError
2502
+ ? result.content.find((item) => item.type === "text")?.text
2503
+ : undefined;
2504
+ const agentsForSummary = hasTasks && effectiveParams.tasks
2505
+ ? effectiveParams.tasks.map((task) => task.agent)
2506
+ : hasChain && effectiveParams.chain
2507
+ ? effectiveParams.chain.flatMap((step) => isParallelStep(step) ? step.parallel.map((task) => task.agent) : [(step as SequentialStep).agent])
2508
+ : effectiveParams.agent ? [effectiveParams.agent] : [];
2509
+ const leafIntercomTarget = intercomBridge.active && agentsForSummary[0]
2510
+ ? resolveSubagentIntercomTarget(runId, agentsForSummary[0], 0)
2511
+ : undefined;
2512
+ try {
2513
+ writeNestedEvent(inheritedNestedRoute, {
2514
+ type,
2515
+ ts: now,
2516
+ parentRunId: nestedParentAddress.parentRunId,
2517
+ parentStepIndex: nestedParentAddress.parentStepIndex,
2518
+ child: {
2519
+ id: runId,
2520
+ parentRunId: nestedParentAddress.parentRunId,
2521
+ parentStepIndex: nestedParentAddress.parentStepIndex,
2522
+ depth: nestedParentAddress.depth,
2523
+ path: nestedParentAddress.path,
2524
+ ownerIntercomTarget: process.env.PI_SUBAGENT_INTERCOM_SESSION_NAME,
2525
+ leafIntercomTarget,
2526
+ intercomTarget: leafIntercomTarget,
2527
+ ownerState: state === "running" ? "live" : "gone",
2528
+ mode: foregroundMode,
2529
+ state,
2530
+ agent: agentsForSummary[0],
2531
+ agents: agentsForSummary,
2532
+ startedAt: foregroundControl?.startedAt ?? now,
2533
+ ...(state !== "running" ? { endedAt: now } : {}),
2534
+ lastUpdate: now,
2535
+ ...(errorText ? { error: errorText } : {}),
2536
+ ...(details?.results.length ? { steps: details.results.map((child) => ({
2537
+ agent: child.agent,
2538
+ status: child.interrupted ? "paused" : child.exitCode === 0 ? "complete" : "failed",
2539
+ ...(child.sessionFile ? { sessionFile: child.sessionFile } : {}),
2540
+ ...(child.error ? { error: child.error } : {}),
2541
+ })) } : {}),
2542
+ },
2543
+ });
2544
+ } catch (error) {
2545
+ console.error("Failed to emit nested foreground status event:", error);
2546
+ }
2547
+ };
2548
+
2549
+ let nestedForegroundStarted = false;
2205
2550
  try {
2206
2551
  const asyncResult = runAsyncPath(execData, deps);
2207
2552
  if (asyncResult) return withForkContext(asyncResult, effectiveParams.context);
2208
- if (hasChain && effectiveParams.chain) return withForkContext(await runChainPath(execData, deps), effectiveParams.context);
2209
- if (hasTasks && effectiveParams.tasks) return withForkContext(await runParallelPath(execData, deps), effectiveParams.context);
2210
- if (hasSingle) return withForkContext(await runSinglePath(execData, deps), effectiveParams.context);
2553
+ if (foregroundControl) {
2554
+ writeNestedForegroundEvent("subagent.nested.started");
2555
+ nestedForegroundStarted = true;
2556
+ }
2557
+ if (hasChain && effectiveParams.chain) {
2558
+ const result = await runChainPath(execData, deps);
2559
+ writeNestedForegroundEvent("subagent.nested.completed", result);
2560
+ return withForkContext(result, effectiveParams.context);
2561
+ }
2562
+ if (hasTasks && effectiveParams.tasks) {
2563
+ const result = await runParallelPath(execData, deps);
2564
+ writeNestedForegroundEvent("subagent.nested.completed", result);
2565
+ return withForkContext(result, effectiveParams.context);
2566
+ }
2567
+ if (hasSingle) {
2568
+ const result = await runSinglePath(execData, deps);
2569
+ writeNestedForegroundEvent("subagent.nested.completed", result);
2570
+ return withForkContext(result, effectiveParams.context);
2571
+ }
2211
2572
  } catch (error) {
2212
- return toExecutionErrorResult(effectiveParams, error);
2573
+ const errorResult = toExecutionErrorResult(effectiveParams, error);
2574
+ if (nestedForegroundStarted) writeNestedForegroundEvent("subagent.nested.completed", errorResult);
2575
+ return errorResult;
2213
2576
  } finally {
2214
2577
  if (foregroundControl) {
2215
2578
  clearPendingForegroundControlNotices(deps.state, runId);