pi-subagents 0.40.0 → 0.41.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 (119) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +246 -525
  3. package/agents/oracle.md +1 -0
  4. package/package.json +12 -4
  5. package/prompts/parallel-context-build.md +1 -1
  6. package/prompts/parallel-handoff-plan.md +1 -1
  7. package/prompts/review-loop.md +1 -1
  8. package/skills/pi-subagents/SKILL.md +6 -6
  9. package/skills/pi-subagents/references/constraints-and-recipes.md +19 -26
  10. package/skills/pi-subagents/references/execution-controls.md +98 -97
  11. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  12. package/skills/pi-subagents/references/prompting-and-roles.md +18 -27
  13. package/src/agents/agent-management.ts +155 -65
  14. package/src/agents/agent-serializer.ts +19 -0
  15. package/src/agents/agents.ts +154 -71
  16. package/src/agents/chain-serializer.ts +10 -7
  17. package/src/agents/frontmatter.ts +5 -3
  18. package/src/agents/identity.ts +1 -1
  19. package/src/agents/proactive-skills.ts +13 -10
  20. package/src/agents/skills.ts +23 -6
  21. package/src/api/control-channel.ts +4 -0
  22. package/src/api/delegation.ts +26 -194
  23. package/src/api/external-runs.ts +129 -0
  24. package/src/api/intercom-bridge.ts +3 -0
  25. package/src/api/pi-args.ts +5 -0
  26. package/src/api/preflight.ts +3 -3
  27. package/src/api/shared-types.ts +19 -0
  28. package/src/extension/config.ts +10 -0
  29. package/src/extension/control-notices.ts +5 -39
  30. package/src/extension/doctor.ts +10 -9
  31. package/src/extension/fanout-child.ts +7 -4
  32. package/src/extension/index.ts +232 -68
  33. package/src/extension/rpc.ts +18 -12
  34. package/src/extension/schemas.ts +48 -37
  35. package/src/extension/tool-description.ts +36 -86
  36. package/src/inspectors/herdr/actions.ts +229 -0
  37. package/src/inspectors/herdr/client.ts +130 -0
  38. package/src/inspectors/herdr/inspector-runner.ts +141 -0
  39. package/src/inspectors/herdr/project-panes.ts +154 -0
  40. package/src/integrations/herdr-status.ts +330 -0
  41. package/src/intercom/intercom-bridge.ts +3 -2
  42. package/src/intercom/result-intercom.ts +5 -1
  43. package/src/missions/actions.ts +372 -0
  44. package/src/missions/lifecycle.ts +314 -0
  45. package/src/missions/store.ts +442 -0
  46. package/src/missions/types.ts +135 -0
  47. package/src/policy/authority.ts +46 -0
  48. package/src/profiles/profiles.ts +29 -3
  49. package/src/runs/background/async-execution.ts +98 -49
  50. package/src/runs/background/async-job-tracker.ts +10 -2
  51. package/src/runs/background/async-resume.ts +6 -6
  52. package/src/runs/background/async-status.ts +29 -1
  53. package/src/runs/background/auto-drain.ts +3 -3
  54. package/src/runs/background/chain-append.ts +3 -2
  55. package/src/runs/background/control-channel.ts +9 -7
  56. package/src/runs/background/fleet-view.ts +3 -4
  57. package/src/runs/background/notify.ts +2 -1
  58. package/src/runs/background/process-terminal.ts +5 -5
  59. package/src/runs/background/result-watcher.ts +13 -5
  60. package/src/runs/background/run-id-resolver.ts +3 -3
  61. package/src/runs/background/run-status.ts +35 -8
  62. package/src/runs/background/scheduled-runs.ts +602 -375
  63. package/src/runs/background/stale-run-reconciler.ts +3 -3
  64. package/src/runs/background/subagent-runner.ts +608 -445
  65. package/src/runs/background/subagent-wait.ts +50 -9
  66. package/src/runs/background/wait-subscriptions.ts +253 -0
  67. package/src/runs/background/wait-tool.ts +12 -4
  68. package/src/runs/foreground/async-steering-action.ts +3 -3
  69. package/src/runs/foreground/chain-clarify.ts +8 -4
  70. package/src/runs/foreground/chain-execution.ts +56 -30
  71. package/src/runs/foreground/execution.ts +15 -2
  72. package/src/runs/foreground/subagent-executor.ts +1023 -273
  73. package/src/runs/shared/acceptance.ts +28 -6
  74. package/src/runs/shared/child-protocol.ts +302 -22
  75. package/src/runs/shared/dynamic-fanout.ts +1 -1
  76. package/src/runs/shared/external-cli-runner.ts +130 -0
  77. package/src/runs/shared/long-running-guard.ts +42 -1
  78. package/src/runs/shared/nested-events.ts +59 -5
  79. package/src/runs/shared/nested-render.ts +9 -4
  80. package/src/runs/shared/parallel-handoff.ts +86 -2
  81. package/src/runs/shared/parallel-utils.ts +11 -2
  82. package/src/runs/shared/permissions.ts +95 -0
  83. package/src/runs/shared/pi-args.ts +11 -1
  84. package/src/runs/shared/pi-spawn.ts +11 -1
  85. package/src/runs/shared/run-history.ts +1 -1
  86. package/src/runs/shared/subagent-prompt-runtime.ts +36 -5
  87. package/src/runs/shared/subagent-startup-retry.ts +5 -2
  88. package/src/runs/shared/turn-budget.ts +6 -6
  89. package/src/runs/shared/worktree.ts +122 -12
  90. package/src/shared/accessible-dir.ts +29 -7
  91. package/src/shared/artifacts.ts +18 -1
  92. package/src/shared/fork-context.ts +3 -2
  93. package/src/shared/launch-contract.ts +1 -0
  94. package/src/shared/settings.ts +10 -0
  95. package/src/shared/types.ts +156 -14
  96. package/src/shared/utils.ts +8 -6
  97. package/src/slash/delegation-adapters.ts +32 -194
  98. package/src/slash/delegation-request.ts +43 -126
  99. package/src/slash/prompt-template-bridge.ts +158 -205
  100. package/src/slash/prompt-workflows.ts +21 -57
  101. package/src/slash/slash-bridge.ts +14 -0
  102. package/src/slash/slash-commands.ts +31 -632
  103. package/src/slash/subagents-admin.ts +18 -14
  104. package/src/tui/fleet-status.ts +156 -21
  105. package/src/tui/fleet-transcript.ts +110 -5
  106. package/src/tui/fleet.ts +56 -24
  107. package/src/tui/render.ts +291 -109
  108. package/src/types/pi-runtime-compat.d.ts +14 -0
  109. package/src/watchdog/lsp-diagnostics.ts +12 -7
  110. package/src/watchdog/model-selection.ts +2 -2
  111. package/src/watchdog/permission-arbiter.ts +145 -0
  112. package/src/watchdog/register-child.ts +1 -1
  113. package/src/watchdog/register-main.ts +1 -1
  114. package/src/watchdog/review.ts +4 -1
  115. package/src/watchdog/runtime.ts +3 -2
  116. package/src/workflows/chat-progress.ts +140 -0
  117. package/src/workflows/scripted-workflow.ts +415 -0
  118. package/agents/advisor.md +0 -73
  119. package/src/extension/chain-validation.ts +0 -181
@@ -4,7 +4,8 @@ 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
6
  import { resolveAgentName, type AgentConfig, type AgentScope } from "../../agents/agents.ts";
7
- import { getArtifactsDir, getProjectChainRunsDir } from "../../shared/artifacts.ts";
7
+ import { getArtifactsDir, getChainRunsDir } from "../../shared/artifacts.ts";
8
+ import { writeAtomicJson } from "../../shared/atomic-json.ts";
8
9
  import { ChainClarifyComponent, type ChainClarifyResult } from "./chain-clarify.ts";
9
10
  import { resolveEffectiveThinking, toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
10
11
  import { executeChain } from "./chain-execution.ts";
@@ -19,7 +20,6 @@ import {
19
20
  import { resolveExecutionAgentScope } from "../../agents/agent-scope.ts";
20
21
  import { handleManagementAction } from "../../agents/agent-management.ts";
21
22
  import { buildDoctorReport } from "../../extension/doctor.ts";
22
- import { clearPendingForegroundControlNotices } from "../../extension/control-notices.ts";
23
23
  import { runSync } from "./execution.ts";
24
24
  import { handleWatchdogToolAction, WATCHDOG_TOOL_ACTIONS } from "../../watchdog/tool-actions.ts";
25
25
  import type { MainWatchdogRuntime } from "../../watchdog/runtime.ts";
@@ -37,6 +37,9 @@ import {
37
37
  suppressProgressForReadOnlyTask,
38
38
  taskDisallowsFileUpdates,
39
39
  type ChainStep,
40
+ type DynamicParallelStep,
41
+ type ParallelStep,
42
+ type ParallelTaskItem,
40
43
  type ResolvedStepBehavior,
41
44
  type SequentialStep,
42
45
  type StepOverrides,
@@ -61,7 +64,7 @@ import { finalizeSingleOutput, injectSingleOutputInstruction, normalizeSingleOut
61
64
  import { cleanupStructuredOutputRuntime, createStructuredOutputRuntime } from "../shared/structured-output.ts";
62
65
  import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, readStatus, resolveChildCwd, sumResultsCost, sumResultsUsage } from "../../shared/utils.ts";
63
66
  import { DEFAULT_GLOBAL_CONCURRENCY_LIMIT, Semaphore } from "../shared/parallel-utils.ts";
64
- import { formatParallelHandoffError, formatParallelHandoffReference, parallelHandoffPath, writeParallelHandoffGroup } from "../shared/parallel-handoff.ts";
67
+ import { discardPreservedWorktrees, formatParallelHandoffError, formatParallelHandoffReference, parallelHandoffPath, writeParallelHandoffGroup, writePendingParallelHandoff } from "../shared/parallel-handoff.ts";
65
68
  import { summarizeContextModes, type ContextMode, type ContextSummary } from "../shared/context-mode.ts";
66
69
  import {
67
70
  attachNestedChildrenToResultChildren,
@@ -83,6 +86,13 @@ import { resolveSubagentRunId, type ResolvedSubagentRunId } from "../background/
83
86
  import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
84
87
  import { inspectSubagentStatus } from "../background/run-status.ts";
85
88
  import { applyForceTopLevelAsyncOverride } from "../background/top-level-async.ts";
89
+ import { handleMissionAction, MISSION_ACTIONS } from "../../missions/actions.ts";
90
+ import { attachMissionToLaunchResult, prepareMissionLaunch, type MissionLaunchBinding } from "../../missions/lifecycle.ts";
91
+ import { resolveAuthorityDecision } from "../../policy/authority.ts";
92
+ import { handleHerdrInspectorAction, HERDR_INSPECTOR_ACTIONS } from "../../inspectors/herdr/actions.ts";
93
+ import { handleHerdrProjectPaneAction, HERDR_PROJECT_PANE_ACTIONS } from "../../inspectors/herdr/project-panes.ts";
94
+ import { runWorkflowScript, WorkflowScriptError, type WorkflowScriptChildResult } from "../../workflows/scripted-workflow.ts";
95
+ import { resolveWorkflowChatProgress, type WorkflowChatProgressProjection } from "../../workflows/chat-progress.ts";
86
96
  import {
87
97
  cleanupWorktrees,
88
98
  createWorktrees,
@@ -94,6 +104,7 @@ import {
94
104
  } from "../shared/worktree.ts";
95
105
  import {
96
106
  type AgentProgress,
107
+ type AsyncJobState,
97
108
  type AsyncStatus,
98
109
  type AcceptanceInput,
99
110
  type AgentContract,
@@ -118,10 +129,9 @@ import {
118
129
  type UsageBudgetConfig,
119
130
  type SubagentRunMode,
120
131
  type SubagentState,
121
- ASYNC_DIR,
132
+ DIRS,
122
133
  DEFAULT_ARTIFACT_CONFIG,
123
134
  DEFAULT_FORK_PREAMBLE,
124
- RESULTS_DIR,
125
135
  SUBAGENT_ACTIONS,
126
136
  SUBAGENT_CONTROL_EVENT,
127
137
  SUBAGENT_CONTROL_INTERCOM_EVENT,
@@ -134,7 +144,39 @@ import {
134
144
  wrapForkTask,
135
145
  } from "../../shared/types.ts";
136
146
 
137
- const MUTATING_MANAGEMENT_ACTIONS = new Set(["create", "update", "delete", "eject", "disable", "enable", "reset", "grant-spawn-budget", "watchdog.configure"]);
147
+ const MUTATING_MANAGEMENT_ACTIONS = new Set(["create", "update", "delete", "eject", "disable", "enable", "reset", "grant-spawn-budget", "watchdog.configure", "mission.create", "mission.update", "mission.attach-run", "mission.close", "inspector.open", "inspector.close", "project.open", "project.close", "worktree.discard", "schedule.create", "schedule.pause", "schedule.resume", "schedule.run", "schedule.run-due", "schedule.delete"]);
148
+
149
+ type UndefinedOmitted<T extends object> = {
150
+ [K in keyof T as undefined extends T[K] ? never : K]: T[K];
151
+ } & {
152
+ [K in keyof T as undefined extends T[K] ? K : never]?: Exclude<T[K], undefined>;
153
+ };
154
+
155
+ // These helpers mutate their argument, so keep calls scoped to fresh object literals or shallow copies.
156
+ function omitUndefinedProperties<T extends object>(value: T): UndefinedOmitted<T> {
157
+ for (const key of Object.keys(value) as Array<keyof T>) {
158
+ if (value[key] === undefined) delete value[key];
159
+ }
160
+ return value as UndefinedOmitted<T>;
161
+ }
162
+
163
+ type WithUndefinedOptionals<T extends object> = {
164
+ [K in keyof T]: {} extends Pick<T, K> ? T[K] | undefined : T[K];
165
+ };
166
+
167
+ type RequiredKeysAllowingUndefined<T extends object> = {
168
+ [K in keyof T]-?: {} extends Pick<T, K> ? never : undefined extends T[K] ? K : never;
169
+ }[keyof T];
170
+
171
+ function compactOptional<T extends object>(
172
+ value: WithUndefinedOptionals<T> & (RequiredKeysAllowingUndefined<T> extends never ? unknown : never),
173
+ ): T {
174
+ for (const key of Object.keys(value) as Array<keyof T>) {
175
+ if (value[key] === undefined) delete value[key];
176
+ }
177
+ return value as T;
178
+ }
179
+
138
180
  interface TaskParam {
139
181
  agent: string;
140
182
  task: string;
@@ -157,13 +199,26 @@ export interface SubagentParamsLike {
157
199
  id?: string;
158
200
  runId?: string;
159
201
  dir?: string;
202
+ handoffPath?: string;
160
203
  index?: number;
161
204
  view?: "fleet" | "transcript";
162
205
  lines?: number;
206
+ chainName?: string;
207
+ config?: unknown;
208
+ name?: string;
209
+ type?: string;
163
210
  agent?: string;
164
211
  task?: string;
165
212
  message?: string;
166
213
  steeringRecovery?: boolean;
214
+ workflowScript?: string;
215
+ chatProgress?: "auto" | "off" | "terminal" | "milestones" | "live-card";
216
+ step?: ChainStep;
217
+ /** Internal workflow ownership metadata; not part of the public schema. */
218
+ workflowParentRunId?: string;
219
+ workflowKey?: string;
220
+ suppressRoutineResultIntercom?: boolean;
221
+ /** Internal durable-run compatibility fields. Public callers must use workflowScript. */
167
222
  chain?: ChainStep[];
168
223
  tasks?: TaskParam[];
169
224
  concurrency?: number;
@@ -190,6 +245,7 @@ export interface SubagentParamsLike {
190
245
  thinking?: string | false;
191
246
  scope?: string;
192
247
  target?: string;
248
+ focus?: boolean;
193
249
  skill?: string | string[] | boolean;
194
250
  output?: string | boolean;
195
251
  outputMode?: "inline" | "file-only";
@@ -200,11 +256,25 @@ export interface SubagentParamsLike {
200
256
  agentContract?: AgentContract;
201
257
  schedule?: string;
202
258
  scheduleName?: string;
259
+ at?: string;
260
+ every?: string;
261
+ on?: string | number;
262
+ timezone?: string;
263
+ overlap?: "skip";
264
+ catchUp?: "none" | "latest";
203
265
  additional?: number;
266
+ missionId?: string;
267
+ mission?: unknown;
268
+ missionUpdate?: unknown;
269
+ missionStatus?: string;
270
+ missionScope?: string;
271
+ runMode?: string;
272
+ runStatus?: string;
273
+ summary?: string;
204
274
  }
205
275
 
206
276
  function rememberParentModel(state: { currentSessionId?: string | null; lastParentModel?: ParentModel }, sessionId: string | null, model: unknown): ParentModel | undefined {
207
- if (state.currentSessionId !== sessionId) state.lastParentModel = undefined;
277
+ if (state.currentSessionId !== sessionId) delete state.lastParentModel;
208
278
  state.currentSessionId = sessionId;
209
279
  const parentModel = normalizeParentModel(model);
210
280
  if (!sessionId) return parentModel;
@@ -263,6 +333,7 @@ interface ExecutionContextData {
263
333
  modelScope?: ModelScopeConfig;
264
334
  parentModel?: ParentModel;
265
335
  parentSessionId: string | null;
336
+ parentPiSessionId?: string;
266
337
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
267
338
  }
268
339
 
@@ -273,7 +344,6 @@ function resolveRequestedCwd(runtimeCwd: string, requestedCwd: string | undefine
273
344
  function removeForegroundControlIfIdle(state: SubagentState, runId: string): boolean {
274
345
  const control = state.foregroundControls.get(runId);
275
346
  if (control && (!foregroundSchedulingSettled(control) || (control.activeChildren?.size ?? 0) > 0)) return false;
276
- clearPendingForegroundControlNotices(state, runId);
277
347
  state.foregroundControls.delete(runId);
278
348
  if (state.lastForegroundControlId === runId) state.lastForegroundControlId = null;
279
349
  return true;
@@ -429,7 +499,7 @@ function rememberForegroundRun(state: SubagentState, input: { runId: string; mod
429
499
  agent: result.agent,
430
500
  index,
431
501
  ...(result.context ? { context: result.context } : {}),
432
- status: resolveSubagentResultStatus({
502
+ status: resolveSubagentResultStatus(omitUndefinedProperties({
433
503
  exitCode: result.exitCode,
434
504
  interrupted: result.interrupted,
435
505
  detached: result.detached,
@@ -437,7 +507,7 @@ function rememberForegroundRun(state: SubagentState, input: { runId: string; mod
437
507
  timedOut: result.timedOut,
438
508
  stopped: result.stopped,
439
509
  turnBudgetExceeded: result.turnBudgetExceeded,
440
- }),
510
+ })),
441
511
  ...foregroundChildActivityFromProgress(result.progress),
442
512
  updatedAt,
443
513
  ...(result.exitCode !== undefined ? { exitCode: result.exitCode } : {}),
@@ -491,7 +561,7 @@ function applyControlEventToRememberedForegroundRun(state: SubagentState, event:
491
561
  };
492
562
  }
493
563
 
494
- function updateRememberedForegroundChild(state: SubagentState, input: { runId: string; mode: "single" | "parallel" | "chain"; cwd: string; sessionId: string | null; index: number; result: SingleResult; events: IntercomEventBus }): void {
564
+ function updateRememberedForegroundChild(state: SubagentState, input: { runId: string; mode: "single" | "parallel" | "chain"; cwd: string; sessionId: string | null; index: number; result: SingleResult; events: IntercomEventBus; notify?: boolean }): void {
495
565
  state.foregroundRuns ??= new Map();
496
566
  const updatedAt = Date.now();
497
567
  let run = state.foregroundRuns.get(input.runId);
@@ -500,7 +570,7 @@ function updateRememberedForegroundChild(state: SubagentState, input: { runId: s
500
570
  state.foregroundRuns.set(input.runId, run);
501
571
  }
502
572
  run.updatedAt = updatedAt;
503
- const terminalStatus = resolveSubagentResultStatus({
573
+ const terminalStatus = resolveSubagentResultStatus(omitUndefinedProperties({
504
574
  exitCode: input.result.exitCode,
505
575
  ...(input.result.acceptance?.status === "rejected" ? { success: false } : {}),
506
576
  interrupted: input.result.interrupted,
@@ -509,9 +579,9 @@ function updateRememberedForegroundChild(state: SubagentState, input: { runId: s
509
579
  timedOut: input.result.timedOut,
510
580
  stopped: input.result.stopped,
511
581
  turnBudgetExceeded: input.result.turnBudgetExceeded,
512
- });
582
+ }));
513
583
  const child = run.children[input.index] ?? { agent: input.result.agent, index: input.index, status: "detached" as const };
514
- run.children[input.index] = {
584
+ run.children[input.index] = omitUndefinedProperties({
515
585
  ...child,
516
586
  agent: input.result.agent,
517
587
  index: input.index,
@@ -539,7 +609,7 @@ function updateRememberedForegroundChild(state: SubagentState, input: { runId: s
539
609
  ...(input.result.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: input.result.runtimeAcknowledgedExtensions } : {}),
540
610
  ...(input.result.capabilityCeiling ? { capabilityCeiling: input.result.capabilityCeiling } : {}),
541
611
  ...(input.result.capabilityAudit ? { capabilityAudit: input.result.capabilityAudit } : {}),
542
- };
612
+ });
543
613
  trimRememberedForegroundRuns(state);
544
614
  const output = getSingleResultOutput(input.result).trim();
545
615
  const success = terminalStatus === "completed";
@@ -548,7 +618,7 @@ function updateRememberedForegroundChild(state: SubagentState, input: { runId: s
548
618
  : output || input.result.error || "Detached child exited without final output.";
549
619
  // A detached callback may outlive its extension runtime. Stale sessions are
550
620
  // intentionally dropped rather than routed through a replacement runtime.
551
- if (!input.sessionId || input.sessionId !== state.currentSessionId) return;
621
+ if (input.notify === false || !input.sessionId || input.sessionId !== state.currentSessionId) return;
552
622
  input.events.emit(SUBAGENT_FOREGROUND_COMPLETE_EVENT, {
553
623
  id: `${input.runId}:${input.index}`,
554
624
  runId: input.runId,
@@ -572,7 +642,7 @@ function updateRememberedForegroundChild(state: SubagentState, input: { runId: s
572
642
  });
573
643
  }
574
644
 
575
- function resolveForegroundResumeTarget(params: SubagentParamsLike, state: SubagentState): { runId: string; mode: "single" | "parallel" | "chain"; state: "complete"; agent: string; index: number; cwd: string; sessionFile: string; model?: string; thinking?: string; launchContractDigest?: string; capabilityCeiling?: ResolvedSubagentCapabilityCeiling } | undefined {
645
+ function resolveForegroundResumeTarget(params: SubagentParamsLike, state: SubagentState): { runId: string; mode: SubagentRunMode; state: "complete"; agent: string; index: number; cwd: string; sessionFile: string; model?: string; thinking?: string; launchContractDigest?: string; capabilityCeiling?: ResolvedSubagentCapabilityCeiling } | undefined {
576
646
  const requested = (params.id ?? params.runId)?.trim();
577
647
  if (!requested || !state.foregroundRuns?.size || !state.currentSessionId) return undefined;
578
648
  const sessionRuns = [...state.foregroundRuns.values()].filter((run) => run.sessionId === state.currentSessionId);
@@ -617,6 +687,9 @@ type NestedResumeSourceTarget = {
617
687
  index: number;
618
688
  cwd?: string;
619
689
  sessionFile: string;
690
+ model?: string;
691
+ thinking?: AgentConfig["thinking"];
692
+ launchContractDigest?: string;
620
693
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
621
694
  };
622
695
  type ResumeSourceTarget = AsyncResumeSourceTarget | ForegroundResumeSourceTarget | NestedResumeSourceTarget;
@@ -658,10 +731,10 @@ function resolveResumeTarget(params: SubagentParamsLike, state: SubagentState, o
658
731
  try {
659
732
  asyncTarget = {
660
733
  source: "async",
661
- ...resolveAsyncResumeTarget(params, {}, {
734
+ ...resolveAsyncResumeTarget(params, {}, compactOptional<NonNullable<Parameters<typeof resolveAsyncResumeTarget>[2]>>({
662
735
  requireSessionFile: options.asyncRequireSessionFile,
663
736
  sessionId: state.currentSessionId ?? undefined,
664
- }),
737
+ })),
665
738
  };
666
739
  } catch (error) {
667
740
  asyncError = error;
@@ -753,7 +826,7 @@ function interruptAsyncRun(
753
826
  ): AgentToolResult<Details> | null {
754
827
  const target = getAsyncInterruptTarget(state, runId, location);
755
828
  if (!target) return null;
756
- const status = reconcileAsyncRun(target.asyncDir, { kill }).status;
829
+ const status = reconcileAsyncRun(target.asyncDir, omitUndefinedProperties({ kill })).status;
757
830
  if (!status || status.state !== "running" || typeof status.pid !== "number") {
758
831
  return {
759
832
  content: [{ type: "text", text: `No running async run with an interrupt-capable pid was found for '${runId ?? "current"}'.` }],
@@ -761,11 +834,19 @@ function interruptAsyncRun(
761
834
  details: { mode: "management", results: [] },
762
835
  };
763
836
  }
837
+ const activeSteps = status.steps?.filter((step) => step.status === "running") ?? [];
838
+ if (activeSteps.length > 0 && activeSteps.every((step) => step.runner?.type === "external-cli")) {
839
+ return {
840
+ content: [{ type: "text", text: `Interrupt is unsupported for one-shot external CLI async run ${target.asyncId}; use stop instead.` }],
841
+ isError: true,
842
+ details: { mode: "management", results: [] },
843
+ };
844
+ }
764
845
  try {
765
- deliverInterruptRequest({ asyncDir: target.asyncDir, pid: status.pid, kill, source: "interrupt-action" });
846
+ deliverInterruptRequest(omitUndefinedProperties({ asyncDir: target.asyncDir, pid: status.pid, kill, source: "interrupt-action" }));
766
847
  const tracked = state.asyncJobs.get(target.asyncId);
767
848
  if (tracked) {
768
- tracked.activityState = undefined;
849
+ delete tracked.activityState;
769
850
  tracked.updatedAt = Date.now();
770
851
  }
771
852
  return {
@@ -807,14 +888,15 @@ function appendStepToAsyncChain(input: {
807
888
  details: { mode: "management", results: [] },
808
889
  };
809
890
  }
810
- if (!input.params.chain || input.params.chain.length !== 1) {
891
+ if (!input.params.step) {
811
892
  return {
812
- content: [{ type: "text", text: "action='append-step' requires chain with exactly one step." }],
893
+ content: [{ type: "text", text: "action='append-step' requires step." }],
813
894
  isError: true,
814
895
  details: { mode: "management", results: [] },
815
896
  };
816
897
  }
817
- const acceptanceErrors = validateExecutionAcceptance(input.params);
898
+ const chain = [input.params.step];
899
+ const acceptanceErrors = validateExecutionAcceptance({ ...input.params, chain } as Parameters<typeof validateExecutionAcceptance>[0]);
818
900
  if (acceptanceErrors.length > 0) {
819
901
  return {
820
902
  content: [{ type: "text", text: `Cannot append step: ${acceptanceErrors.join(" ")}` }],
@@ -825,7 +907,7 @@ function appendStepToAsyncChain(input: {
825
907
 
826
908
  let resolved: ResolvedSubagentRunId | undefined;
827
909
  try {
828
- resolved = resolveSubagentRunId(targetRunId, { state: input.deps.state, nested: nestedResolutionScopeForExecutor(input.deps) });
910
+ resolved = resolveSubagentRunId(targetRunId, omitUndefinedProperties({ state: input.deps.state, nested: nestedResolutionScopeForExecutor(input.deps) }));
829
911
  } catch (error) {
830
912
  const message = error instanceof Error ? error.message : String(error);
831
913
  return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
@@ -883,7 +965,7 @@ function appendStepToAsyncChain(input: {
883
965
  ...pendingAppendRequests.flatMap((request) => runnerStepOutputNames(request.steps)),
884
966
  ]);
885
967
  try {
886
- validateChainOutputBindingsWithContext(input.params.chain, { maxItems: input.deps.config.chain?.dynamicFanout?.maxItems }, {
968
+ validateChainOutputBindingsWithContext(chain, omitUndefinedProperties({ maxItems: input.deps.config.chain?.dynamicFanout?.maxItems }), {
887
969
  priorOutputNames: reservedOutputNames,
888
970
  startStepIndex: status.chainStepCount ?? status.steps?.length ?? 0,
889
971
  });
@@ -903,7 +985,7 @@ function appendStepToAsyncChain(input: {
903
985
  const chainSkillInput = normalizeSkillInput(input.params.skill);
904
986
  const chainSkills = chainSkillInput === false ? [] : (chainSkillInput ?? []);
905
987
  const parentModel = input.parentModel;
906
- const asyncCtx = {
988
+ const asyncCtx = compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
907
989
  pi: input.deps.pi,
908
990
  cwd: input.ctx.cwd,
909
991
  currentSessionId: resolveCurrentSessionId(input.ctx.sessionManager),
@@ -912,9 +994,10 @@ function appendStepToAsyncChain(input: {
912
994
  currentModel: parentModel,
913
995
  modelScope: discoveredForAppend.modelScope,
914
996
  interactive: input.ctx.hasUI,
915
- };
916
- const built = buildAsyncRunnerSteps(resolved.id, {
917
- chain: wrapChainTasksForFork(input.params.chain, contextPolicy),
997
+ permissions: input.deps.config.permissions,
998
+ });
999
+ const built = buildAsyncRunnerSteps(resolved.id, compactOptional<Parameters<typeof buildAsyncRunnerSteps>[1]>({
1000
+ chain: wrapChainTasksForFork(chain, contextPolicy),
918
1001
  task: input.params.task,
919
1002
  resultMode: "chain",
920
1003
  agents,
@@ -929,7 +1012,7 @@ function appendStepToAsyncChain(input: {
929
1012
  asyncDir: resolved.location.asyncDir,
930
1013
  validateOutputBindings: false,
931
1014
  capabilityCeiling: intersectSubagentCapabilityCeilings(status.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(asyncCtx.currentSessionId)),
932
- });
1015
+ }));
933
1016
  if ("error" in built) {
934
1017
  return {
935
1018
  content: [{ type: "text", text: built.error }],
@@ -1024,7 +1107,7 @@ function resolveNestedResumeTarget(match: ResolvedSubagentRunId & { kind: "neste
1024
1107
  if (!agent) throw new Error(`Could not determine child agent for nested run '${run.id}'.`);
1025
1108
  const state = run.state === "complete" || run.state === "failed" || run.state === "paused" ? run.state : "failed";
1026
1109
  const asyncDir = resolveNestedAsyncDir(match.match.rootRunId, run);
1027
- return {
1110
+ return compactOptional<NestedResumeSourceTarget>({
1028
1111
  kind: "revive",
1029
1112
  source: "nested",
1030
1113
  runId: run.id,
@@ -1034,7 +1117,7 @@ function resolveNestedResumeTarget(match: ResolvedSubagentRunId & { kind: "neste
1034
1117
  cwd: asyncDir ? path.dirname(asyncDir) : undefined,
1035
1118
  sessionFile: validateNestedSessionFile(run, trustedSessionRoots),
1036
1119
  ...(run.capabilityCeiling ? { capabilityCeiling: run.capabilityCeiling } : {}),
1037
- };
1120
+ });
1038
1121
  }
1039
1122
 
1040
1123
  async function waitForNestedControlResult(target: ResolvedSubagentRunId & { kind: "nested" }, requestId: string, ignoredFiles: ReadonlySet<string>, timeoutMs = 1_000) {
@@ -1065,7 +1148,7 @@ function directNestedAsyncInterrupt(target: ResolvedSubagentRunId & { kind: "nes
1065
1148
  const run = target.match.run;
1066
1149
  const asyncDir = resolveNestedAsyncDir(target.match.rootRunId, run);
1067
1150
  if (!asyncDir) return undefined;
1068
- const status = reconcileAsyncRun(asyncDir, { resultsDir: path.join(RESULTS_DIR, "nested", target.match.rootRunId) }).status;
1151
+ const status = reconcileAsyncRun(asyncDir, { resultsDir: path.join(DIRS.results, "nested", target.match.rootRunId) }).status;
1069
1152
  const pid = typeof status?.pid === "number" && status.pid > 0 ? status.pid : run.pid;
1070
1153
  if (!status || status.state !== "running" || typeof pid !== "number" || pid <= 0) return undefined;
1071
1154
  try {
@@ -1081,7 +1164,7 @@ async function directNestedAsyncSteer(input: { target: ResolvedSubagentRunId & {
1081
1164
  const run = input.target.match.run;
1082
1165
  const asyncDir = resolveNestedAsyncDir(input.target.match.rootRunId, run);
1083
1166
  if (!asyncDir) return undefined;
1084
- const status = reconcileAsyncRun(asyncDir, { resultsDir: path.join(RESULTS_DIR, "nested", input.target.match.rootRunId) }).status;
1167
+ const status = reconcileAsyncRun(asyncDir, { resultsDir: path.join(DIRS.results, "nested", input.target.match.rootRunId) }).status;
1085
1168
  if (!status || (status.state !== "running" && status.state !== "queued")) return undefined;
1086
1169
  const steps = status.steps ?? [];
1087
1170
  if (input.index !== undefined) {
@@ -1111,7 +1194,7 @@ async function directNestedAsyncSteer(input: { target: ResolvedSubagentRunId & {
1111
1194
  const scheduled = { requestId, state: "scheduled" as const, sourceRunId: run.id, targets };
1112
1195
  return { content: [{ type: "text", text: `Steering scheduled for nested async run ${run.id} (request ${requestId}).` }], details: { mode: "management", results: [], steering: scheduled } };
1113
1196
  }
1114
- const waited = await waitForSteeringAction({ asyncDir, sourceRunId: run.id, requestId, timeoutMs: 3_000, signal: input.signal });
1197
+ const waited = await waitForSteeringAction(omitUndefinedProperties({ asyncDir, sourceRunId: run.id, requestId, timeoutMs: 3_000, signal: input.signal }));
1115
1198
  const result = waited ?? { requestId, state: "pending" as const, sourceRunId: run.id, targets };
1116
1199
  const stateText = result.state === "delivered" ? "delivered" : result.state === "failed" ? "failed" : result.state === "partial" ? "partial" : "pending";
1117
1200
  return { content: [{ type: "text", text: `Steering ${stateText} for nested async run ${run.id} (request ${requestId}).` }], ...(result.state === "failed" || result.state === "partial" ? { isError: true } : {}), details: { mode: "management", results: [], steering: result } };
@@ -1123,7 +1206,7 @@ async function interruptNestedRun(target: ResolvedSubagentRunId & { kind: "neste
1123
1206
  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: [] } };
1124
1207
  if (run.state === "paused") return { content: [{ type: "text", text: `Nested run ${run.id} is already paused.` }], isError: true, details: { mode: "management", results: [] } };
1125
1208
  const result = await sendNestedControlRequest(target, "interrupt");
1126
- if (result) return { content: [{ type: "text", text: result.message }], isError: result.ok ? undefined : true, details: { mode: "management", results: [] } };
1209
+ if (result) return { content: [{ type: "text", text: result.message }], ...(result.ok ? {} : { isError: true }), details: { mode: "management", results: [] } };
1127
1210
  const direct = directNestedAsyncInterrupt(target);
1128
1211
  if (direct) return direct;
1129
1212
  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: [] } };
@@ -1132,7 +1215,7 @@ async function interruptNestedRun(target: ResolvedSubagentRunId & { kind: "neste
1132
1215
  async function resumeLiveNestedRun(input: { target: ResolvedSubagentRunId & { kind: "nested" }; message: string }): Promise<AgentToolResult<Details>> {
1133
1216
  const run = input.target.match.run;
1134
1217
  const result = await sendNestedControlRequest(input.target, "resume", input.message);
1135
- if (result) return { content: [{ type: "text", text: result.message }], isError: result.ok ? undefined : true, details: { mode: "management", results: [] } };
1218
+ if (result) return { content: [{ type: "text", text: result.message }], ...(result.ok ? {} : { isError: true }), details: { mode: "management", results: [] } };
1136
1219
  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: [] } };
1137
1220
  }
1138
1221
 
@@ -1144,6 +1227,15 @@ async function steerNestedRun(input: { target: ResolvedSubagentRunId & { kind: "
1144
1227
  return { content: [{ type: "text", text: `Nested run ${run.id} is not a live async Pi child session with a steering inbox. action='steer' cannot target foreground nested runs.` }], isError: true, details: { mode: "management", results: [] } };
1145
1228
  }
1146
1229
 
1230
+ function externalRunnerControlError(asyncDir: string, action: "steer" | "resume"): AgentToolResult<Details> | undefined {
1231
+ const status = readStatus(asyncDir);
1232
+ if (!status?.steps?.length || !status.steps.every((step) => step.runner?.type === "external-cli")) return undefined;
1233
+ const message = action === "steer"
1234
+ ? "One-shot external CLI runners do not accept live steer messages."
1235
+ : "One-shot external CLI runners do not persist sessions and cannot be resumed.";
1236
+ return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
1237
+ }
1238
+
1147
1239
  async function resumeAsyncRun(input: {
1148
1240
  params: SubagentParamsLike;
1149
1241
  requestCwd: string;
@@ -1168,7 +1260,7 @@ async function resumeAsyncRun(input: {
1168
1260
  details: { mode: "management", results: [] },
1169
1261
  };
1170
1262
  }
1171
- const acceptanceErrors = validateExecutionAcceptance(input.params);
1263
+ const acceptanceErrors = validateExecutionAcceptance(input.params as Parameters<typeof validateExecutionAcceptance>[0]);
1172
1264
  if (acceptanceErrors.length > 0) {
1173
1265
  return {
1174
1266
  content: [{ type: "text", text: `Cannot resume: ${acceptanceErrors.join(" ")}` }],
@@ -1184,7 +1276,7 @@ async function resumeAsyncRun(input: {
1184
1276
  const requestedId = input.params.id ?? input.params.runId;
1185
1277
  let resolved: ResolvedSubagentRunId | undefined;
1186
1278
  try {
1187
- resolved = requestedId ? resolveSubagentRunId(requestedId, { state: input.deps.state, nested: nestedResolutionScopeForExecutor(input.deps) }) : undefined;
1279
+ resolved = requestedId ? resolveSubagentRunId(requestedId, omitUndefinedProperties({ state: input.deps.state, nested: nestedResolutionScopeForExecutor(input.deps) })) : undefined;
1188
1280
  } catch (error) {
1189
1281
  const message = error instanceof Error ? error.message : "";
1190
1282
  const asyncMatches = message.match(/async:/g)?.length ?? 0;
@@ -1207,6 +1299,10 @@ async function resumeAsyncRun(input: {
1207
1299
  ];
1208
1300
  target = resolveNestedResumeTarget(resolved, trustedSessionRoots);
1209
1301
  } else {
1302
+ if (resolved?.kind === "async" && resolved.location.asyncDir) {
1303
+ const unsupported = externalRunnerControlError(resolved.location.asyncDir, "resume");
1304
+ if (unsupported) return unsupported;
1305
+ }
1210
1306
  target = resolveResumeTarget(input.params, input.deps.state, { asyncRequireSessionFile: !attachChain });
1211
1307
  }
1212
1308
  } catch (error) {
@@ -1288,7 +1384,7 @@ async function resumeAsyncRun(input: {
1288
1384
  };
1289
1385
  }
1290
1386
  const runId = randomUUID().slice(0, 8);
1291
- const artifactConfig: ArtifactConfig = { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false, dir: input.deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir };
1387
+ const artifactConfig: ArtifactConfig = omitUndefinedProperties({ ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false, dir: input.deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir });
1292
1388
  const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo);
1293
1389
  const contextPolicy = resolveExplicitContextPolicy(input.params);
1294
1390
  const workflowTask = (input.params.task ?? followUp) || undefined;
@@ -1296,20 +1392,20 @@ async function resumeAsyncRun(input: {
1296
1392
  const chain = wrapChainTasksForFork(attachChain, contextPolicy);
1297
1393
  const normalized = normalizeSkillInput(input.params.skill);
1298
1394
  const parentModel = input.parentModel;
1299
- const result = executeAsyncChain(runId, {
1395
+ const result = executeAsyncChain(runId, compactOptional<Parameters<typeof executeAsyncChain>[1]>({
1300
1396
  chain,
1301
1397
  task: workflowTask,
1302
1398
  goal,
1303
1399
  attachRoot: {
1304
1400
  runId: target.runId,
1305
- asyncDir: target.asyncDir ?? path.join(ASYNC_DIR, target.runId),
1306
- resultPath: resolveAsyncRootResultPath(RESULTS_DIR, target.runId),
1401
+ asyncDir: target.asyncDir ?? path.join(DIRS.async, target.runId),
1402
+ resultPath: resolveAsyncRootResultPath(DIRS.results, target.runId),
1307
1403
  index: target.index,
1308
1404
  agent: target.agent,
1309
1405
  label: `Attached ${target.runId}`,
1310
1406
  },
1311
1407
  agents,
1312
- ctx: {
1408
+ ctx: compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
1313
1409
  pi: input.deps.pi,
1314
1410
  cwd: input.requestCwd,
1315
1411
  currentSessionId: input.deps.state.currentSessionId,
@@ -1318,7 +1414,8 @@ async function resumeAsyncRun(input: {
1318
1414
  currentModel: parentModel,
1319
1415
  modelScope,
1320
1416
  interactive: input.ctx.hasUI,
1321
- },
1417
+ permissions: input.deps.config.permissions,
1418
+ }),
1322
1419
  availableModels,
1323
1420
  cwd: effectiveCwd,
1324
1421
  maxOutput: input.params.maxOutput,
@@ -1339,7 +1436,7 @@ async function resumeAsyncRun(input: {
1339
1436
  childIntercomTarget: intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
1340
1437
  globalConcurrencyLimit: input.deps.config.globalConcurrencyLimit,
1341
1438
  capabilityCeiling: intersectSubagentCapabilityCeilings("capabilityCeiling" in target ? target.capabilityCeiling : undefined, resolveCurrentSubagentCapabilityCeiling(input.deps.state.currentSessionId)),
1342
- });
1439
+ }));
1343
1440
  if (result.isError) return result;
1344
1441
  const attachedId = result.details.asyncId ?? runId;
1345
1442
  const lines = [
@@ -1352,18 +1449,22 @@ async function resumeAsyncRun(input: {
1352
1449
  return { content: [{ type: "text", text: formatAsyncStartedMessage(lines.join("\n"), input.ctx.hasUI) }], details: result.details };
1353
1450
  }
1354
1451
 
1452
+ const revivalSessionFile = target.sessionFile;
1453
+ if (!revivalSessionFile) {
1454
+ return { content: [{ type: "text", text: `Async child '${target.runId}' has no persisted session file to resume.` }], isError: true, details: { mode: "management", results: [] } };
1455
+ }
1355
1456
  const runId = randomUUID().slice(0, 8);
1356
1457
  const recoveryAgentConfig = recoveryDescriptor ? applySteeringRecoveryAgentConfig(agentConfig, recoveryDescriptor) : agentConfig;
1357
- const artifactConfig: ArtifactConfig = recoveryDescriptor?.artifactConfig ?? { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false, dir: input.deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir };
1458
+ const artifactConfig: ArtifactConfig = recoveryDescriptor?.artifactConfig ?? omitUndefinedProperties({ ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false, dir: input.deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir });
1358
1459
  const artifactsDir = recoveryDescriptor?.artifactsDir ?? getArtifactsDir(parentSessionFile, effectiveCwd, artifactConfig.dir);
1359
1460
  const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo);
1360
1461
  const parentModel = input.parentModel;
1361
- const result = executeAsyncSingle(runId, {
1462
+ const result = executeAsyncSingle(runId, compactOptional<Parameters<typeof executeAsyncSingle>[1]>({
1362
1463
  agent: target.agent,
1363
- task: buildRevivedAsyncTask(target, followUp),
1464
+ task: buildRevivedAsyncTask(target as Parameters<typeof buildRevivedAsyncTask>[0], followUp),
1364
1465
  goal: followUp,
1365
1466
  agentConfig: recoveryAgentConfig,
1366
- ctx: {
1467
+ ctx: compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
1367
1468
  pi: input.deps.pi,
1368
1469
  cwd: input.requestCwd,
1369
1470
  currentSessionId: input.deps.state.currentSessionId,
@@ -1372,17 +1473,18 @@ async function resumeAsyncRun(input: {
1372
1473
  currentModel: parentModel,
1373
1474
  modelScope,
1374
1475
  interactive: input.ctx.hasUI,
1375
- },
1476
+ permissions: input.deps.config.permissions,
1477
+ }),
1376
1478
  cwd: effectiveCwd,
1377
1479
  maxOutput: input.params.maxOutput ?? recoveryDescriptor?.maxOutput,
1378
1480
  artifactsDir,
1379
1481
  artifactConfig,
1380
1482
  shareEnabled: recoveryDescriptor?.share ?? input.params.share === true,
1381
- sessionRoot: input.deps.getSubagentSessionRoot(parentSessionFile),
1483
+ sessionRoot: input.deps.getSubagentSessionRoot(parentSessionFile ?? revivalSessionFile),
1382
1484
  ...(recoveryDescriptor?.sessionDir ? { sessionDir: recoveryDescriptor.sessionDir } : {}),
1383
- sessionFile: target.sessionFile,
1485
+ sessionFile: revivalSessionFile,
1384
1486
  revivalLease: {
1385
- sessionFile: target.sessionFile,
1487
+ sessionFile: revivalSessionFile,
1386
1488
  runId,
1387
1489
  sourceRunId: target.runId,
1388
1490
  ...(input.deps.state.currentSessionId ? { parentSessionId: input.deps.state.currentSessionId } : {}),
@@ -1410,7 +1512,7 @@ async function resumeAsyncRun(input: {
1410
1512
  ...(input.params.turnBudget !== undefined ? { turnBudget: input.params.turnBudget } : {}),
1411
1513
  ...(input.params.toolBudget !== undefined ? { toolBudget: input.params.toolBudget } : {}),
1412
1514
  capabilityCeiling: intersectSubagentCapabilityCeilings("capabilityCeiling" in target ? target.capabilityCeiling : undefined, recoveryDescriptor?.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(input.deps.state.currentSessionId)),
1413
- });
1515
+ }));
1414
1516
  if (result.isError) return result;
1415
1517
 
1416
1518
  const revivedId = result.details.asyncId ?? runId;
@@ -1467,6 +1569,25 @@ function createForegroundControlNotifier(data: Pick<ExecutionContextData, "contr
1467
1569
  };
1468
1570
  }
1469
1571
 
1572
+ export function foregroundResultIntercomStatus(result: SingleResult): ReturnType<typeof resolveSubagentResultStatus> {
1573
+ return resolveSubagentResultStatus(omitUndefinedProperties({
1574
+ exitCode: result.exitCode,
1575
+ ...(result.acceptance?.status === "rejected" ? { success: false } : {}),
1576
+ interrupted: result.interrupted,
1577
+ detached: result.detached,
1578
+ processSignal: result.processSignal,
1579
+ timedOut: result.timedOut,
1580
+ stopped: result.stopped,
1581
+ turnBudgetExceeded: result.turnBudgetExceeded,
1582
+ }));
1583
+ }
1584
+
1585
+ export function shouldSuppressRoutineResultIntercom(input: { suppressRoutineResultIntercom?: boolean; results: SingleResult[] }): boolean {
1586
+ return input.suppressRoutineResultIntercom === true
1587
+ && input.results.length > 0
1588
+ && input.results.every((result) => foregroundResultIntercomStatus(result) === "completed");
1589
+ }
1590
+
1470
1591
  async function emitForegroundResultIntercom(input: {
1471
1592
  pi: ExtensionAPI;
1472
1593
  intercomBridge: IntercomBridgeState;
@@ -1478,24 +1599,16 @@ async function emitForegroundResultIntercom(input: {
1478
1599
  parallelHandoff?: Details["parallelHandoff"];
1479
1600
  }): Promise<ReturnType<typeof buildSubagentResultIntercomPayload> | null> {
1480
1601
  if (!input.intercomBridge.active || !input.intercomBridge.resultDelivery || !input.intercomBridge.orchestratorTarget) return null;
1481
- const children = input.results.flatMap((result, index) => result.detached ? [] : [{
1602
+ const children = input.results.flatMap((result, index) => result.detached ? [] : [omitUndefinedProperties({
1482
1603
  agent: result.agent,
1483
- status: resolveSubagentResultStatus({
1484
- exitCode: result.exitCode,
1485
- interrupted: result.interrupted,
1486
- detached: result.detached,
1487
- processSignal: result.processSignal,
1488
- timedOut: result.timedOut,
1489
- stopped: result.stopped,
1490
- turnBudgetExceeded: result.turnBudgetExceeded,
1491
- }),
1604
+ status: foregroundResultIntercomStatus(result),
1492
1605
  outputState: result.outputState ?? "unknown",
1493
1606
  summary: resultSummaryForIntercom(result),
1494
1607
  index,
1495
1608
  artifactPath: result.artifactPaths?.outputPath,
1496
1609
  sessionPath: result.sessionFile,
1497
1610
  intercomTarget: resolveSubagentIntercomTarget(input.runId, result.agent, index),
1498
- }]);
1611
+ })]);
1499
1612
  if (children.length === 0) return null;
1500
1613
  const payload = buildSubagentResultIntercomPayload({
1501
1614
  to: input.intercomBridge.orchestratorTarget,
@@ -1551,7 +1664,7 @@ function canonicalizeExecutionParams(params: SubagentParamsLike, agents: AgentCo
1551
1664
  if (params.agent) {
1552
1665
  const result = resolve(params.agent);
1553
1666
  if (result.error) return { error: result.error };
1554
- params = { ...params, agent: result.name };
1667
+ params = omitUndefinedProperties({ ...params, agent: result.name });
1555
1668
  }
1556
1669
  if (params.tasks) {
1557
1670
  const tasks: TaskParam[] = [];
@@ -1568,7 +1681,7 @@ function canonicalizeExecutionParams(params: SubagentParamsLike, agents: AgentCo
1568
1681
  for (let index = 0; index < params.chain.length; index++) {
1569
1682
  const step = params.chain[index]!;
1570
1683
  if (isParallelStep(step)) {
1571
- const parallel: typeof step.parallel = [];
1684
+ const parallel: ParallelTaskItem[] = [];
1572
1685
  for (let taskIndex = 0; taskIndex < step.parallel.length; taskIndex++) {
1573
1686
  const task = step.parallel[taskIndex]!;
1574
1687
  const result = resolve(task.agent, `step ${index + 1}, task ${taskIndex + 1}`);
@@ -1618,7 +1731,7 @@ function validateExecutionInput(
1618
1731
  };
1619
1732
  }
1620
1733
 
1621
- const acceptanceErrors = validateExecutionAcceptance(params);
1734
+ const acceptanceErrors = validateExecutionAcceptance(params as Parameters<typeof validateExecutionAcceptance>[0]);
1622
1735
  if (acceptanceErrors.length > 0) {
1623
1736
  return {
1624
1737
  content: [{ type: "text", text: acceptanceErrors.join(" ") }],
@@ -1707,7 +1820,7 @@ function validateExecutionInput(
1707
1820
  function validateExecutionChainBindings(params: SubagentParamsLike, dynamicFanoutMaxItems?: number): AgentToolResult<Details> | null {
1708
1821
  if ((params.chain?.length ?? 0) === 0) return null;
1709
1822
  try {
1710
- validateChainOutputBindingsWithContext(params.chain as ChainStep[], { maxItems: dynamicFanoutMaxItems });
1823
+ validateChainOutputBindingsWithContext(params.chain as ChainStep[], dynamicFanoutMaxItems === undefined ? {} : { maxItems: dynamicFanoutMaxItems });
1711
1824
  } catch (error) {
1712
1825
  if (error instanceof ChainOutputValidationError) {
1713
1826
  return {
@@ -1722,6 +1835,7 @@ function validateExecutionChainBindings(params: SubagentParamsLike, dynamicFanou
1722
1835
  }
1723
1836
 
1724
1837
  function getRequestedModeLabel(params: SubagentParamsLike): Details["mode"] {
1838
+ if (params.workflowScript !== undefined) return "workflow";
1725
1839
  if ((params.chain?.length ?? 0) > 0) return "chain";
1726
1840
  if ((params.tasks?.length ?? 0) > 0) return "parallel";
1727
1841
  if (params.agent) return "single";
@@ -1745,12 +1859,12 @@ function resolveAgentDefaultContextPolicy(params: SubagentParamsLike, agents: Ag
1745
1859
  const requestedAgentNames = collectRequestedAgentNames(params);
1746
1860
  const contextSummary = summarizeContextModes(requestedAgentNames.map((name) => contextForAgent(name)));
1747
1861
  const usesFork = contextSummary === "fork" || contextSummary === "mixed";
1748
- return {
1862
+ return omitUndefinedProperties({
1749
1863
  params,
1750
1864
  contextForAgent,
1751
1865
  contextSummary,
1752
1866
  usesFork,
1753
- };
1867
+ });
1754
1868
  }
1755
1869
 
1756
1870
  function resolveExplicitContextPolicy(params: SubagentParamsLike): AgentDefaultContextPolicy {
@@ -1826,7 +1940,8 @@ function resolveForegroundTimeout(params: SubagentParamsLike, defaultTimeoutMs?:
1826
1940
  if (rawTimeout !== undefined && rawMaxRuntime !== undefined && rawTimeout !== rawMaxRuntime) {
1827
1941
  return { error: "timeoutMs and maxRuntimeMs are aliases; provide only one value or use the same value for both." };
1828
1942
  }
1829
- return { timeoutMs: rawTimeout ?? rawMaxRuntime };
1943
+ const timeoutMs = rawTimeout ?? rawMaxRuntime;
1944
+ return timeoutMs === undefined ? {} : { timeoutMs };
1830
1945
  }
1831
1946
 
1832
1947
  function resolveToolBudget(
@@ -1835,7 +1950,7 @@ function resolveToolBudget(
1835
1950
  options?: { minimumHard?: 0 | 1 },
1836
1951
  ): { toolBudget?: ResolvedToolBudget; error?: string } {
1837
1952
  const resolved = validateToolBudgetConfig(raw, label, options);
1838
- return { toolBudget: resolved.budget, error: resolved.error };
1953
+ return { ...(resolved.budget === undefined ? {} : { toolBudget: resolved.budget }), ...(resolved.error === undefined ? {} : { error: resolved.error }) };
1839
1954
  }
1840
1955
 
1841
1956
  function resolveEffectiveToolBudget(input: { stepBudget?: ToolBudgetConfig; runBudget?: ResolvedToolBudget; agentBudget?: ToolBudgetConfig; configBudget?: ToolBudgetConfig }): { toolBudget?: ResolvedToolBudget; error?: string } {
@@ -1869,7 +1984,7 @@ function expandChainParallelCounts(chain: ChainStep[]): { chain?: ChainStep[]; e
1869
1984
  expandedChain.push(step);
1870
1985
  continue;
1871
1986
  }
1872
- const expandedParallel = [];
1987
+ const expandedParallel: ParallelTaskItem[] = [];
1873
1988
  for (let taskIndex = 0; taskIndex < step.parallel.length; taskIndex++) {
1874
1989
  const task = step.parallel[taskIndex]!;
1875
1990
  const rawCount = (task as typeof task & { count?: unknown }).count;
@@ -1892,14 +2007,14 @@ function normalizeRepeatedParallelCounts(params: SubagentParamsLike): { params?:
1892
2007
  if (expandedTasks.error) {
1893
2008
  return { error: buildRequestedModeError(params, expandedTasks.error) };
1894
2009
  }
1895
- return { params: { ...params, tasks: expandedTasks.tasks } };
2010
+ return { params: { ...params, ...(expandedTasks.tasks === undefined ? {} : { tasks: expandedTasks.tasks }) } };
1896
2011
  }
1897
2012
  if (params.chain) {
1898
2013
  const expandedChain = expandChainParallelCounts(params.chain);
1899
2014
  if (expandedChain.error) {
1900
2015
  return { error: buildRequestedModeError(params, expandedChain.error) };
1901
2016
  }
1902
- return { params: { ...params, chain: expandedChain.chain } };
2017
+ return { params: { ...params, ...(expandedChain.chain === undefined ? {} : { chain: expandedChain.chain }) } };
1903
2018
  }
1904
2019
  return { params };
1905
2020
  }
@@ -2005,6 +2120,87 @@ function collectChainThinkingOverrides(
2005
2120
  return thinkingOverrides;
2006
2121
  }
2007
2122
 
2123
+ type StaticLaunchSummary = { agent: string; model?: string; thinking?: string };
2124
+
2125
+ function resolveStaticLaunchSummary(input: {
2126
+ agent: string;
2127
+ index: number;
2128
+ explicitModel?: string;
2129
+ agents: AgentConfig[];
2130
+ parentModel?: ParentModel;
2131
+ availableModels: ModelInfo[];
2132
+ currentProvider?: string;
2133
+ modelScope?: ModelScopeConfig;
2134
+ thinkingOverrideForTask: ForkThinkingOverrideForTask;
2135
+ }): StaticLaunchSummary {
2136
+ const agentConfig = input.agents.find((agent) => agent.name === input.agent);
2137
+ const model = resolveEffectiveSubagentModel(
2138
+ input.explicitModel,
2139
+ agentConfig?.model,
2140
+ input.parentModel,
2141
+ input.availableModels,
2142
+ input.currentProvider,
2143
+ input.modelScope === undefined ? {} : { scope: input.modelScope },
2144
+ );
2145
+ const thinkingOverride = input.thinkingOverrideForTask(input.agent, input.index, model);
2146
+ const thinking = resolveEffectiveThinking(model, thinkingOverride ?? agentConfig?.thinking);
2147
+ return {
2148
+ agent: input.agent,
2149
+ ...(model ? { model } : {}),
2150
+ ...(thinking ? { thinking } : {}),
2151
+ };
2152
+ }
2153
+
2154
+ function collectStaticLaunchSummaries(input: {
2155
+ params: SubagentParamsLike;
2156
+ agents: AgentConfig[];
2157
+ parentModel?: ParentModel;
2158
+ availableModels: ModelInfo[];
2159
+ currentProvider?: string;
2160
+ modelScope?: ModelScopeConfig;
2161
+ thinkingOverrideForTask: ForkThinkingOverrideForTask;
2162
+ dynamicFanoutMaxItems?: number;
2163
+ }): StaticLaunchSummary[] {
2164
+ const summary = (agent: string, index: number, explicitModel?: string) => resolveStaticLaunchSummary({
2165
+ agent,
2166
+ index,
2167
+ explicitModel,
2168
+ agents: input.agents,
2169
+ parentModel: input.parentModel,
2170
+ availableModels: input.availableModels,
2171
+ currentProvider: input.currentProvider,
2172
+ modelScope: input.modelScope,
2173
+ thinkingOverrideForTask: input.thinkingOverrideForTask,
2174
+ });
2175
+ if (input.params.tasks) return input.params.tasks.map((task, index) => summary(task.agent, index, task.model));
2176
+ if (input.params.chain?.length) {
2177
+ const launches: StaticLaunchSummary[] = [];
2178
+ let flatIndex = 0;
2179
+ for (const step of input.params.chain) {
2180
+ if (isParallelStep(step)) {
2181
+ for (const task of step.parallel) {
2182
+ launches.push(summary(task.agent, flatIndex, task.model));
2183
+ flatIndex++;
2184
+ }
2185
+ continue;
2186
+ }
2187
+ if (isDynamicParallelStep(step)) {
2188
+ const maxItems = step.expand.maxItems ?? input.dynamicFanoutMaxItems ?? 0;
2189
+ for (let itemIndex = 0; itemIndex < maxItems; itemIndex++) {
2190
+ launches.push(summary(step.parallel.agent, flatIndex, step.parallel.model));
2191
+ flatIndex++;
2192
+ }
2193
+ continue;
2194
+ }
2195
+ const sequential = step as SequentialStep;
2196
+ launches.push(summary(sequential.agent, flatIndex, sequential.model));
2197
+ flatIndex++;
2198
+ }
2199
+ return launches;
2200
+ }
2201
+ return input.params.agent ? [summary(input.params.agent, 0, input.params.model as string | undefined)] : [];
2202
+ }
2203
+
2008
2204
  function firstChainAgent(chain: ChainStep[]): string | undefined {
2009
2205
  const first = chain[0];
2010
2206
  if (!first) return undefined;
@@ -2032,34 +2228,34 @@ function resolveAsyncEventGoal(workflowTask: string | undefined, rawChain: Chain
2032
2228
  function wrapChainTasksForFork(chain: ChainStep[], contextPolicy: AgentDefaultContextPolicy): ChainStep[] {
2033
2229
  return chain.map((step, stepIndex) => {
2034
2230
  if (isParallelStep(step)) {
2035
- return {
2231
+ return compactOptional<ParallelStep>({
2036
2232
  ...step,
2037
- parallel: step.parallel.map((task) => ({
2233
+ parallel: step.parallel.map((task) => compactOptional<ParallelTaskItem>({
2038
2234
  ...task,
2039
2235
  task: shouldForkAgent(contextPolicy, task.agent)
2040
2236
  ? wrapForkTask(task.task ?? "{previous}")
2041
2237
  : task.task,
2042
2238
  })),
2043
- };
2239
+ });
2044
2240
  }
2045
2241
  if (isDynamicParallelStep(step)) {
2046
- return {
2242
+ return compactOptional<DynamicParallelStep>({
2047
2243
  ...step,
2048
- parallel: {
2244
+ parallel: compactOptional<DynamicParallelStep["parallel"]>({
2049
2245
  ...step.parallel,
2050
2246
  task: shouldForkAgent(contextPolicy, step.parallel.agent)
2051
2247
  ? wrapForkTask(step.parallel.task ?? "{previous}")
2052
2248
  : step.parallel.task,
2053
- },
2054
- };
2249
+ }),
2250
+ });
2055
2251
  }
2056
2252
  const sequential = step as SequentialStep;
2057
- return {
2253
+ return compactOptional<SequentialStep>({
2058
2254
  ...sequential,
2059
2255
  task: shouldForkAgent(contextPolicy, sequential.agent)
2060
2256
  ? wrapForkTask(sequential.task ?? (stepIndex === 0 ? "{task}" : "{previous}"))
2061
2257
  : sequential.task,
2062
- };
2258
+ });
2063
2259
  });
2064
2260
  }
2065
2261
 
@@ -2159,16 +2355,17 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2159
2355
  }
2160
2356
  const id = randomUUID();
2161
2357
  const parentModel = data.parentModel;
2162
- const asyncCtx = {
2358
+ const asyncCtx = compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
2163
2359
  pi: deps.pi,
2164
2360
  cwd: ctx.cwd,
2165
- currentSessionId: deps.state.currentSessionId!,
2166
- parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
2361
+ currentSessionId: data.parentSessionId!,
2362
+ parentSessionId: data.parentPiSessionId,
2167
2363
  currentModelProvider: parentModel?.provider,
2168
2364
  currentModel: parentModel,
2169
2365
  modelScope: data.modelScope,
2170
2366
  interactive: ctx.hasUI,
2171
- };
2367
+ permissions: deps.config.permissions,
2368
+ });
2172
2369
  const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo);
2173
2370
  const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
2174
2371
  const currentProvider = parentModel?.provider;
@@ -2177,7 +2374,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2177
2374
 
2178
2375
  if (hasTasks && params.tasks) {
2179
2376
  const skillOverrides = params.tasks.map((task) => normalizeSkillInput(task.skill));
2180
- const parallelTasks = params.tasks.map((task, index) => ({
2377
+ const parallelTasks = params.tasks.map((task, index) => compactOptional<ParallelTaskItem>({
2181
2378
  agent: task.agent,
2182
2379
  task: shouldForkAgent(contextPolicy, task.agent) ? wrapForkTask(task.task) : task.task,
2183
2380
  cwd: task.cwd,
@@ -2192,12 +2389,12 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2192
2389
  ...(task.agentContract !== undefined ? { agentContract: task.agentContract } : {}),
2193
2390
  ...(task.acceptance !== undefined ? { acceptance: task.acceptance } : {}),
2194
2391
  }));
2195
- return executeAsyncChain(id, {
2196
- chain: [{
2392
+ return executeAsyncChain(id, compactOptional<Parameters<typeof executeAsyncChain>[1]>({
2393
+ chain: [compactOptional<ParallelStep>({
2197
2394
  parallel: parallelTasks,
2198
2395
  concurrency: resolveTopLevelParallelConcurrency(params.concurrency, deps.config.parallel?.concurrency),
2199
2396
  worktree: params.worktree,
2200
- }],
2397
+ })],
2201
2398
  resultMode: "parallel",
2202
2399
  goal: params.tasks[0]?.task ?? "",
2203
2400
  agents,
@@ -2230,7 +2427,9 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2230
2427
  configToolBudget: data.configToolBudget,
2231
2428
  capabilityCeiling: data.capabilityCeiling,
2232
2429
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2233
- });
2430
+ parentWorkflowRunId: params.workflowParentRunId,
2431
+ workflowKey: params.workflowKey,
2432
+ }));
2234
2433
  }
2235
2434
 
2236
2435
  if (hasChain && params.chain) {
@@ -2238,7 +2437,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2238
2437
  const chainSkills = normalized === false ? [] : (normalized ?? []);
2239
2438
  const rawChain = params.chain as ChainStep[];
2240
2439
  const chain = wrapChainTasksForFork(rawChain, contextPolicy);
2241
- return executeAsyncChain(id, {
2440
+ return executeAsyncChain(id, compactOptional<Parameters<typeof executeAsyncChain>[1]>({
2242
2441
  chain,
2243
2442
  task: params.task,
2244
2443
  goal: resolveAsyncEventGoal(params.task, rawChain),
@@ -2273,7 +2472,9 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2273
2472
  configToolBudget: data.configToolBudget,
2274
2473
  capabilityCeiling: data.capabilityCeiling,
2275
2474
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2276
- });
2475
+ parentWorkflowRunId: params.workflowParentRunId,
2476
+ workflowKey: params.workflowKey,
2477
+ }));
2277
2478
  }
2278
2479
 
2279
2480
  if (hasSingle) {
@@ -2291,8 +2492,8 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2291
2492
  const normalizedSkills = normalizeSkillInput(params.skill);
2292
2493
  const skills = normalizedSkills === false ? [] : normalizedSkills;
2293
2494
  const maxSubagentDepth = resolveChildMaxSubagentDepth(currentMaxSubagentDepth, a.maxSubagentDepth);
2294
- const modelOverride = resolveEffectiveSubagentModel(params.model as string | undefined, a.model, parentModel, availableModels, currentProvider, { scope: data.modelScope });
2295
- return executeAsyncSingle(id, {
2495
+ const modelOverride = resolveEffectiveSubagentModel(params.model as string | undefined, a.model, parentModel, availableModels, currentProvider, data.modelScope === undefined ? {} : { scope: data.modelScope });
2496
+ return executeAsyncSingle(id, compactOptional<Parameters<typeof executeAsyncSingle>[1]>({
2296
2497
  agent: params.agent!,
2297
2498
  task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(params.task ?? "") : (params.task ?? ""),
2298
2499
  goal: params.task ?? "",
@@ -2331,7 +2532,9 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2331
2532
  usageBudget: data.usageBudget,
2332
2533
  configToolBudget: data.configToolBudget,
2333
2534
  capabilityCeiling: data.capabilityCeiling,
2334
- });
2535
+ parentWorkflowRunId: params.workflowParentRunId,
2536
+ workflowKey: params.workflowKey,
2537
+ }));
2335
2538
  }
2336
2539
 
2337
2540
  return null;
@@ -2364,8 +2567,8 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2364
2567
  const chainSkills = normalized === false ? [] : (normalized ?? []);
2365
2568
  const chain = wrapChainTasksForFork(params.chain as ChainStep[], contextPolicy);
2366
2569
  const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
2367
- const chainCtx = normalizeParentModel(ctx.model) || !data.parentModel ? ctx : { ...ctx, model: data.parentModel };
2368
- const chainResult = await executeChain({
2570
+ const chainCtx = normalizeParentModel(ctx.model) || !data.parentModel ? ctx : ({ ...ctx, model: data.parentModel } as ExtensionContext);
2571
+ const chainResult = await executeChain(compactOptional<Parameters<typeof executeChain>[0]>({
2369
2572
  chain,
2370
2573
  task: params.task,
2371
2574
  agents,
@@ -2394,7 +2597,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2394
2597
  foregroundControl,
2395
2598
  nestedRoute: foregroundControl?.nestedRoute,
2396
2599
  chainSkills,
2397
- chainDir: params.chainDir ?? getProjectChainRunsDir(effectiveCwd),
2600
+ chainDir: params.chainDir ?? getChainRunsDir(effectiveCwd, artifactConfig.dir),
2398
2601
  dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
2399
2602
  maxSubagentDepth: currentMaxSubagentDepth,
2400
2603
  worktreeSetupHook: deps.config.worktreeSetupHook,
@@ -2416,9 +2619,10 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2416
2619
  toolBudget: data.toolBudget,
2417
2620
  usageBudget: data.usageBudget,
2418
2621
  configToolBudget: data.configToolBudget,
2622
+ permissions: deps.config.permissions,
2419
2623
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2420
2624
  capabilityCeiling: data.capabilityCeiling,
2421
- });
2625
+ }));
2422
2626
 
2423
2627
  if (chainResult.requestedAsync) {
2424
2628
  if (!isAsyncAvailable()) {
@@ -2430,20 +2634,21 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2430
2634
  }
2431
2635
  const id = randomUUID();
2432
2636
  const parentModel = data.parentModel;
2433
- const asyncCtx = {
2637
+ const asyncCtx = compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
2434
2638
  pi: deps.pi,
2435
2639
  cwd: ctx.cwd,
2436
- currentSessionId: deps.state.currentSessionId!,
2437
- parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
2640
+ currentSessionId: data.parentSessionId!,
2641
+ parentSessionId: data.parentPiSessionId,
2438
2642
  currentModelProvider: parentModel?.provider,
2439
2643
  currentModel: parentModel,
2440
2644
  modelScope: data.modelScope,
2441
2645
  interactive: ctx.hasUI,
2442
- };
2646
+ permissions: deps.config.permissions,
2647
+ });
2443
2648
  const rawAsyncChain = chainResult.requestedAsync.chain;
2444
2649
  const asyncChain = wrapChainTasksForFork(rawAsyncChain, contextPolicy);
2445
2650
  const firstAgent = firstChainAgent(rawAsyncChain);
2446
- return executeAsyncChain(id, {
2651
+ return executeAsyncChain(id, compactOptional<Parameters<typeof executeAsyncChain>[1]>({
2447
2652
  chain: asyncChain,
2448
2653
  task: params.task,
2449
2654
  goal: resolveAsyncEventGoal(params.task, rawAsyncChain, firstAgent ? shouldForkAgent(contextPolicy, firstAgent) : false),
@@ -2478,15 +2683,17 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2478
2683
  configToolBudget: data.configToolBudget,
2479
2684
  capabilityCeiling: data.capabilityCeiling,
2480
2685
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2481
- });
2686
+ }));
2482
2687
  }
2483
2688
 
2484
- const rawChainDetails = chainResult.details ? { ...chainResult.details, runId, timeoutMs: data.timeoutMs } : undefined;
2689
+ const rawChainDetails = chainResult.details ? compactOptional<Details>({ ...chainResult.details, runId, timeoutMs: data.timeoutMs }) : undefined;
2485
2690
  if (foregroundControl && rawChainDetails) {
2486
2691
  updateForegroundNestedProjection(foregroundControl);
2487
2692
  attachRootChildrenToSteps(runId, rawChainDetails.results, foregroundControl.nestedChildren);
2488
2693
  rawChainDetails.totalCost = sumResultsCost(rawChainDetails.results);
2489
- rawChainDetails.usageBudget = usageBudgetState(data.usageBudget, rawChainDetails.totalCost);
2694
+ const usageBudget = usageBudgetState(data.usageBudget, rawChainDetails.totalCost);
2695
+ if (usageBudget === undefined) delete rawChainDetails.usageBudget;
2696
+ else rawChainDetails.usageBudget = usageBudget;
2490
2697
  }
2491
2698
  const chainDetails = rawChainDetails ? compactForegroundDetails(rawChainDetails) : undefined;
2492
2699
  if (chainDetails) rememberForegroundRun(deps.state, { runId, mode: "chain", cwd: effectiveCwd, sessionId: data.parentSessionId, results: chainDetails.results, checkpoint: chainDetails.checkpoint });
@@ -2560,6 +2767,7 @@ interface ForegroundParallelRunInput {
2560
2767
  usageBudget?: UsageBudgetConfig;
2561
2768
  toolBudgets: (ResolvedToolBudget | undefined)[];
2562
2769
  agentContract?: AgentContract;
2770
+ permissions?: ExtensionConfig["permissions"];
2563
2771
  }
2564
2772
  function buildParallelModeError(message: string): AgentToolResult<Details> {
2565
2773
  return {
@@ -2581,13 +2789,13 @@ function createParallelWorktreeSetup(
2581
2789
  if (!enabled) return {};
2582
2790
  try {
2583
2791
  return {
2584
- setup: createWorktrees(cwd, runId, tasks.length, {
2792
+ setup: createWorktrees(cwd, runId, tasks.length, omitUndefinedProperties({
2585
2793
  agents: tasks.map((task) => task.agent),
2586
2794
  setupHook: setupHook
2587
- ? { hookPath: setupHook, timeoutMs: setupHookTimeoutMs }
2795
+ ? { hookPath: setupHook, ...(setupHookTimeoutMs === undefined ? {} : { timeoutMs: setupHookTimeoutMs }) }
2588
2796
  : undefined,
2589
2797
  baseDir,
2590
- }),
2798
+ })),
2591
2799
  };
2592
2800
  } catch (error) {
2593
2801
  const message = error instanceof Error ? error.message : String(error);
@@ -2643,39 +2851,41 @@ function finalizeParallelWorktreeHandoff(input: {
2643
2851
  }): { suffix: string; reference?: NonNullable<Details["parallelHandoff"]> } {
2644
2852
  const diffsDir = path.join(input.artifactsDir, "worktree-diffs", input.runId);
2645
2853
  const diffs = diffWorktrees(input.worktreeSetup, input.tasks.map((task) => task.agent), diffsDir);
2646
- const cleanup = cleanupWorktrees(input.worktreeSetup);
2647
2854
  const diffSummary = formatWorktreeDiffSummary(diffs);
2648
- try {
2649
- const reference = writeParallelHandoffGroup({
2650
- manifestPath: parallelHandoffPath(input.artifactsDir, input.runId),
2651
- runId: input.runId,
2652
- mode: "parallel",
2653
- source: "foreground",
2654
- cwd: input.cwd,
2655
- stepIndex: 0,
2656
- flatStartIndex: 0,
2657
- setup: input.worktreeSetup,
2658
- diffs,
2659
- cleanup,
2660
- results: input.results.map((result) => ({
2661
- agent: result.agent,
2662
- status: resolveSubagentResultStatus({
2663
- exitCode: result.exitCode,
2664
- interrupted: result.interrupted,
2665
- detached: result.detached,
2666
- state: result.stopped ? "stopped" : undefined,
2667
- processSignal: result.processSignal,
2668
- timedOut: result.timedOut,
2669
- stopped: result.stopped,
2670
- turnBudgetExceeded: result.turnBudgetExceeded,
2671
- }),
2672
- summary: resultSummaryForIntercom(result),
2673
- ...(result.artifactPaths?.outputPath ? { outputPath: result.artifactPaths.outputPath } : {}),
2674
- ...(result.structuredOutput !== undefined ? { structuredOutput: result.structuredOutput } : {}),
2675
- ...(result.structuredOutputPath ? { structuredOutputPath: result.structuredOutputPath } : {}),
2676
- ...(result.sessionFile ? { sessionPath: result.sessionFile } : {}),
2855
+ const manifestPath = parallelHandoffPath(input.artifactsDir, input.runId);
2856
+ const handoff = {
2857
+ manifestPath,
2858
+ runId: input.runId,
2859
+ mode: "parallel" as const,
2860
+ source: "foreground" as const,
2861
+ cwd: input.cwd,
2862
+ stepIndex: 0,
2863
+ flatStartIndex: 0,
2864
+ setup: input.worktreeSetup,
2865
+ diffs,
2866
+ results: input.results.map((result) => ({
2867
+ agent: result.agent,
2868
+ status: resolveSubagentResultStatus(omitUndefinedProperties({
2869
+ exitCode: result.exitCode,
2870
+ interrupted: result.interrupted,
2871
+ detached: result.detached,
2872
+ state: result.stopped ? "stopped" : undefined,
2873
+ processSignal: result.processSignal,
2874
+ timedOut: result.timedOut,
2875
+ stopped: result.stopped,
2876
+ turnBudgetExceeded: result.turnBudgetExceeded,
2677
2877
  })),
2678
- });
2878
+ summary: resultSummaryForIntercom(result),
2879
+ ...(result.artifactPaths?.outputPath ? { outputPath: result.artifactPaths.outputPath } : {}),
2880
+ ...(result.structuredOutput !== undefined ? { structuredOutput: result.structuredOutput } : {}),
2881
+ ...(result.structuredOutputPath ? { structuredOutputPath: result.structuredOutputPath } : {}),
2882
+ ...(result.sessionFile ? { sessionPath: result.sessionFile } : {}),
2883
+ })),
2884
+ };
2885
+ try {
2886
+ writeParallelHandoffGroup(handoff);
2887
+ const cleanup = cleanupWorktrees(input.worktreeSetup, { kind: "preserve", capturedDiffs: diffs, handoffManifestPath: manifestPath });
2888
+ const reference = writeParallelHandoffGroup({ ...handoff, cleanup });
2679
2889
  return {
2680
2890
  suffix: [diffSummary, formatParallelHandoffReference(reference)].filter(Boolean).join("\n\n"),
2681
2891
  reference,
@@ -2755,7 +2965,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
2755
2965
  beginForegroundChild(input.foregroundControl, {
2756
2966
  index,
2757
2967
  agent: task.agent,
2758
- description: input.taskDescriptions[index],
2968
+ ...(input.taskDescriptions[index] === undefined ? {} : { description: input.taskDescriptions[index] }),
2759
2969
  ...(model ? { model } : {}),
2760
2970
  ...(thinking ? { thinking } : {}),
2761
2971
  interrupt: () => {
@@ -2769,7 +2979,8 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
2769
2979
  ? createStructuredOutputRuntime(task.outputSchema, path.join(input.artifactsDir, "structured-output", input.runId))
2770
2980
  : undefined;
2771
2981
  let detachedReceipt = false;
2772
- const result = await runSync(input.ctx.cwd, input.agents, task.agent, taskText, {
2982
+ const result = await runSync(input.ctx.cwd, input.agents, task.agent, taskText, compactOptional<Parameters<typeof runSync>[4]>({
2983
+ permissions: input.permissions,
2773
2984
  parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
2774
2985
  context: input.contextPolicy.contextForAgent(task.agent),
2775
2986
  cwd: taskCwd,
@@ -2833,17 +3044,17 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
2833
3044
  const mergedProgress = input.liveProgress.filter((progress): progress is AgentProgress => progress !== undefined);
2834
3045
  input.onUpdate?.({
2835
3046
  content: progressUpdate.content,
2836
- details: {
3047
+ details: compactOptional<Details>({
2837
3048
  mode: "parallel",
2838
3049
  results: mergedResults,
2839
3050
  progress: mergedProgress,
2840
3051
  controlEvents: progressUpdate.details?.controlEvents,
2841
3052
  totalSteps: input.tasks.length,
2842
- },
3053
+ }),
2843
3054
  });
2844
3055
  }
2845
3056
  : undefined,
2846
- }).then((result) => {
3057
+ })).then((result) => {
2847
3058
  detachedReceipt = result.detached === true;
2848
3059
  return result;
2849
3060
  }).finally(() => {
@@ -2918,7 +3129,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2918
3129
  );
2919
3130
  const toolBudgets: (ResolvedToolBudget | undefined)[] = [];
2920
3131
  for (let index = 0; index < tasks.length; index++) {
2921
- const resolved = resolveEffectiveToolBudget({ stepBudget: tasks[index]?.toolBudget, runBudget: data.toolBudget, agentBudget: agentConfigs[index]?.toolBudget, configBudget: data.configToolBudget });
3132
+ const resolved = resolveEffectiveToolBudget(omitUndefinedProperties({ stepBudget: tasks[index]?.toolBudget, runBudget: data.toolBudget, agentBudget: agentConfigs[index]?.toolBudget, configBudget: data.configToolBudget }));
2922
3133
  if (resolved.error) return buildParallelModeError(resolved.error);
2923
3134
  toolBudgets.push(resolved.toolBudget);
2924
3135
  }
@@ -2944,7 +3155,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2944
3155
  ...(task.model !== undefined ? { model: task.model } : {}),
2945
3156
  }));
2946
3157
  const modelOverrides: (string | undefined)[] = tasks.map((_, i) =>
2947
- resolveEffectiveSubagentModel(behaviorOverrides[i]?.model, agentConfigs[i]?.model, parentModel, availableModels, currentProvider, { scope: data.modelScope }),
3158
+ resolveEffectiveSubagentModel(behaviorOverrides[i]?.model, agentConfigs[i]?.model, parentModel, availableModels, currentProvider, data.modelScope === undefined ? {} : { scope: data.modelScope }),
2948
3159
  );
2949
3160
 
2950
3161
  if (params.clarify === true && ctx.hasUI) {
@@ -2979,7 +3190,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2979
3190
  for (let i = 0; i < result.behaviorOverrides.length; i++) {
2980
3191
  const override = result.behaviorOverrides[i];
2981
3192
  if (override?.model !== undefined) {
2982
- modelOverrides[i] = resolveEffectiveSubagentModel(override.model, agentConfigs[i]?.model, parentModel, availableModels, currentProvider, { scope: data.modelScope });
3193
+ modelOverrides[i] = resolveEffectiveSubagentModel(override.model, agentConfigs[i]?.model, parentModel, availableModels, currentProvider, data.modelScope === undefined ? {} : { scope: data.modelScope });
2983
3194
  behaviorOverrides[i]!.model = override.model;
2984
3195
  }
2985
3196
  if (override?.output !== undefined) behaviorOverrides[i]!.output = override.output;
@@ -3000,20 +3211,21 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3000
3211
  };
3001
3212
  }
3002
3213
  const id = randomUUID();
3003
- const asyncCtx = {
3214
+ const asyncCtx = compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
3004
3215
  pi: deps.pi,
3005
3216
  cwd: ctx.cwd,
3006
- currentSessionId: deps.state.currentSessionId!,
3007
- parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
3217
+ currentSessionId: data.parentSessionId!,
3218
+ parentSessionId: data.parentPiSessionId,
3008
3219
  currentModelProvider: parentModel?.provider,
3009
3220
  currentModel: parentModel,
3010
3221
  modelScope: data.modelScope,
3011
3222
  interactive: ctx.hasUI,
3012
- };
3223
+ permissions: deps.config.permissions,
3224
+ });
3013
3225
  const parallelTasks = tasks.map((t, i) => {
3014
3226
  const taskText = shouldForkAgent(contextPolicy, t.agent) ? wrapForkTask(taskTexts[i]!) : taskTexts[i]!;
3015
3227
  const progress = taskDisallowsFileUpdates(taskText) ? false : behaviorOverrides[i]?.progress;
3016
- return {
3228
+ return compactOptional<ParallelTaskItem>({
3017
3229
  agent: t.agent,
3018
3230
  task: taskText,
3019
3231
  cwd: t.cwd,
@@ -3027,10 +3239,10 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3027
3239
  ...(t.outputSchema !== undefined ? { outputSchema: t.outputSchema } : {}),
3028
3240
  ...(t.acceptance !== undefined ? { acceptance: t.acceptance } : {}),
3029
3241
  ...(t.agentContract !== undefined ? { agentContract: t.agentContract } : {}),
3030
- };
3242
+ });
3031
3243
  });
3032
- return executeAsyncChain(id, {
3033
- chain: [{ parallel: parallelTasks, concurrency: parallelConcurrency, worktree: params.worktree }],
3244
+ return executeAsyncChain(id, compactOptional<Parameters<typeof executeAsyncChain>[1]>({
3245
+ chain: [compactOptional<ParallelStep>({ parallel: parallelTasks, concurrency: parallelConcurrency, worktree: params.worktree })],
3034
3246
  resultMode: "parallel",
3035
3247
  goal: taskTexts[0] ?? "",
3036
3248
  agents,
@@ -3062,7 +3274,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3062
3274
  toolBudget: data.toolBudget,
3063
3275
  configToolBudget: data.configToolBudget,
3064
3276
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
3065
- });
3277
+ }));
3066
3278
  }
3067
3279
  }
3068
3280
 
@@ -3090,15 +3302,27 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3090
3302
 
3091
3303
  let worktreeFinalized = false;
3092
3304
  try {
3305
+ if (worktreeSetup) {
3306
+ writePendingParallelHandoff({
3307
+ manifestPath: parallelHandoffPath(artifactsDir, runId),
3308
+ runId,
3309
+ mode: "parallel",
3310
+ source: "foreground",
3311
+ cwd: effectiveCwd,
3312
+ stepIndex: 0,
3313
+ flatStartIndex: 0,
3314
+ setup: worktreeSetup,
3315
+ });
3316
+ }
3093
3317
  const outputBaseDir = path.join(artifactsDir, "outputs", runId);
3094
- const duplicateOutputError = findDuplicateParallelOutputPath({
3318
+ const duplicateOutputError = findDuplicateParallelOutputPath(omitUndefinedProperties({
3095
3319
  tasks,
3096
3320
  behaviors,
3097
3321
  paramsCwd: effectiveCwd,
3098
3322
  ctxCwd: ctx.cwd,
3099
3323
  outputBaseDir,
3100
3324
  worktreeSetup,
3101
- });
3325
+ }));
3102
3326
  if (duplicateOutputError) return buildParallelModeError(duplicateOutputError);
3103
3327
  for (let index = 0; index < tasks.length; index++) {
3104
3328
  const taskCwd = resolveParallelTaskCwd(tasks[index]!, effectiveCwd, worktreeSetup, index);
@@ -3117,8 +3341,9 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3117
3341
  }
3118
3342
 
3119
3343
  const deadlineAt = data.deadlineAt ?? (data.timeoutMs !== undefined ? Date.now() + data.timeoutMs : undefined);
3120
- const results = await runForegroundParallelTasks({
3344
+ const results = await runForegroundParallelTasks(compactOptional<ForegroundParallelRunInput>({
3121
3345
  tasks,
3346
+ permissions: deps.config.permissions,
3122
3347
  taskTexts,
3123
3348
  taskDescriptions,
3124
3349
  agents,
@@ -3166,7 +3391,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3166
3391
  usageBudget: data.usageBudget,
3167
3392
  toolBudgets,
3168
3393
  agentContract: params.agentContract,
3169
- });
3394
+ }));
3170
3395
  for (let i = 0; i < results.length; i++) {
3171
3396
  const run = results[i]!;
3172
3397
  recordRun(run.agent, taskTexts[i]!, run.exitCode, run.progressSummary?.durationMs ?? 0);
@@ -3188,7 +3413,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3188
3413
  }
3189
3414
  const interrupted = results.find((result) => result.interrupted);
3190
3415
  const totalCost = sumResultsCost(results);
3191
- const details = compactForegroundDetails({
3416
+ const details = compactForegroundDetails(compactOptional<Details>({
3192
3417
  mode: "parallel",
3193
3418
  runId,
3194
3419
  timeoutMs: data.timeoutMs,
@@ -3199,7 +3424,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3199
3424
  totalCost,
3200
3425
  usageBudget: usageBudgetState(data.usageBudget, totalCost),
3201
3426
  ...(handoff?.reference ? { parallelHandoff: handoff.reference } : {}),
3202
- });
3427
+ }));
3203
3428
  rememberForegroundRun(deps.state, { runId, mode: "parallel", cwd: effectiveCwd, sessionId: data.parentSessionId, results: details.results });
3204
3429
  if (interrupted) {
3205
3430
  return {
@@ -3216,20 +3441,23 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3216
3441
  };
3217
3442
  }
3218
3443
 
3444
+ const suppressRoutineResultIntercom = shouldSuppressRoutineResultIntercom({ suppressRoutineResultIntercom: params.suppressRoutineResultIntercom, results });
3219
3445
  if (foregroundControl) updateForegroundNestedProjection(foregroundControl);
3220
- const intercomReceipt = await maybeBuildForegroundIntercomReceipt({
3221
- pi: deps.pi,
3222
- intercomBridge: data.intercomBridge,
3223
- runId,
3224
- mode: "parallel",
3225
- details,
3226
- ...(foregroundControl?.nestedChildren?.length ? { nestedChildren: foregroundControl.nestedChildren } : {}),
3227
- });
3228
- if (intercomReceipt) {
3229
- return {
3230
- content: [{ type: "text", text: intercomReceipt.text }],
3231
- details: intercomReceipt.details,
3232
- };
3446
+ if (!suppressRoutineResultIntercom) {
3447
+ const intercomReceipt = await maybeBuildForegroundIntercomReceipt({
3448
+ pi: deps.pi,
3449
+ intercomBridge: data.intercomBridge,
3450
+ runId,
3451
+ mode: "parallel",
3452
+ details,
3453
+ ...(foregroundControl?.nestedChildren?.length ? { nestedChildren: foregroundControl.nestedChildren } : {}),
3454
+ });
3455
+ if (intercomReceipt) {
3456
+ return {
3457
+ content: [{ type: "text", text: intercomReceipt.text }],
3458
+ details: intercomReceipt.details,
3459
+ };
3460
+ }
3233
3461
  }
3234
3462
 
3235
3463
  const worktreeSuffix = handoff?.suffix ?? "";
@@ -3240,8 +3468,8 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3240
3468
  agent: result.agent,
3241
3469
  output: result.truncation?.text || getSingleResultOutput(result),
3242
3470
  exitCode: result.exitCode,
3243
- error: result.error,
3244
- timedOut: result.timedOut,
3471
+ ...(result.error === undefined ? {} : { error: result.error }),
3472
+ ...(result.timedOut === undefined ? {} : { timedOut: result.timedOut }),
3245
3473
  })),
3246
3474
  (i, agent) => `=== Task ${i + 1}: ${agent} ===`,
3247
3475
  );
@@ -3291,7 +3519,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3291
3519
  details: { mode: "single", results: [] },
3292
3520
  };
3293
3521
  }
3294
- const effectiveToolBudget = resolveEffectiveToolBudget({ runBudget: data.toolBudget, agentBudget: agentConfig.toolBudget, configBudget: data.configToolBudget });
3522
+ const effectiveToolBudget = resolveEffectiveToolBudget(omitUndefinedProperties({ runBudget: data.toolBudget, agentBudget: agentConfig.toolBudget, configBudget: data.configToolBudget }));
3295
3523
  if (effectiveToolBudget.error) return toExecutionErrorResult(params, new Error(effectiveToolBudget.error), data.contextPolicy.contextSummary);
3296
3524
 
3297
3525
  const parentModel = data.parentModel;
@@ -3304,7 +3532,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3304
3532
  parentModel,
3305
3533
  availableModels,
3306
3534
  currentProvider,
3307
- { scope: data.modelScope },
3535
+ data.modelScope === undefined ? {} : { scope: data.modelScope },
3308
3536
  );
3309
3537
  let skillOverride: string[] | false | undefined = normalizeSkillInput(params.skill);
3310
3538
  const rawOutput = params.output !== undefined ? params.output : agentConfig.output;
@@ -3314,7 +3542,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3314
3542
  const maxSubagentDepth = resolveChildMaxSubagentDepth(currentMaxSubagentDepth, agentConfig.maxSubagentDepth);
3315
3543
 
3316
3544
  if (params.clarify === true && ctx.hasUI) {
3317
- const behavior = resolveStepBehavior(agentConfig, { output: effectiveOutput, skills: skillOverride });
3545
+ const behavior = resolveStepBehavior(agentConfig, omitUndefinedProperties({ output: effectiveOutput, skills: skillOverride }));
3318
3546
  const availableSkills = discoverAvailableSkills(effectiveCwd);
3319
3547
 
3320
3548
  const result = await ctx.ui.custom<ChainClarifyResult>(
@@ -3341,7 +3569,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3341
3569
 
3342
3570
  task = result.templates[0]!;
3343
3571
  const override = result.behaviorOverrides[0];
3344
- if (override?.model !== undefined) modelOverride = resolveEffectiveSubagentModel(override.model, agentConfig.model, parentModel, availableModels, currentProvider, { scope: data.modelScope });
3572
+ if (override?.model !== undefined) modelOverride = resolveEffectiveSubagentModel(override.model, agentConfig.model, parentModel, availableModels, currentProvider, data.modelScope === undefined ? {} : { scope: data.modelScope });
3345
3573
  if (override?.output !== undefined) effectiveOutput = normalizeSingleOutputOverride(override.output, agentConfig.output);
3346
3574
  if (override?.skills !== undefined) skillOverride = override.skills;
3347
3575
 
@@ -3354,17 +3582,18 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3354
3582
  };
3355
3583
  }
3356
3584
  const id = randomUUID();
3357
- const asyncCtx = {
3585
+ const asyncCtx = compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
3358
3586
  pi: deps.pi,
3359
3587
  cwd: ctx.cwd,
3360
- currentSessionId: deps.state.currentSessionId!,
3361
- parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
3588
+ currentSessionId: data.parentSessionId!,
3589
+ parentSessionId: data.parentPiSessionId,
3362
3590
  currentModelProvider: parentModel?.provider,
3363
3591
  currentModel: parentModel,
3364
3592
  modelScope: data.modelScope,
3365
3593
  interactive: ctx.hasUI,
3366
- };
3367
- return executeAsyncSingle(id, {
3594
+ permissions: deps.config.permissions,
3595
+ });
3596
+ return executeAsyncSingle(id, compactOptional<Parameters<typeof executeAsyncSingle>[1]>({
3368
3597
  agent: params.agent!,
3369
3598
  task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(task) : task,
3370
3599
  goal: task,
@@ -3402,7 +3631,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3402
3631
  toolBudget: effectiveToolBudget.toolBudget,
3403
3632
  usageBudget: data.usageBudget,
3404
3633
  allowZeroToolBudget: data.allowZeroToolBudget && effectiveToolBudget.toolBudget === data.toolBudget,
3405
- });
3634
+ }));
3406
3635
  }
3407
3636
  }
3408
3637
 
@@ -3431,7 +3660,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3431
3660
  const foregroundControl = deps.state.foregroundControls.get(runId);
3432
3661
  if (foregroundControl) {
3433
3662
  const thinking = resolveEffectiveThinking(modelOverride, thinkingOverrideForTask(params.agent!, 0, modelOverride));
3434
- beginForegroundChild(foregroundControl, {
3663
+ beginForegroundChild(foregroundControl, omitUndefinedProperties({
3435
3664
  index: 0,
3436
3665
  agent: params.agent!,
3437
3666
  description: foregroundControl.description,
@@ -3443,7 +3672,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3443
3672
  return true;
3444
3673
  },
3445
3674
  detach: () => detachForeground?.("user request") === true,
3446
- });
3675
+ }));
3447
3676
  }
3448
3677
 
3449
3678
  const forwardSingleUpdate = onUpdate
@@ -3454,9 +3683,10 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3454
3683
  : undefined;
3455
3684
 
3456
3685
  const deadlineAt = data.deadlineAt ?? (data.timeoutMs !== undefined ? Date.now() + data.timeoutMs : undefined);
3457
- let r: Awaited<ReturnType<typeof runSync>>;
3686
+ let r: Awaited<ReturnType<typeof runSync>> | undefined;
3458
3687
  try {
3459
- r = await runSync(ctx.cwd, agents, params.agent!, task, {
3688
+ r = await runSync(ctx.cwd, agents, params.agent!, task, compactOptional<Parameters<typeof runSync>[4]>({
3689
+ permissions: deps.config.permissions,
3460
3690
  parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
3461
3691
  context: data.contextPolicy.contextForAgent(params.agent!),
3462
3692
  cwd: effectiveCwd,
@@ -3498,7 +3728,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3498
3728
  onDetachedExit: (result) => {
3499
3729
  try {
3500
3730
  try {
3501
- updateRememberedForegroundChild(deps.state, { runId, mode: "single", cwd: effectiveCwd, sessionId: data.parentSessionId, index: 0, result, events: deps.pi.events });
3731
+ updateRememberedForegroundChild(deps.state, { runId, mode: "single", cwd: effectiveCwd, sessionId: data.parentSessionId, index: 0, result, events: deps.pi.events, notify: params.workflowParentRunId === undefined });
3502
3732
  } catch {
3503
3733
  // Remembered foreground state is best-effort; run history and cleanup must still complete.
3504
3734
  }
@@ -3522,7 +3752,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3522
3752
  toolBudget: effectiveToolBudget.toolBudget,
3523
3753
  capabilityCeiling: data.capabilityCeiling,
3524
3754
  allowZeroToolBudget: data.allowZeroToolBudget && effectiveToolBudget.toolBudget === data.toolBudget,
3525
- });
3755
+ }));
3526
3756
  } finally {
3527
3757
  // An attached runSync rejection still owns its child and structured runtime.
3528
3758
  // A successful detached receipt transfers both to onDetachedExit while the
@@ -3540,7 +3770,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3540
3770
  if (r.artifactPaths) allArtifactPaths.push(r.artifactPaths);
3541
3771
 
3542
3772
  const fullOutput = getSingleResultOutput(r);
3543
- const finalizedOutput = finalizeSingleOutput({
3773
+ const finalizedOutput = finalizeSingleOutput(omitUndefinedProperties({
3544
3774
  fullOutput,
3545
3775
  truncatedOutput: r.truncation?.text,
3546
3776
  outputPath,
@@ -3549,13 +3779,13 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3549
3779
  savedPath: r.savedOutputPath,
3550
3780
  outputReference: r.outputReference,
3551
3781
  saveError: r.outputSaveError,
3552
- });
3782
+ }));
3553
3783
  if (foregroundControl) {
3554
3784
  updateForegroundNestedProjection(foregroundControl);
3555
3785
  attachRootChildrenToSteps(runId, [r], foregroundControl.nestedChildren);
3556
3786
  }
3557
3787
  const totalCost = sumResultsCost([r]);
3558
- const details = compactForegroundDetails({
3788
+ const details = compactForegroundDetails(compactOptional<Details>({
3559
3789
  mode: "single",
3560
3790
  runId,
3561
3791
  timeoutMs: data.timeoutMs,
@@ -3568,10 +3798,11 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3568
3798
  totalChildUsage: sumResultsUsage([r]),
3569
3799
  totalCost,
3570
3800
  usageBudget: usageBudgetState(data.usageBudget, totalCost),
3571
- });
3801
+ }));
3572
3802
  rememberForegroundRun(deps.state, { runId, mode: "single", cwd: effectiveCwd, sessionId: data.parentSessionId, results: details.results });
3573
3803
 
3574
- if (!r.detached && !r.interrupted) {
3804
+ const suppressRoutineResultIntercom = shouldSuppressRoutineResultIntercom({ suppressRoutineResultIntercom: params.suppressRoutineResultIntercom, results: [r] });
3805
+ if (!r.detached && !r.interrupted && !suppressRoutineResultIntercom) {
3575
3806
  if (foregroundControl) updateForegroundNestedProjection(foregroundControl);
3576
3807
  const intercomReceipt = await maybeBuildForegroundIntercomReceipt({
3577
3808
  pi: deps.pi,
@@ -3616,7 +3847,8 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3616
3847
  };
3617
3848
  }
3618
3849
 
3619
- function inferExecutionMode(params: SubagentParamsLike): SubagentRunMode {
3850
+ function inferExecutionMode(params: SubagentParamsLike): Details["mode"] {
3851
+ if (params.workflowScript !== undefined) return "workflow";
3620
3852
  if ((params.chain?.length ?? 0) > 0) return "chain";
3621
3853
  if ((params.tasks?.length ?? 0) > 0) return "parallel";
3622
3854
  return "single";
@@ -3633,6 +3865,84 @@ function duplicateSubagentCallResult(params: SubagentParamsLike): AgentToolResul
3633
3865
  };
3634
3866
  }
3635
3867
 
3868
+ function workflowChildResult(key: string, result: AgentToolResult<Details>): WorkflowScriptChildResult {
3869
+ const output = result.content.map((part) => part.type === "text" ? part.text : "").filter(Boolean).join("\n");
3870
+ const artifactPaths = new Set<string>();
3871
+ if (result.details.asyncDir) artifactPaths.add(result.details.asyncDir);
3872
+ if (result.details.parallelHandoff?.path) artifactPaths.add(result.details.parallelHandoff.path);
3873
+ for (const child of result.details.results) {
3874
+ if (child.savedOutputPath) artifactPaths.add(child.savedOutputPath);
3875
+ if (child.outputReference?.path) artifactPaths.add(child.outputReference.path);
3876
+ if (child.sessionFile) artifactPaths.add(child.sessionFile);
3877
+ }
3878
+ const structured = result.details.results.map((child) => child.structuredOutput).filter((value) => value !== undefined);
3879
+ return {
3880
+ key,
3881
+ ok: result.isError !== true,
3882
+ ...(result.details.runId || result.details.asyncId ? { runId: result.details.runId ?? result.details.asyncId } : {}),
3883
+ output,
3884
+ ...(result.isError === true ? { error: output || "Child run failed." } : {}),
3885
+ ...(structured.length === 1 ? { structuredOutput: structured[0] } : structured.length > 1 ? { structuredOutput: structured } : {}),
3886
+ artifactPaths: [...artifactPaths],
3887
+ results: result.details.results,
3888
+ };
3889
+ }
3890
+
3891
+ function prepareWorkflowChildParams(params: SubagentParamsLike): SubagentParamsLike {
3892
+ if (params.worktree !== true || !params.agent) return params;
3893
+ const {
3894
+ agent,
3895
+ task = "",
3896
+ model,
3897
+ skill,
3898
+ output,
3899
+ outputMode,
3900
+ outputSchema,
3901
+ acceptance,
3902
+ agentContract,
3903
+ toolBudget,
3904
+ ...runParams
3905
+ } = params;
3906
+ return {
3907
+ ...runParams,
3908
+ worktree: true,
3909
+ tasks: [{
3910
+ agent,
3911
+ task,
3912
+ ...(model !== undefined ? { model } : {}),
3913
+ ...(skill !== undefined ? { skill } : {}),
3914
+ ...(output !== undefined ? { output } : {}),
3915
+ ...(outputMode !== undefined ? { outputMode } : {}),
3916
+ ...(outputSchema !== undefined ? { outputSchema } : {}),
3917
+ ...(acceptance !== undefined ? { acceptance } : {}),
3918
+ ...(agentContract !== undefined ? { agentContract } : {}),
3919
+ ...(toolBudget !== undefined ? { toolBudget } : {}),
3920
+ }],
3921
+ };
3922
+ }
3923
+
3924
+ function formatWorkflowValue(value: unknown): string {
3925
+ if (value === undefined) return "(undefined)";
3926
+ if (typeof value === "string") return value;
3927
+ try {
3928
+ return JSON.stringify(value, null, 2);
3929
+ } catch {
3930
+ return String(value);
3931
+ }
3932
+ }
3933
+
3934
+ function workflowChatProgressUpdate(
3935
+ runId: string,
3936
+ chatProgress: WorkflowChatProgressProjection,
3937
+ workflow: NonNullable<Details["workflow"]>,
3938
+ ): AgentToolResult<Details> | undefined {
3939
+ if (chatProgress.mode !== "live-card") return undefined;
3940
+ return {
3941
+ content: [{ type: "text", text: "Workflow running." }],
3942
+ details: { mode: "workflow", runId, results: [], workflow, chatProgress },
3943
+ };
3944
+ }
3945
+
3636
3946
  function omitExecutionModeActionAlias(params: SubagentParamsLike): SubagentParamsLike {
3637
3947
  const action = params.action?.toLowerCase();
3638
3948
  if (action === "single" && (params.agent !== undefined || params.task !== undefined)) {
@@ -3648,6 +3958,36 @@ function omitExecutionModeActionAlias(params: SubagentParamsLike): SubagentParam
3648
3958
  return params;
3649
3959
  }
3650
3960
 
3961
+ function createScheduledOwnerState(source: SubagentState, ownerSessionId: string, ctx: ExtensionContext): SubagentState {
3962
+ const ownerSpawns = source.subagentSpawns?.sessionId === ownerSessionId
3963
+ ? {
3964
+ ...source.subagentSpawns,
3965
+ grantHistory: [...(source.subagentSpawns.grantHistory ?? [])],
3966
+ }
3967
+ : undefined;
3968
+ return {
3969
+ ...source,
3970
+ baseCwd: ctx.cwd,
3971
+ currentSessionId: ownerSessionId,
3972
+ parentSessionFile: ctx.sessionManager.getSessionFile() ?? null,
3973
+ subagentInProgress: false,
3974
+ ...(ownerSpawns ? { subagentSpawns: ownerSpawns } : { subagentSpawns: undefined }),
3975
+ asyncJobs: new Map(),
3976
+ fleetJobs: new Map(),
3977
+ foregroundRuns: new Map(),
3978
+ foregroundControls: new Map(),
3979
+ lastForegroundControlId: null,
3980
+ cleanupTimers: new Map(),
3981
+ lastUiContext: null,
3982
+ poller: null,
3983
+ completionSeen: new Map(),
3984
+ watcher: null,
3985
+ watcherRestartTimer: null,
3986
+ waitSubscriptions: new Map(),
3987
+ workflowControllers: new Map(),
3988
+ };
3989
+ }
3990
+
3651
3991
  export function createSubagentExecutor(deps: ExecutorDeps): {
3652
3992
  execute: (
3653
3993
  id: string,
@@ -3668,34 +4008,319 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3668
4008
  onUpdate: ((r: AgentToolResult<Details>) => void) | undefined,
3669
4009
  ctx: ExtensionContext,
3670
4010
  ) => Promise<AgentToolResult<Details>>;
4011
+ /** Scheduled launches retain their owning context without replacing the live active session. */
4012
+ executeScheduled: (
4013
+ id: string,
4014
+ params: SubagentParamsLike,
4015
+ signal: AbortSignal,
4016
+ ctx: ExtensionContext,
4017
+ ) => Promise<AgentToolResult<Details>>;
3671
4018
  } {
3672
4019
  const delegatedThinkingOverrides = new WeakMap<object, AgentConfig["thinking"]>();
3673
4020
  const delegatedZeroToolBudgets = new WeakSet<object>();
4021
+ const scheduledOwnerExecutors = new Map<string, ReturnType<typeof createSubagentExecutor>>();
3674
4022
  const execute = async (
3675
4023
  _id: string,
3676
4024
  params: SubagentParamsLike,
3677
4025
  signal: AbortSignal,
3678
4026
  onUpdate: ((r: AgentToolResult<Details>) => void) | undefined,
3679
4027
  ctx: ExtensionContext,
4028
+ preserveActiveSession = false,
3680
4029
  ): Promise<AgentToolResult<Details>> => {
3681
4030
  const delegatedThinkingOverride = delegatedThinkingOverrides.get(params);
3682
4031
  const allowZeroToolBudget = delegatedZeroToolBudgets.has(params);
3683
- deps.state.baseCwd = ctx.cwd;
4032
+ if (!preserveActiveSession) deps.state.baseCwd = ctx.cwd;
3684
4033
  deps.state.foregroundRuns ??= new Map();
3685
4034
  deps.state.foregroundControls ??= new Map();
3686
4035
  deps.state.lastForegroundControlId ??= null;
3687
4036
  const requestParams = omitExecutionModeActionAlias(params);
3688
- const requestCwd = resolveRequestedCwd(ctx.cwd, requestParams.cwd);
3689
- const paramsWithResolvedCwd = requestParams.cwd === undefined ? requestParams : { ...requestParams, cwd: requestCwd };
4037
+ if (requestParams.workflowScript !== undefined) {
4038
+ const invalidMode = requestParams.action !== undefined || requestParams.agent !== undefined || requestParams.step !== undefined || requestParams.tasks !== undefined || requestParams.chain !== undefined;
4039
+ if (invalidMode) {
4040
+ return { content: [{ type: "text", text: "workflowScript is its own execution mode; do not combine it with action, agent, step, tasks, or chain." }], isError: true, details: { mode: "workflow", results: [] } };
4041
+ }
4042
+ if (requestParams.clarify === true) {
4043
+ return { content: [{ type: "text", text: "workflowScript does not support clarify UI." }], isError: true, details: { mode: "workflow", results: [] } };
4044
+ }
4045
+ const timeout = requestParams.timeoutMs ?? requestParams.maxRuntimeMs ?? DEFAULT_FOREGROUND_TIMEOUT_MS;
4046
+ const workflowUsageBudget = validateUsageBudgetConfig(requestParams.usageBudget ?? deps.config.usageBudget, requestParams.usageBudget ? "usageBudget" : "config.usageBudget");
4047
+ if (workflowUsageBudget.error) return buildRequestedModeError(requestParams, workflowUsageBudget.error);
4048
+ const workflowCwd = resolveRequestedCwd(ctx.cwd, requestParams.cwd);
4049
+ const chatProgressResult = resolveWorkflowChatProgress({ requested: requestParams.chatProgress, parentCwd: ctx.cwd, workflowCwd, background: requestParams.async !== false });
4050
+ if (chatProgressResult.error) return { content: [{ type: "text", text: chatProgressResult.error }], isError: true, details: { mode: "workflow", results: [] } };
4051
+ const chatProgress = chatProgressResult.projection!;
4052
+ if (requestParams.async !== false) {
4053
+ const workflowRunId = _id;
4054
+ const asyncDir = path.join(DIRS.async, workflowRunId);
4055
+ const resultPath = path.join(DIRS.results, `${workflowRunId}.json`);
4056
+ const statusPath = path.join(asyncDir, "status.json");
4057
+ const eventsPath = path.join(asyncDir, "events.jsonl");
4058
+ const startedAt = Date.now();
4059
+ const currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
4060
+ fs.mkdirSync(asyncDir, { recursive: true });
4061
+ fs.mkdirSync(DIRS.results, { recursive: true });
4062
+ const controller = new AbortController();
4063
+ deps.state.workflowControllers ??= new Map();
4064
+ deps.state.workflowControllers.set(workflowRunId, controller);
4065
+ let status: AsyncStatus = {
4066
+ runId: workflowRunId,
4067
+ sessionId: currentSessionId ?? undefined,
4068
+ mode: "workflow",
4069
+ state: "running",
4070
+ startedAt,
4071
+ lastUpdate: startedAt,
4072
+ deadlineAt: startedAt + timeout,
4073
+ timeoutMs: timeout,
4074
+ cwd: ctx.cwd,
4075
+ pid: process.pid,
4076
+ steps: [],
4077
+ workflow: { trace: [], emits: [], console: [] },
4078
+ };
4079
+ const appendWorkflowEvent = (event: Record<string, unknown>) => fs.appendFileSync(eventsPath, `${JSON.stringify({ ts: Date.now(), runId: workflowRunId, ...event })}\n`, "utf-8");
4080
+ const persist = () => {
4081
+ status.lastUpdate = Date.now();
4082
+ writeAtomicJson(statusPath, status);
4083
+ const job = deps.state.asyncJobs.get(workflowRunId);
4084
+ if (job) {
4085
+ job.status = status.state;
4086
+ job.updatedAt = status.lastUpdate;
4087
+ if (status.steps) {
4088
+ job.steps = status.steps.map((step, index) => ({ ...step, index }));
4089
+ job.agents = status.steps.map((step) => step.agent);
4090
+ } else {
4091
+ delete job.steps;
4092
+ delete job.agents;
4093
+ }
4094
+ job.workflow = status.workflow;
4095
+ }
4096
+ };
4097
+ const workflowJob: AsyncJobState = { asyncId: workflowRunId, asyncDir, cwd: ctx.cwd, status: "running", sessionId: currentSessionId ?? undefined, mode: "workflow", agents: [], steps: [], startedAt, updatedAt: startedAt, timeoutMs: timeout, deadlineAt: startedAt + timeout, workflow: status.workflow };
4098
+ deps.state.asyncJobs.set(workflowRunId, workflowJob);
4099
+ deps.state.fleetJobs ??= new Map();
4100
+ deps.state.fleetJobs.set(workflowRunId, workflowJob);
4101
+ persist();
4102
+ appendWorkflowEvent({ type: "subagent.workflow.started" });
4103
+ const { workflowScript, async: _workflowAsync, chatProgress: _chatProgress, ...workflowRequest } = requestParams;
4104
+ void Promise.resolve().then(async () => {
4105
+ const workflowResults: SingleResult[] = [];
4106
+ const { action: _action, agent: _agent, task: _task, tasks: _tasks, chain: _chain, concurrency: _concurrency, foregroundOnly: _foregroundOnly, clarify: _clarify, timeoutMs: _timeoutMs, maxRuntimeMs: _maxRuntimeMs, usageBudget: _usageBudget, ...workflowChildDefaults } = workflowRequest;
4107
+ const updateTrace = (trace: NonNullable<Details["workflow"]>["trace"]) => {
4108
+ status.workflow = { ...(status.workflow ?? { emits: [], console: [] }), trace };
4109
+ for (const entry of trace.filter((candidate) => candidate.operation === "run")) {
4110
+ const existing = status.steps?.find((step) => step.workflowKey === entry.key);
4111
+ if (entry.state === "reused" && existing) continue;
4112
+ const mapped = entry.state === "started" || entry.state === "reused" ? "running" : entry.state === "completed" ? "completed" : "failed";
4113
+ if (existing) {
4114
+ existing.status = mapped;
4115
+ if (entry.error === undefined) delete existing.error;
4116
+ else existing.error = entry.error;
4117
+ if (entry.durationMs === undefined) delete existing.durationMs;
4118
+ else existing.durationMs = entry.durationMs;
4119
+ } else {
4120
+ status.steps?.push({ agent: entry.key, label: entry.key, workflowKey: entry.key, parentWorkflowRunId: workflowRunId, status: mapped });
4121
+ }
4122
+ }
4123
+ persist();
4124
+ appendWorkflowEvent({ type: "subagent.workflow.trace", trace });
4125
+ };
4126
+ try {
4127
+ const workflow = await runWorkflowScript({
4128
+ script: workflowScript,
4129
+ timeoutMs: timeout,
4130
+ signal: controller.signal,
4131
+ onTrace: updateTrace,
4132
+ onEmit: (emits) => {
4133
+ // Each emit is validated at the host boundary in runWorkflowScript before onEmit fires.
4134
+ status.workflow = { ...(status.workflow ?? { trace: [], console: [] }), emits };
4135
+ persist();
4136
+ appendWorkflowEvent({ type: "subagent.workflow.emit", value: emits.at(-1) });
4137
+ },
4138
+ launch: async (key, childParams, workflowSignal) => {
4139
+ if (workflowUsageBudget.budget && childParams.async === true) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, "workflow usageBudget does not support async runs.run launches."));
4140
+ const budgetState = usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults));
4141
+ if (budgetState?.exhausted) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, usageBudgetExceededMessage(budgetState)));
4142
+ const childRequest = prepareWorkflowChildParams({ ...workflowChildDefaults, ...childParams, workflowParentRunId: workflowRunId, workflowKey: key } as SubagentParamsLike);
4143
+ const result = await execute(randomUUID(), childRequest, workflowSignal, undefined, ctx, preserveActiveSession);
4144
+ workflowResults.push(...result.details.results);
4145
+ const child = workflowChildResult(key, result);
4146
+ if (result.details.asyncId) {
4147
+ const childJob = deps.state.asyncJobs.get(result.details.asyncId);
4148
+ if (childJob) { childJob.parentWorkflowRunId = workflowRunId; childJob.workflowKey = key; }
4149
+ }
4150
+ return child;
4151
+ },
4152
+ status: async (keyOrRunId, workflowSignal) => workflowChildResult(keyOrRunId, await execute(randomUUID(), { action: "status", id: keyOrRunId }, workflowSignal, undefined, ctx, preserveActiveSession)),
4153
+ });
4154
+ const returnPreview = formatWorkflowValue(workflow.value).slice(0, 1_000);
4155
+ const emitPreview = workflow.emits.length > 0 ? ` Emitted: ${workflow.emits.map(formatWorkflowValue).join(", ").slice(0, 1_000)}` : "";
4156
+ const summary = `Workflow completed with ${workflow.children.length} child run(s). Return: ${returnPreview}${emitPreview} Trace: ${workflow.trace.length} event(s).`;
4157
+ const workflowUsage = sumResultsUsage(workflowResults);
4158
+ status = { ...status, state: "complete", endedAt: Date.now(), workflow: { value: workflow.value, trace: workflow.trace, emits: workflow.emits, console: workflow.console }, totalTokens: { input: workflowUsage.input, output: workflowUsage.output, total: workflowUsage.input + workflowUsage.output }, totalCost: sumResultsCost(workflowResults) };
4159
+ persist();
4160
+ appendWorkflowEvent({ type: "subagent.workflow.completed", state: "complete" });
4161
+ writeAtomicJson(resultPath, { id: workflowRunId, runId: workflowRunId, agent: "workflow", mode: "workflow", success: true, state: "complete", summary, output: summary, results: workflow.children.map((child) => ({ agent: child.key, output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: ctx.cwd, sessionId: currentSessionId, timestamp: Date.now(), durationMs: Date.now() - startedAt });
4162
+ } catch (error) {
4163
+ const partial = error instanceof WorkflowScriptError ? error.partial : { trace: [], emits: [], console: [], children: [] };
4164
+ const stopped = controller.signal.aborted;
4165
+ status = compactOptional<AsyncStatus>({ ...status, state: stopped ? "stopped" : "failed", stopped: stopped || undefined, error: error instanceof Error ? error.message : String(error), endedAt: Date.now(), workflow: { trace: partial.trace, emits: partial.emits, console: partial.console } });
4166
+ persist();
4167
+ appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, error: status.error });
4168
+ writeAtomicJson(resultPath, { id: workflowRunId, runId: workflowRunId, agent: "workflow", mode: "workflow", success: false, state: status.state, summary: status.error, error: status.error, stopped: status.stopped, results: partial.children.map((child) => ({ agent: child.key, output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: ctx.cwd, sessionId: currentSessionId, timestamp: Date.now(), durationMs: Date.now() - startedAt });
4169
+ } finally {
4170
+ deps.state.workflowControllers?.delete(workflowRunId);
4171
+ }
4172
+ });
4173
+ return {
4174
+ content: [{ type: "text", text: formatAsyncStartedMessage(`Async workflow [${workflowRunId}]`, ctx.hasUI === true) }],
4175
+ details: { mode: "workflow", runId: workflowRunId, asyncId: workflowRunId, asyncDir, results: [], chatProgress },
4176
+ };
4177
+ }
4178
+ const { workflowScript: _workflowScript, action: _action, agent: _agent, task: _task, tasks: _tasks, chain: _chain, concurrency: _concurrency, async: _async, foregroundOnly: _foregroundOnly, clarify: _clarify, timeoutMs: _timeoutMs, maxRuntimeMs: _maxRuntimeMs, usageBudget: _usageBudget, chatProgress: _chatProgress, ...workflowChildDefaults } = requestParams;
4179
+ const workflowResults: SingleResult[] = [];
4180
+ let liveWorkflow: NonNullable<Details["workflow"]> = { trace: [], emits: [], console: [] };
4181
+ const sendWorkflowProgress = () => {
4182
+ const update = workflowChatProgressUpdate(_id, chatProgress, liveWorkflow);
4183
+ if (update) onUpdate?.(update);
4184
+ };
4185
+ try {
4186
+ const workflow = await runWorkflowScript({
4187
+ script: requestParams.workflowScript,
4188
+ timeoutMs: timeout,
4189
+ signal,
4190
+ onTrace: (trace) => {
4191
+ liveWorkflow = { ...liveWorkflow, trace };
4192
+ sendWorkflowProgress();
4193
+ },
4194
+ onEmit: (emits) => {
4195
+ liveWorkflow = { ...liveWorkflow, emits };
4196
+ sendWorkflowProgress();
4197
+ },
4198
+ launch: async (key, childParams, workflowSignal) => {
4199
+ if (workflowUsageBudget.budget && childParams.async === true) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, "workflow usageBudget does not support async runs.run launches."));
4200
+ const budgetState = usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults));
4201
+ if (budgetState?.exhausted) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, usageBudgetExceededMessage(budgetState)));
4202
+ const childRequest = prepareWorkflowChildParams({ ...workflowChildDefaults, ...childParams, workflowParentRunId: _id, workflowKey: key, suppressRoutineResultIntercom: chatProgress.mode === "live-card" } as SubagentParamsLike);
4203
+ const result = await execute(randomUUID(), childRequest, workflowSignal, undefined, ctx, preserveActiveSession);
4204
+ workflowResults.push(...result.details.results);
4205
+ return workflowChildResult(key, result);
4206
+ },
4207
+ status: async (keyOrRunId, workflowSignal) => workflowChildResult(keyOrRunId, await execute(randomUUID(), { action: "status", id: keyOrRunId }, workflowSignal, undefined, ctx, preserveActiveSession)),
4208
+ });
4209
+ const traceLines = workflow.trace.map((entry) => `- ${entry.operation} ${entry.key}: ${entry.state}${entry.runId ? ` (${entry.runId})` : ""}${entry.durationMs !== undefined ? ` in ${entry.durationMs}ms` : ""}${entry.error ? ` — ${entry.error}` : ""}`);
4210
+ const sections = ["Workflow completed.", `Return:\n${formatWorkflowValue(workflow.value)}`];
4211
+ if (workflow.emits.length > 0) sections.push(`Emitted:\n${workflow.emits.map(formatWorkflowValue).join("\n")}`);
4212
+ if (workflow.console.length > 0) sections.push(`Console:\n${workflow.console.map((entry) => `[${entry.level}] ${entry.text}`).join("\n")}`);
4213
+ if (traceLines.length > 0) sections.push(`Call trace:\n${traceLines.join("\n")}`);
4214
+ return {
4215
+ content: [{ type: "text", text: sections.join("\n\n") }],
4216
+ details: compactOptional<Details>({ mode: "workflow", runId: _id, results: workflow.children.flatMap((child) => (child.results ?? []) as SingleResult[]), totalChildUsage: sumResultsUsage(workflowResults), totalCost: sumResultsCost(workflowResults), usageBudget: usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults)), workflow: { value: workflow.value, trace: workflow.trace, emits: workflow.emits, console: workflow.console }, chatProgress }),
4217
+ };
4218
+ } catch (error) {
4219
+ const partial = error instanceof WorkflowScriptError ? error.partial : { trace: [], emits: [], console: [], children: [] };
4220
+ const text = error instanceof Error ? error.message : String(error);
4221
+ const traceLines = partial.trace.map((entry) => `- ${entry.operation} ${entry.key}: ${entry.state}${entry.runId ? ` (${entry.runId})` : ""}${entry.error ? ` — ${entry.error}` : ""}`);
4222
+ const sections = [`Workflow failed: ${text}`];
4223
+ if (partial.emits.length > 0) sections.push(`Emitted:\n${partial.emits.map(formatWorkflowValue).join("\n")}`);
4224
+ if (partial.console.length > 0) sections.push(`Console:\n${partial.console.map((entry) => `[${entry.level}] ${entry.text}`).join("\n")}`);
4225
+ if (traceLines.length > 0) sections.push(`Call trace:\n${traceLines.join("\n")}`);
4226
+ return {
4227
+ content: [{ type: "text", text: sections.join("\n\n") }],
4228
+ isError: true,
4229
+ details: compactOptional<Details>({ mode: "workflow", runId: _id, results: partial.children.flatMap((child) => (child.results ?? []) as SingleResult[]), totalChildUsage: sumResultsUsage(workflowResults), totalCost: sumResultsCost(workflowResults), usageBudget: usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults)), workflow: { trace: partial.trace, emits: partial.emits, console: partial.console }, chatProgress }),
4230
+ };
4231
+ }
4232
+ }
4233
+ const directParams = prepareWorkflowChildParams(requestParams);
4234
+ const requestCwd = resolveRequestedCwd(ctx.cwd, directParams.cwd);
4235
+ const paramsWithResolvedCwd = directParams.cwd === undefined ? directParams : { ...directParams, cwd: requestCwd };
3690
4236
  const action = paramsWithResolvedCwd.action;
4237
+ let requestSessionId = "";
4238
+ let requestPiSessionId: string | undefined;
3691
4239
  let requestParentModel: ParentModel | undefined;
3692
4240
  try {
3693
- requestParentModel = rememberParentModel(deps.state, resolveCurrentSessionId(ctx.sessionManager), ctx.model);
4241
+ requestSessionId = resolveCurrentSessionId(ctx.sessionManager);
4242
+ requestPiSessionId = ctx.sessionManager.getSessionId() ?? undefined;
4243
+ requestParentModel = preserveActiveSession
4244
+ ? normalizeParentModel(ctx.model)
4245
+ : rememberParentModel(deps.state, requestSessionId, ctx.model);
3694
4246
  } catch (error) {
3695
4247
  if (action?.toLowerCase() !== "doctor") throw error;
3696
4248
  requestParentModel = normalizeParentModel(ctx.model);
3697
4249
  }
3698
4250
  if (action) {
4251
+ if (action === "worktree.discard") {
4252
+ if (deps.allowMutatingManagementActions === false) {
4253
+ return { content: [{ type: "text", text: "Action 'worktree.discard' is not available from child-safe subagent fanout mode." }], isError: true, details: { mode: "management", results: [] } };
4254
+ }
4255
+ if (!paramsWithResolvedCwd.handoffPath?.trim()) {
4256
+ return { content: [{ type: "text", text: "worktree.discard requires handoffPath from parallelHandoff.path or async status." }], isError: true, details: { mode: "management", results: [] } };
4257
+ }
4258
+ const decision = resolveAuthorityDecision({ action: "discardWorktree", ...(deps.config.authorityPolicy === undefined ? {} : { policy: deps.config.authorityPolicy }) });
4259
+ if (decision === "forbid") {
4260
+ return { content: [{ type: "text", text: "Authority policy forbids worktree discard." }], isError: true, details: { mode: "management", results: [] } };
4261
+ }
4262
+ let confirmed = decision === "auto";
4263
+ if (decision === "confirm") {
4264
+ if (!ctx.hasUI) return { content: [{ type: "text", text: "Authority policy requires user confirmation for worktree discard, but this session has no interactive UI. Preserved worktrees were not changed." }], isError: true, details: { mode: "management", results: [] } };
4265
+ confirmed = await ctx.ui.confirm("Discard preserved subagent worktrees?", `This permanently removes preserved worktrees and temporary branches recorded in:\n${paramsWithResolvedCwd.handoffPath}`);
4266
+ }
4267
+ if (!confirmed) return { content: [{ type: "text", text: "Worktree discard canceled; preserved worktrees were not changed." }], details: { mode: "management", results: [] } };
4268
+ try {
4269
+ const discarded = discardPreservedWorktrees(
4270
+ path.isAbsolute(paramsWithResolvedCwd.handoffPath) ? paramsWithResolvedCwd.handoffPath : path.resolve(requestCwd, paramsWithResolvedCwd.handoffPath),
4271
+ { kind: decision === "confirm" ? "confirmed" : "policy", ...(deps.config.authorityPolicy ? { policy: deps.config.authorityPolicy } : {}) },
4272
+ );
4273
+ return { content: [{ type: "text", text: discarded.text }], details: { mode: "management", results: [] } };
4274
+ } catch (error) {
4275
+ return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, details: { mode: "management", results: [] } };
4276
+ }
4277
+ }
4278
+ if ((HERDR_PROJECT_PANE_ACTIONS as readonly string[]).includes(action)) {
4279
+ if (deps.allowMutatingManagementActions === false && MUTATING_MANAGEMENT_ACTIONS.has(action)) {
4280
+ return { content: [{ type: "text", text: `Action '${action}' is not available from child-safe subagent fanout mode.` }], isError: true, details: { mode: "management", results: [] } };
4281
+ }
4282
+ return handleHerdrProjectPaneAction(action as (typeof HERDR_PROJECT_PANE_ACTIONS)[number], paramsWithResolvedCwd, { cwd: requestCwd, signal });
4283
+ }
4284
+ if ((HERDR_INSPECTOR_ACTIONS as readonly string[]).includes(action)) {
4285
+ if (deps.allowMutatingManagementActions === false && MUTATING_MANAGEMENT_ACTIONS.has(action)) {
4286
+ return { content: [{ type: "text", text: `Action '${action}' is not available from child-safe subagent fanout mode.` }], isError: true, details: { mode: "management", results: [] } };
4287
+ }
4288
+ deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
4289
+ return handleHerdrInspectorAction(action as (typeof HERDR_INSPECTOR_ACTIONS)[number], paramsWithResolvedCwd, {
4290
+ state: deps.state,
4291
+ cwd: requestCwd,
4292
+ ...(deps.config.missions ? { missions: deps.config.missions } : {}),
4293
+ ...(deps.config.authorityPolicy ? { authorityPolicy: deps.config.authorityPolicy } : {}),
4294
+ signal,
4295
+ });
4296
+ }
4297
+ if ((MISSION_ACTIONS as readonly string[]).includes(action)) {
4298
+ if (deps.allowMutatingManagementActions === false && MUTATING_MANAGEMENT_ACTIONS.has(action)) {
4299
+ return {
4300
+ content: [{ type: "text", text: `Action '${action}' is not available from child-safe subagent fanout mode.` }],
4301
+ isError: true,
4302
+ details: { mode: "management", results: [] },
4303
+ };
4304
+ }
4305
+ const currentSessionId = deps.state.currentSessionId ?? ctx.sessionManager.getSessionId() ?? undefined;
4306
+ return handleMissionAction(action as (typeof MISSION_ACTIONS)[number], paramsWithResolvedCwd, {
4307
+ cwd: requestCwd,
4308
+ ...(deps.config.missions ? { config: deps.config.missions } : {}),
4309
+ ...(currentSessionId ? { currentSessionId } : {}),
4310
+ });
4311
+ }
4312
+ const policyAction = action === "stop" ? "stopRun" : action === "steer" ? "steerRun" : action === "schedule.create" ? "scheduleCreate" : undefined;
4313
+ if (policyAction) {
4314
+ const decision = resolveAuthorityDecision({ action: policyAction, ...(deps.config.authorityPolicy === undefined ? {} : { policy: deps.config.authorityPolicy }) });
4315
+ if (decision === "forbid") {
4316
+ return { content: [{ type: "text", text: `Authority policy forbids action '${action}'.` }], isError: true, details: { mode: "management", results: [] } };
4317
+ }
4318
+ if (decision === "confirm") {
4319
+ if (!ctx.hasUI) return { content: [{ type: "text", text: `Authority policy requires user confirmation for action '${action}', but this session has no interactive UI.` }], isError: true, details: { mode: "management", results: [] } };
4320
+ const confirmed = await ctx.ui.confirm(`Authorize subagent ${action}?`, `Authority policy requires confirmation before '${action}'.`);
4321
+ if (!confirmed) return { content: [{ type: "text", text: `Action '${action}' canceled; authority was not granted.` }], details: { mode: "management", results: [] } };
4322
+ }
4323
+ }
3699
4324
  if ((WATCHDOG_TOOL_ACTIONS as readonly string[]).includes(action)) {
3700
4325
  if (deps.allowMutatingManagementActions === false && MUTATING_MANAGEMENT_ACTIONS.has(action)) {
3701
4326
  return {
@@ -3740,7 +4365,15 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3740
4365
  details: { mode: "management", results: [], spawnBudget: preview.snapshot },
3741
4366
  };
3742
4367
  }
3743
- const confirmed = await ctx.ui.confirm(
4368
+ const authority = resolveAuthorityDecision({ action: "spawnBudgetGrant", ...(deps.config.authorityPolicy === undefined ? {} : { policy: deps.config.authorityPolicy }) });
4369
+ if (authority === "forbid") {
4370
+ return {
4371
+ content: [{ type: "text", text: "Authority policy forbids spawn budget grants." }],
4372
+ isError: true,
4373
+ details: { mode: "management", results: [], spawnBudget: preview.snapshot },
4374
+ };
4375
+ }
4376
+ const confirmed = authority === "auto" || await ctx.ui.confirm(
3744
4377
  "Grant subagent spawn budget?",
3745
4378
  `Add ${additional} launches to this logical session?\n\n${formatSpawnBudget(preview.snapshot)}\n\nUsage is not reset. Compaction keeps the same budget; a new parent session starts a fresh one.`,
3746
4379
  );
@@ -3790,7 +4423,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3790
4423
  return {
3791
4424
  content: [{
3792
4425
  type: "text",
3793
- text: buildDoctorReport({
4426
+ text: buildDoctorReport(omitUndefinedProperties({
3794
4427
  cwd: requestCwd,
3795
4428
  config: deps.config,
3796
4429
  state: deps.state,
@@ -3801,13 +4434,13 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3801
4434
  orchestratorTarget,
3802
4435
  sessionError,
3803
4436
  expandTilde: deps.expandTilde,
3804
- }),
4437
+ })),
3805
4438
  }],
3806
4439
  details: { mode: "management", results: [], spawnBudget },
3807
4440
  };
3808
4441
  }
3809
4442
  if (action === "status") {
3810
- deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
4443
+ if (!preserveActiveSession) deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
3811
4444
  const withBudget = (result: AgentToolResult<Details>) => withSpawnBudgetStatus(
3812
4445
  result,
3813
4446
  deps.state,
@@ -3818,11 +4451,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3818
4451
  const nestedScope = nestedResolutionScopeForExecutor(deps);
3819
4452
  const sessionRoots = trustedSessionRootsForStatus(ctx, deps);
3820
4453
  if (paramsWithResolvedCwd.view === "fleet") {
3821
- return withBudget(inspectSubagentStatus(paramsWithResolvedCwd, { state: deps.state, nested: nestedScope, sessionRoots }));
4454
+ return withBudget(inspectSubagentStatus(paramsWithResolvedCwd, omitUndefinedProperties({ state: deps.state, nested: nestedScope, sessionRoots })));
3822
4455
  }
3823
4456
  if (targetRunId) {
3824
4457
  try {
3825
- const resolved = resolveSubagentRunId(targetRunId, { state: deps.state, nested: nestedScope });
4458
+ const resolved = resolveSubagentRunId(targetRunId, omitUndefinedProperties({ state: deps.state, nested: nestedScope }));
3826
4459
  if (resolved?.kind === "foreground") {
3827
4460
  const foreground = getForegroundControl(deps.state, resolved.id);
3828
4461
  if (foreground) {
@@ -3849,7 +4482,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3849
4482
  });
3850
4483
  }
3851
4484
  }
3852
- return withBudget(inspectSubagentStatus(paramsWithResolvedCwd, { state: deps.state, nested: nestedScope, sessionRoots }));
4485
+ return withBudget(inspectSubagentStatus(paramsWithResolvedCwd, omitUndefinedProperties({ state: deps.state, nested: nestedScope, sessionRoots })));
3853
4486
  }
3854
4487
 
3855
4488
  if (action === "approve-checkpoint" || action === "reject-checkpoint") {
@@ -3859,7 +4492,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3859
4492
  return { content: [{ type: "text", text: `action='${action}' requires id or dir.` }], isError: true, details: { mode: "management", results: [] } };
3860
4493
  }
3861
4494
  try {
3862
- const resolved = targetRunId ? resolveSubagentRunId(targetRunId, { state: deps.state, nested: nestedResolutionScopeForExecutor(deps) }) : undefined;
4495
+ const resolved = targetRunId ? resolveSubagentRunId(targetRunId, omitUndefinedProperties({ state: deps.state, nested: nestedResolutionScopeForExecutor(deps) })) : undefined;
3863
4496
  const decision = action === "approve-checkpoint" ? "approved" : "rejected";
3864
4497
  if (resolved?.kind === "foreground") {
3865
4498
  const run = deps.state.foregroundRuns?.get(resolved.id);
@@ -3870,7 +4503,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3870
4503
  return { content: [{ type: "text", text: `Run '${resolved.id}' was not found in the active session.` }], isError: true, details: { mode: "management", results: [] } };
3871
4504
  }
3872
4505
  const decidedAt = Date.now();
3873
- const checkpoint = { ...run.checkpoint, status: decision, ...(decision === "approved" ? { approvedAt: decidedAt } : { rejectedAt: decidedAt }) };
4506
+ const checkpoint: NonNullable<Details["checkpoint"]> = decision === "approved"
4507
+ ? { ...run.checkpoint, status: "approved", approvedAt: decidedAt }
4508
+ : { ...run.checkpoint, status: "rejected", rejectedAt: decidedAt };
3874
4509
  run.checkpoint = checkpoint;
3875
4510
  run.updatedAt = decidedAt;
3876
4511
  return {
@@ -3879,7 +4514,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3879
4514
  };
3880
4515
  }
3881
4516
  const location = paramsWithResolvedCwd.dir
3882
- ? resolveAsyncRunLocation(paramsWithResolvedCwd, ASYNC_DIR, RESULTS_DIR)
4517
+ ? resolveAsyncRunLocation(paramsWithResolvedCwd, DIRS.async, DIRS.results)
3883
4518
  : resolved?.kind === "async"
3884
4519
  ? resolved.location
3885
4520
  : undefined;
@@ -3905,7 +4540,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3905
4540
  }
3906
4541
  }
3907
4542
  if (action === "resume") {
3908
- return resumeAsyncRun({ params: paramsWithResolvedCwd, requestCwd, ctx, deps, parentModel: requestParentModel });
4543
+ return resumeAsyncRun(omitUndefinedProperties({ params: paramsWithResolvedCwd, requestCwd, ctx, deps, parentModel: requestParentModel }));
3909
4544
  }
3910
4545
  if (action === "steer") {
3911
4546
  deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
@@ -3914,9 +4549,13 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3914
4549
  const targetRunId = paramsWithResolvedCwd.runId ?? paramsWithResolvedCwd.id;
3915
4550
  if (paramsWithResolvedCwd.dir) {
3916
4551
  try {
3917
- const location = resolveAsyncRunLocation(paramsWithResolvedCwd, ASYNC_DIR, RESULTS_DIR);
4552
+ const location = resolveAsyncRunLocation(paramsWithResolvedCwd, DIRS.async, DIRS.results);
3918
4553
  const runId = location.resolvedId ?? targetRunId ?? path.basename(location.asyncDir ?? paramsWithResolvedCwd.dir);
3919
- return steerAsyncRun({
4554
+ if (location.asyncDir) {
4555
+ const unsupported = externalRunnerControlError(location.asyncDir, "steer");
4556
+ if (unsupported) return unsupported;
4557
+ }
4558
+ return steerAsyncRun(compactOptional<Parameters<typeof steerAsyncRun>[0]>({
3920
4559
  state: deps.state,
3921
4560
  runId,
3922
4561
  message,
@@ -3928,10 +4567,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3928
4567
  ? {}
3929
4568
  : {
3930
4569
  recover: ({ absoluteDeadlineAt, ...limits }) =>
3931
- resumeAsyncRun({ params: { ...limits, action: "resume", id: runId, message }, requestCwd, ctx, deps, parentModel: requestParentModel, absoluteDeadlineAt }),
4570
+ resumeAsyncRun(omitUndefinedProperties({ params: { ...limits, action: "resume", id: runId, message }, requestCwd, ctx, deps, parentModel: requestParentModel, absoluteDeadlineAt })),
3932
4571
  }
3933
4572
  ),
3934
- });
4573
+ }));
3935
4574
  } catch (error) {
3936
4575
  const text = error instanceof Error ? error.message : String(error);
3937
4576
  return { content: [{ type: "text", text }], isError: true, details: { mode: "management", results: [] } };
@@ -3940,15 +4579,19 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3940
4579
  if (!targetRunId) return { content: [{ type: "text", text: "action='steer' requires id or dir." }], isError: true, details: { mode: "management", results: [] } };
3941
4580
  let resolved: ResolvedSubagentRunId | undefined;
3942
4581
  try {
3943
- resolved = resolveSubagentRunId(targetRunId, { state: deps.state, nested: nestedResolutionScopeForExecutor(deps) });
4582
+ resolved = resolveSubagentRunId(targetRunId, omitUndefinedProperties({ state: deps.state, nested: nestedResolutionScopeForExecutor(deps) }));
3944
4583
  } catch (error) {
3945
4584
  const text = error instanceof Error ? error.message : String(error);
3946
4585
  return { content: [{ type: "text", text }], isError: true, details: { mode: "management", results: [] } };
3947
4586
  }
3948
- if (resolved?.kind === "nested") return steerNestedRun({ target: resolved, message, index: paramsWithResolvedCwd.index, signal });
4587
+ if (resolved?.kind === "nested") return steerNestedRun(omitUndefinedProperties({ target: resolved, message, index: paramsWithResolvedCwd.index, signal }));
3949
4588
  if (resolved?.kind === "foreground") return { content: [{ type: "text", text: "action='steer' currently supports live async Pi child sessions only; use action='interrupt' or action='resume' for foreground runs." }], isError: true, details: { mode: "management", results: [] } };
3950
4589
  if (resolved?.kind !== "async") return { content: [{ type: "text", text: `No async run found for '${targetRunId}'.` }], isError: true, details: { mode: "management", results: [] } };
3951
- return steerAsyncRun({
4590
+ if (resolved.location.asyncDir) {
4591
+ const unsupported = externalRunnerControlError(resolved.location.asyncDir, "steer");
4592
+ if (unsupported) return unsupported;
4593
+ }
4594
+ return steerAsyncRun(compactOptional<Parameters<typeof steerAsyncRun>[0]>({
3952
4595
  state: deps.state,
3953
4596
  runId: resolved.id,
3954
4597
  message,
@@ -3960,22 +4603,29 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3960
4603
  ? {}
3961
4604
  : {
3962
4605
  recover: ({ absoluteDeadlineAt, ...limits }) =>
3963
- resumeAsyncRun({
4606
+ resumeAsyncRun(omitUndefinedProperties({
3964
4607
  params: { ...limits, action: "resume", id: resolved!.id, message },
3965
4608
  requestCwd,
3966
4609
  ctx,
3967
4610
  deps,
3968
4611
  parentModel: requestParentModel,
3969
4612
  absoluteDeadlineAt,
3970
- }),
4613
+ })),
3971
4614
  }
3972
4615
  ),
3973
- });
4616
+ }));
3974
4617
  }
3975
4618
  if (action === "append-step") {
3976
- return appendStepToAsyncChain({ params: paramsWithResolvedCwd, requestCwd, ctx, deps, parentModel: requestParentModel });
4619
+ return appendStepToAsyncChain(omitUndefinedProperties({ params: paramsWithResolvedCwd, requestCwd, ctx, deps, parentModel: requestParentModel }));
3977
4620
  }
3978
- if (action === "schedule" || action === "schedule-list" || action === "schedule-status" || action === "schedule-cancel") {
4621
+ if (action.startsWith("schedule.")) {
4622
+ if (deps.allowMutatingManagementActions === false && MUTATING_MANAGEMENT_ACTIONS.has(action)) {
4623
+ return {
4624
+ content: [{ type: "text", text: `Action '${action}' is not available from child-safe subagent fanout mode.` }],
4625
+ isError: true,
4626
+ details: { mode: "management", results: [] },
4627
+ };
4628
+ }
3979
4629
  if (!deps.handleScheduledRunAction) {
3980
4630
  return {
3981
4631
  content: [{ type: "text", text: `Action '${action}' is not available in this subagent context.` }],
@@ -3987,11 +4637,17 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3987
4637
  }
3988
4638
  if (action === "stop") {
3989
4639
  const targetRunId = paramsWithResolvedCwd.runId ?? paramsWithResolvedCwd.id;
4640
+ const workflowController = targetRunId ? deps.state.workflowControllers?.get(targetRunId) : undefined;
4641
+ if (workflowController) {
4642
+ workflowController.abort(new Error("Workflow stopped by user."));
4643
+ return { content: [{ type: "text", text: `Stop requested for async workflow ${targetRunId}.` }], details: { mode: "management", results: [] } };
4644
+ }
3990
4645
  let resolved: ResolvedSubagentRunId | undefined;
3991
4646
  if (paramsWithResolvedCwd.dir) {
3992
4647
  try {
3993
- const location = resolveAsyncRunLocation(paramsWithResolvedCwd, ASYNC_DIR, RESULTS_DIR);
3994
- return stopAsyncRun(deps.state, location.resolvedId ?? targetRunId ?? path.basename(location.asyncDir ?? paramsWithResolvedCwd.dir), deps.kill, location);
4648
+ const location = resolveAsyncRunLocation(paramsWithResolvedCwd, DIRS.async, DIRS.results);
4649
+ const stopResult = stopAsyncRun(deps.state, location.resolvedId ?? targetRunId ?? path.basename(location.asyncDir ?? paramsWithResolvedCwd.dir), deps.kill, location);
4650
+ return stopResult ?? { content: [{ type: "text", text: `No running or queued async run was found for '${targetRunId ?? paramsWithResolvedCwd.dir}'.` }], isError: true, details: { mode: "management", results: [] } };
3995
4651
  } catch (error) {
3996
4652
  const text = error instanceof Error ? error.message : String(error);
3997
4653
  return { content: [{ type: "text", text }], isError: true, details: { mode: "management", results: [] } };
@@ -3999,13 +4655,19 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3999
4655
  }
4000
4656
  if (!targetRunId) return { content: [{ type: "text", text: "action='stop' requires id or dir." }], isError: true, details: { mode: "management", results: [] } };
4001
4657
  try {
4002
- resolved = resolveSubagentRunId(targetRunId, { state: deps.state, nested: nestedResolutionScopeForExecutor(deps) });
4658
+ resolved = resolveSubagentRunId(targetRunId, omitUndefinedProperties({ state: deps.state, nested: nestedResolutionScopeForExecutor(deps) }));
4003
4659
  } catch (error) {
4004
4660
  const message = error instanceof Error ? error.message : String(error);
4005
4661
  return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
4006
4662
  }
4007
4663
  if (resolved?.kind === "nested") return { content: [{ type: "text", text: "action='stop' supports current-session top-level async runs only." }], isError: true, details: { mode: "management", results: [] } };
4008
4664
  if (resolved?.kind === "foreground") return { content: [{ type: "text", text: "action='stop' supports async runs only. Use action='interrupt' for foreground runs." }], isError: true, details: { mode: "management", results: [] } };
4665
+ if (resolved?.kind === "async") {
4666
+ const existingStatus = readStatus(resolved.location.asyncDir ?? "");
4667
+ if (existingStatus?.mode === "workflow" && existingStatus.state === "running") {
4668
+ return { content: [{ type: "text", text: `Workflow ${resolved.id} is not controlled by this extension runtime; reload recovery cannot stop it safely.` }], isError: true, details: { mode: "management", results: [] } };
4669
+ }
4670
+ }
4009
4671
  const stopResult = stopAsyncRun(
4010
4672
  deps.state,
4011
4673
  resolved?.kind === "async" ? resolved.id : targetRunId,
@@ -4024,7 +4686,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4024
4686
  let resolved: ResolvedSubagentRunId | undefined;
4025
4687
  if (targetRunId) {
4026
4688
  try {
4027
- resolved = resolveSubagentRunId(targetRunId, { state: deps.state, nested: nestedResolutionScopeForExecutor(deps) });
4689
+ resolved = resolveSubagentRunId(targetRunId, omitUndefinedProperties({ state: deps.state, nested: nestedResolutionScopeForExecutor(deps) }));
4028
4690
  } catch (error) {
4029
4691
  const message = error instanceof Error ? error.message : String(error);
4030
4692
  return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
@@ -4036,7 +4698,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4036
4698
  const interrupted = foreground.interrupt();
4037
4699
  if (interrupted) {
4038
4700
  foreground.updatedAt = Date.now();
4039
- foreground.currentActivityState = undefined;
4701
+ delete foreground.currentActivityState;
4040
4702
  return {
4041
4703
  content: [{ type: "text", text: `Interrupt requested for foreground run ${foreground.runId}.` }],
4042
4704
  details: { mode: "management", results: [] },
@@ -4171,7 +4833,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4171
4833
  const spawnPreflight = preflightSpawnBudget(
4172
4834
  deps.state,
4173
4835
  deps.config,
4174
- deps.state.currentSessionId,
4836
+ requestSessionId,
4175
4837
  requestedSpawns,
4176
4838
  );
4177
4839
  if (spawnPreflight.error) return spawnBudgetErrorResult(spawnPreflight.error, foregroundMode);
@@ -4216,6 +4878,17 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4216
4878
  const requestedAsync = effectiveParams.async ?? deps.asyncByDefault;
4217
4879
  const backgroundRequestedWhileClarifying = (hasChain || hasTasks) && requestedAsync && effectiveParams.clarify === true;
4218
4880
  const effectiveAsync = requestedAsync && effectiveParams.clarify !== true;
4881
+ const selectedAgentNames = hasSingle
4882
+ ? [effectiveParams.agent!]
4883
+ : hasTasks
4884
+ ? (effectiveParams.tasks ?? []).map((task) => task.agent)
4885
+ : (effectiveParams.chain ?? []).flatMap((step) => getStepAgents(step as ChainStep));
4886
+ const externalAgent = selectedAgentNames
4887
+ .map((name) => agents.find((agent) => agent.name === name))
4888
+ .find((agent) => agent?.runner?.type === "external-cli");
4889
+ if (externalAgent && (!effectiveAsync || effectiveParams.foregroundOnly === true)) {
4890
+ return buildRequestedModeError(effectiveParams, `Agent '${externalAgent.name}' uses runner.type='external-cli', which currently supports async/background execution only. Omit async or pass async:true; clarify and foregroundOnly are unsupported.`);
4891
+ }
4219
4892
  const foregroundTimeout = resolveForegroundTimeout(
4220
4893
  effectiveParams,
4221
4894
  effectiveAsync ? undefined : DEFAULT_FOREGROUND_TIMEOUT_MS,
@@ -4223,11 +4896,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4223
4896
  if (foregroundTimeout.error) return buildRequestedModeError(effectiveParams, foregroundTimeout.error);
4224
4897
  const controlConfig = resolveControlConfig(deps.config.control, effectiveParams.control);
4225
4898
 
4226
- const artifactConfig: ArtifactConfig = {
4899
+ const artifactConfig: ArtifactConfig = omitUndefinedProperties({
4227
4900
  ...DEFAULT_ARTIFACT_CONFIG,
4228
4901
  enabled: effectiveParams.artifacts !== false,
4229
4902
  dir: deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir,
4230
- };
4903
+ });
4231
4904
  const artifactsDir = getArtifactsDir(parentSessionFile, effectiveCwd, artifactConfig.dir);
4232
4905
 
4233
4906
  let sessionRoot: string;
@@ -4284,15 +4957,48 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4284
4957
  }, contextPolicy.contextSummary))
4285
4958
  : undefined;
4286
4959
 
4960
+ let missionBinding: MissionLaunchBinding | undefined;
4961
+ let missionWarning: string | undefined;
4962
+ const explicitMission = effectiveParams.missionId !== undefined || effectiveParams.mission !== undefined;
4963
+ try {
4964
+ missionBinding = prepareMissionLaunch({
4965
+ params: effectiveParams,
4966
+ projectRoot: effectiveCwd,
4967
+ ...(deps.config.missions ? { config: deps.config.missions } : {}),
4968
+ ownerSessionId: requestSessionId,
4969
+ });
4970
+ } catch (error) {
4971
+ if (explicitMission) return toExecutionErrorResult(effectiveParams, error, contextPolicy.contextSummary);
4972
+ missionWarning = `Mission tracking unavailable: ${error instanceof Error ? error.message : String(error)}`;
4973
+ }
4974
+
4975
+ const attachMission = (result: AgentToolResult<Details>): AgentToolResult<Details> => {
4976
+ if (!missionBinding) return missionWarning ? { ...result, details: { ...result.details, missionWarning } } : result;
4977
+ try {
4978
+ return attachMissionToLaunchResult({ binding: missionBinding, result });
4979
+ } catch (error) {
4980
+ const warning = `Mission tracking unavailable after launch: ${error instanceof Error ? error.message : String(error)}`;
4981
+ if (explicitMission) {
4982
+ return {
4983
+ ...result,
4984
+ isError: true,
4985
+ content: [...result.content, { type: "text", text: warning }],
4986
+ details: { ...result.details, missionWarning: warning },
4987
+ };
4988
+ }
4989
+ return { ...result, details: { ...result.details, missionWarning: warning } };
4990
+ }
4991
+ };
4992
+
4287
4993
  const reservation = reserveSpawnBudget(
4288
4994
  deps.state,
4289
4995
  deps.config,
4290
- deps.state.currentSessionId,
4996
+ requestSessionId,
4291
4997
  requestedSpawns,
4292
4998
  );
4293
- if (reservation.error) return spawnBudgetErrorResult(reservation.error, foregroundMode);
4999
+ if (reservation.error) return attachMission(spawnBudgetErrorResult(reservation.error, foregroundMode));
4294
5000
 
4295
- const execData: ExecutionContextData = {
5001
+ const execData: ExecutionContextData = omitUndefinedProperties({
4296
5002
  params: effectiveParams,
4297
5003
  effectiveCwd,
4298
5004
  ctx,
@@ -4322,18 +5028,19 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4322
5028
  contextPolicy,
4323
5029
  modelScope,
4324
5030
  parentModel: requestParentModel,
4325
- parentSessionId: deps.state.currentSessionId,
4326
- capabilityCeiling: resolveCurrentSubagentCapabilityCeiling(deps.state.currentSessionId ?? undefined),
4327
- };
5031
+ parentSessionId: requestSessionId,
5032
+ parentPiSessionId: requestPiSessionId,
5033
+ capabilityCeiling: resolveCurrentSubagentCapabilityCeiling(requestSessionId),
5034
+ });
4328
5035
 
4329
5036
  const foregroundDescription = effectiveParams.task?.trim()
4330
5037
  || effectiveParams.tasks?.[0]?.task?.trim()
4331
5038
  || (effectiveParams.chain ? firstRawChainTask(effectiveParams.chain)?.trim() : undefined);
4332
5039
  const foregroundControl: ForegroundRunControl | undefined = effectiveAsync
4333
5040
  ? undefined
4334
- : {
5041
+ : compactOptional<ForegroundRunControl>({
4335
5042
  runId,
4336
- ...(deps.state.currentSessionId ? { sessionId: deps.state.currentSessionId } : {}),
5043
+ sessionId: requestSessionId,
4337
5044
  mode: foregroundMode,
4338
5045
  startedAt: Date.now(),
4339
5046
  updatedAt: Date.now(),
@@ -4347,7 +5054,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4347
5054
  schedulingOwners: 1,
4348
5055
  nestedRoute,
4349
5056
  interrupt: undefined,
4350
- };
5057
+ });
4351
5058
  if (foregroundControl) {
4352
5059
  deps.state.foregroundControls.set(runId, foregroundControl);
4353
5060
  deps.state.lastForegroundControlId = runId;
@@ -4367,21 +5074,33 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4367
5074
  const errorText = result?.isError
4368
5075
  ? result.content.find((item) => item.type === "text")?.text
4369
5076
  : undefined;
4370
- const agentsForSummary = hasTasks && effectiveParams.tasks
4371
- ? effectiveParams.tasks.map((task) => task.agent)
4372
- : hasChain && effectiveParams.chain
4373
- ? effectiveParams.chain.flatMap((step) => isParallelStep(step) ? step.parallel.map((task) => task.agent) : [(step as SequentialStep).agent])
4374
- : effectiveParams.agent ? [effectiveParams.agent] : [];
5077
+ let startedLaunches: StaticLaunchSummary[];
5078
+ try {
5079
+ startedLaunches = collectStaticLaunchSummaries({
5080
+ params: effectiveParams,
5081
+ agents,
5082
+ parentModel: requestParentModel,
5083
+ availableModels: ctx.modelRegistry.getAvailable().map(toModelInfo),
5084
+ currentProvider: requestParentModel?.provider,
5085
+ modelScope,
5086
+ thinkingOverrideForTask: forkThinkingOverrideForTask,
5087
+ dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
5088
+ });
5089
+ } catch (error) {
5090
+ console.error("Failed to resolve nested foreground launch metadata:", error);
5091
+ startedLaunches = selectedAgentNames.map((agent) => ({ agent }));
5092
+ }
5093
+ const agentsForSummary = startedLaunches.map((launch) => launch.agent);
4375
5094
  const leafIntercomTarget = intercomBridge.active && agentsForSummary[0]
4376
5095
  ? resolveSubagentIntercomTarget(runId, agentsForSummary[0], 0)
4377
5096
  : undefined;
4378
5097
  try {
4379
- writeNestedEvent(inheritedNestedRoute, {
5098
+ writeNestedEvent(inheritedNestedRoute, compactOptional<Parameters<typeof writeNestedEvent>[1]>({
4380
5099
  type,
4381
5100
  ts: now,
4382
5101
  parentRunId: nestedParentAddress.parentRunId,
4383
5102
  parentStepIndex: nestedParentAddress.parentStepIndex,
4384
- child: {
5103
+ child: compactOptional<NestedRunSummary>({
4385
5104
  id: runId,
4386
5105
  parentRunId: nestedParentAddress.parentRunId,
4387
5106
  parentStepIndex: nestedParentAddress.parentStepIndex,
@@ -4395,19 +5114,32 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4395
5114
  state,
4396
5115
  agent: agentsForSummary[0],
4397
5116
  agents: agentsForSummary,
5117
+ ...(agentsForSummary.length === 1 && (type === "subagent.nested.started" ? startedLaunches[0]?.model : details?.results[0]?.model) ? { model: type === "subagent.nested.started" ? startedLaunches[0]?.model : details?.results[0]?.model } : {}),
5118
+ ...(agentsForSummary.length === 1 && (type === "subagent.nested.started" ? startedLaunches[0]?.thinking : details?.results[0]?.thinking) ? { thinking: type === "subagent.nested.started" ? startedLaunches[0]?.thinking : details?.results[0]?.thinking } : {}),
4398
5119
  startedAt: foregroundControl?.startedAt ?? now,
4399
5120
  ...(state !== "running" ? { endedAt: now } : {}),
4400
5121
  lastUpdate: now,
4401
5122
  ...(details?.totalCost ? { totalCost: details.totalCost } : {}),
4402
5123
  ...(errorText ? { error: errorText } : {}),
4403
- ...(details?.results.length ? { steps: details.results.map((child) => ({
4404
- agent: child.agent,
4405
- status: child.interrupted || child.detached ? "paused" : child.exitCode === 0 ? "complete" : "failed",
4406
- ...(child.sessionFile ? { sessionFile: child.sessionFile } : {}),
4407
- ...(child.error ? { error: child.error } : {}),
4408
- })) } : {}),
4409
- },
4410
- });
5124
+ ...(type === "subagent.nested.started"
5125
+ ? { steps: startedLaunches.map((launch) => ({
5126
+ agent: launch.agent,
5127
+ status: "running" as const,
5128
+ ...(launch.model ? { model: launch.model } : {}),
5129
+ ...(launch.thinking ? { thinking: launch.thinking } : {}),
5130
+ })) }
5131
+ : details?.results.length
5132
+ ? { steps: details.results.map((child) => ({
5133
+ agent: child.agent,
5134
+ status: child.interrupted || child.detached ? "paused" as const : child.exitCode === 0 ? "complete" as const : "failed" as const,
5135
+ ...(child.model ? { model: child.model } : {}),
5136
+ ...(child.thinking ? { thinking: child.thinking } : {}),
5137
+ ...(child.sessionFile ? { sessionFile: child.sessionFile } : {}),
5138
+ ...(child.error ? { error: child.error } : {}),
5139
+ })) }
5140
+ : {}),
5141
+ }),
5142
+ }));
4411
5143
  } catch (error) {
4412
5144
  console.error("Failed to emit nested foreground status event:", error);
4413
5145
  }
@@ -4416,7 +5148,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4416
5148
  let nestedForegroundStarted = false;
4417
5149
  try {
4418
5150
  const asyncResult = runAsyncPath(execData, deps);
4419
- if (asyncResult) return withResolvedContext(withForkThinkingNotes(asyncResult, forkThinkingDowngrades), contextPolicy.contextSummary);
5151
+ if (asyncResult) return attachMission(withResolvedContext(withForkThinkingNotes(asyncResult, forkThinkingDowngrades), contextPolicy.contextSummary));
4420
5152
  if (foregroundControl) {
4421
5153
  writeNestedForegroundEvent("subagent.nested.started");
4422
5154
  nestedForegroundStarted = true;
@@ -4424,22 +5156,22 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4424
5156
  if (hasChain && effectiveParams.chain) {
4425
5157
  const result = await runChainPath(execData, deps);
4426
5158
  writeNestedForegroundEvent("subagent.nested.completed", result);
4427
- return withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary);
5159
+ return attachMission(withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary));
4428
5160
  }
4429
5161
  if (hasTasks && effectiveParams.tasks) {
4430
5162
  const result = await runParallelPath(execData, deps);
4431
5163
  writeNestedForegroundEvent("subagent.nested.completed", result);
4432
- return withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary);
5164
+ return attachMission(withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary));
4433
5165
  }
4434
5166
  if (hasSingle) {
4435
5167
  const result = await runSinglePath(execData, deps);
4436
5168
  writeNestedForegroundEvent("subagent.nested.completed", result);
4437
- return withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary);
5169
+ return attachMission(withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary));
4438
5170
  }
4439
5171
  } catch (error) {
4440
5172
  const errorResult = withForkThinkingNotes(toExecutionErrorResult(effectiveParams, error, contextPolicy.contextSummary), forkThinkingDowngrades);
4441
5173
  if (nestedForegroundStarted) writeNestedForegroundEvent("subagent.nested.completed", errorResult);
4442
- return errorResult;
5174
+ return attachMission(errorResult);
4443
5175
  } finally {
4444
5176
  if (foregroundControl) {
4445
5177
  settleForegroundSchedulingOwner(foregroundControl);
@@ -4497,5 +5229,23 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4497
5229
  return execute(id, delegatedParams, signal, onUpdate, ctx);
4498
5230
  };
4499
5231
 
4500
- return { execute: executeWithSingleDispatchGuard, executeDelegated };
5232
+ const executeScheduled = (
5233
+ id: string,
5234
+ params: SubagentParamsLike,
5235
+ signal: AbortSignal,
5236
+ ctx: ExtensionContext,
5237
+ ) => {
5238
+ const ownerSessionId = resolveCurrentSessionId(ctx.sessionManager);
5239
+ let ownerExecutor = scheduledOwnerExecutors.get(ownerSessionId);
5240
+ if (!ownerExecutor) {
5241
+ ownerExecutor = createSubagentExecutor({
5242
+ ...deps,
5243
+ state: createScheduledOwnerState(deps.state, ownerSessionId, ctx),
5244
+ });
5245
+ scheduledOwnerExecutors.set(ownerSessionId, ownerExecutor);
5246
+ }
5247
+ return ownerExecutor.execute(id, params, signal, undefined, ctx);
5248
+ };
5249
+
5250
+ return { execute: executeWithSingleDispatchGuard, executeDelegated, executeScheduled };
4501
5251
  }