pi-subagents 0.40.0 → 0.42.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 (121) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +54 -1879
  3. package/agents/oracle.md +1 -0
  4. package/inspector-runner.mjs +11 -0
  5. package/package.json +13 -5
  6. package/prompts/parallel-context-build.md +1 -1
  7. package/prompts/parallel-handoff-plan.md +1 -1
  8. package/prompts/review-loop.md +1 -1
  9. package/skills/pi-subagents/SKILL.md +9 -7
  10. package/skills/pi-subagents/references/constraints-and-recipes.md +24 -32
  11. package/skills/pi-subagents/references/execution-controls.md +99 -97
  12. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  13. package/skills/pi-subagents/references/prompting-and-roles.md +23 -30
  14. package/src/agents/agent-management.ts +155 -65
  15. package/src/agents/agent-serializer.ts +19 -0
  16. package/src/agents/agents.ts +154 -71
  17. package/src/agents/chain-serializer.ts +10 -7
  18. package/src/agents/frontmatter.ts +5 -3
  19. package/src/agents/identity.ts +1 -1
  20. package/src/agents/proactive-skills.ts +13 -10
  21. package/src/agents/skills.ts +23 -6
  22. package/src/api/control-channel.ts +4 -0
  23. package/src/api/delegation.ts +26 -194
  24. package/src/api/external-runs.ts +129 -0
  25. package/src/api/intercom-bridge.ts +3 -0
  26. package/src/api/pi-args.ts +5 -0
  27. package/src/api/preflight.ts +3 -3
  28. package/src/api/shared-types.ts +19 -0
  29. package/src/extension/config.ts +10 -0
  30. package/src/extension/control-notices.ts +5 -39
  31. package/src/extension/doctor.ts +10 -9
  32. package/src/extension/fanout-child.ts +7 -4
  33. package/src/extension/index.ts +232 -68
  34. package/src/extension/rpc.ts +18 -12
  35. package/src/extension/schemas.ts +48 -37
  36. package/src/extension/tool-description.ts +36 -86
  37. package/src/inspectors/herdr/actions.ts +229 -0
  38. package/src/inspectors/herdr/client.ts +130 -0
  39. package/src/inspectors/herdr/inspector-runner.ts +141 -0
  40. package/src/inspectors/herdr/project-panes.ts +154 -0
  41. package/src/integrations/herdr-status.ts +330 -0
  42. package/src/intercom/intercom-bridge.ts +3 -2
  43. package/src/intercom/result-intercom.ts +5 -1
  44. package/src/missions/actions.ts +374 -0
  45. package/src/missions/lifecycle.ts +314 -0
  46. package/src/missions/store.ts +442 -0
  47. package/src/missions/types.ts +135 -0
  48. package/src/policy/authority.ts +46 -0
  49. package/src/profiles/profiles.ts +29 -3
  50. package/src/runs/background/async-execution.ts +110 -50
  51. package/src/runs/background/async-job-tracker.ts +27 -4
  52. package/src/runs/background/async-resume.ts +6 -6
  53. package/src/runs/background/async-status.ts +29 -1
  54. package/src/runs/background/auto-drain.ts +3 -3
  55. package/src/runs/background/chain-append.ts +3 -2
  56. package/src/runs/background/control-channel.ts +9 -7
  57. package/src/runs/background/fleet-view.ts +3 -4
  58. package/src/runs/background/notify.ts +2 -1
  59. package/src/runs/background/process-terminal.ts +5 -5
  60. package/src/runs/background/result-watcher.ts +13 -5
  61. package/src/runs/background/run-id-resolver.ts +3 -3
  62. package/src/runs/background/run-status.ts +35 -8
  63. package/src/runs/background/scheduled-runs.ts +602 -375
  64. package/src/runs/background/stale-run-reconciler.ts +3 -3
  65. package/src/runs/background/subagent-runner.ts +608 -445
  66. package/src/runs/background/subagent-wait.ts +50 -9
  67. package/src/runs/background/wait-subscriptions.ts +253 -0
  68. package/src/runs/background/wait-tool.ts +12 -4
  69. package/src/runs/foreground/async-steering-action.ts +3 -3
  70. package/src/runs/foreground/chain-clarify.ts +8 -4
  71. package/src/runs/foreground/chain-execution.ts +56 -30
  72. package/src/runs/foreground/execution.ts +34 -9
  73. package/src/runs/foreground/subagent-executor.ts +1036 -274
  74. package/src/runs/shared/acceptance.ts +28 -6
  75. package/src/runs/shared/child-protocol.ts +302 -22
  76. package/src/runs/shared/dynamic-fanout.ts +1 -1
  77. package/src/runs/shared/external-cli-runner.ts +130 -0
  78. package/src/runs/shared/long-running-guard.ts +42 -1
  79. package/src/runs/shared/mcp-direct-tool-allowlist.ts +2 -0
  80. package/src/runs/shared/nested-events.ts +59 -5
  81. package/src/runs/shared/nested-render.ts +9 -4
  82. package/src/runs/shared/parallel-handoff.ts +86 -2
  83. package/src/runs/shared/parallel-utils.ts +11 -2
  84. package/src/runs/shared/permissions.ts +95 -0
  85. package/src/runs/shared/pi-args.ts +468 -109
  86. package/src/runs/shared/pi-spawn.ts +11 -1
  87. package/src/runs/shared/run-history.ts +1 -1
  88. package/src/runs/shared/subagent-prompt-runtime.ts +36 -5
  89. package/src/runs/shared/subagent-startup-retry.ts +5 -2
  90. package/src/runs/shared/turn-budget.ts +6 -6
  91. package/src/runs/shared/worktree.ts +122 -12
  92. package/src/shared/accessible-dir.ts +29 -7
  93. package/src/shared/artifacts.ts +143 -1
  94. package/src/shared/fork-context.ts +3 -2
  95. package/src/shared/launch-contract.ts +1 -0
  96. package/src/shared/settings.ts +10 -0
  97. package/src/shared/types.ts +156 -14
  98. package/src/shared/utils.ts +8 -6
  99. package/src/slash/delegation-adapters.ts +32 -194
  100. package/src/slash/delegation-request.ts +43 -126
  101. package/src/slash/prompt-template-bridge.ts +158 -205
  102. package/src/slash/prompt-workflows.ts +21 -57
  103. package/src/slash/slash-bridge.ts +14 -0
  104. package/src/slash/slash-commands.ts +31 -632
  105. package/src/slash/subagents-admin.ts +18 -14
  106. package/src/tui/fleet-status.ts +156 -21
  107. package/src/tui/fleet-transcript.ts +110 -5
  108. package/src/tui/fleet.ts +57 -25
  109. package/src/tui/render.ts +368 -152
  110. package/src/types/pi-runtime-compat.d.ts +8 -0
  111. package/src/watchdog/lsp-diagnostics.ts +12 -7
  112. package/src/watchdog/model-selection.ts +2 -2
  113. package/src/watchdog/permission-arbiter.ts +145 -0
  114. package/src/watchdog/register-child.ts +1 -1
  115. package/src/watchdog/register-main.ts +1 -1
  116. package/src/watchdog/review.ts +4 -1
  117. package/src/watchdog/runtime.ts +3 -2
  118. package/src/workflows/chat-progress.ts +140 -0
  119. package/src/workflows/scripted-workflow.ts +415 -0
  120. package/agents/advisor.md +0 -73
  121. 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, getProjectArtifactPackagingWarning } 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,
@@ -1518,6 +1631,7 @@ async function maybeBuildForegroundIntercomReceipt(input: {
1518
1631
  mode: SubagentRunMode;
1519
1632
  details: Details;
1520
1633
  nestedChildren?: NestedRunSummary[];
1634
+ preserveDetailsOutputs?: boolean;
1521
1635
  }): Promise<{ text: string; details: Details } | null> {
1522
1636
  const payload = await emitForegroundResultIntercom({
1523
1637
  pi: input.pi,
@@ -1532,7 +1646,7 @@ async function maybeBuildForegroundIntercomReceipt(input: {
1532
1646
  if (!payload) return null;
1533
1647
  return {
1534
1648
  text: formatSubagentResultReceipt({ mode: input.mode, runId: input.runId, payload }),
1535
- details: stripDetailsOutputsForIntercomReceipt(input.details),
1649
+ details: input.preserveDetailsOutputs ? input.details : stripDetailsOutputsForIntercomReceipt(input.details),
1536
1650
  };
1537
1651
  }
1538
1652
 
@@ -1551,7 +1665,7 @@ function canonicalizeExecutionParams(params: SubagentParamsLike, agents: AgentCo
1551
1665
  if (params.agent) {
1552
1666
  const result = resolve(params.agent);
1553
1667
  if (result.error) return { error: result.error };
1554
- params = { ...params, agent: result.name };
1668
+ params = omitUndefinedProperties({ ...params, agent: result.name });
1555
1669
  }
1556
1670
  if (params.tasks) {
1557
1671
  const tasks: TaskParam[] = [];
@@ -1568,7 +1682,7 @@ function canonicalizeExecutionParams(params: SubagentParamsLike, agents: AgentCo
1568
1682
  for (let index = 0; index < params.chain.length; index++) {
1569
1683
  const step = params.chain[index]!;
1570
1684
  if (isParallelStep(step)) {
1571
- const parallel: typeof step.parallel = [];
1685
+ const parallel: ParallelTaskItem[] = [];
1572
1686
  for (let taskIndex = 0; taskIndex < step.parallel.length; taskIndex++) {
1573
1687
  const task = step.parallel[taskIndex]!;
1574
1688
  const result = resolve(task.agent, `step ${index + 1}, task ${taskIndex + 1}`);
@@ -1618,7 +1732,7 @@ function validateExecutionInput(
1618
1732
  };
1619
1733
  }
1620
1734
 
1621
- const acceptanceErrors = validateExecutionAcceptance(params);
1735
+ const acceptanceErrors = validateExecutionAcceptance(params as Parameters<typeof validateExecutionAcceptance>[0]);
1622
1736
  if (acceptanceErrors.length > 0) {
1623
1737
  return {
1624
1738
  content: [{ type: "text", text: acceptanceErrors.join(" ") }],
@@ -1707,7 +1821,7 @@ function validateExecutionInput(
1707
1821
  function validateExecutionChainBindings(params: SubagentParamsLike, dynamicFanoutMaxItems?: number): AgentToolResult<Details> | null {
1708
1822
  if ((params.chain?.length ?? 0) === 0) return null;
1709
1823
  try {
1710
- validateChainOutputBindingsWithContext(params.chain as ChainStep[], { maxItems: dynamicFanoutMaxItems });
1824
+ validateChainOutputBindingsWithContext(params.chain as ChainStep[], dynamicFanoutMaxItems === undefined ? {} : { maxItems: dynamicFanoutMaxItems });
1711
1825
  } catch (error) {
1712
1826
  if (error instanceof ChainOutputValidationError) {
1713
1827
  return {
@@ -1722,6 +1836,7 @@ function validateExecutionChainBindings(params: SubagentParamsLike, dynamicFanou
1722
1836
  }
1723
1837
 
1724
1838
  function getRequestedModeLabel(params: SubagentParamsLike): Details["mode"] {
1839
+ if (params.workflowScript !== undefined) return "workflow";
1725
1840
  if ((params.chain?.length ?? 0) > 0) return "chain";
1726
1841
  if ((params.tasks?.length ?? 0) > 0) return "parallel";
1727
1842
  if (params.agent) return "single";
@@ -1745,12 +1860,12 @@ function resolveAgentDefaultContextPolicy(params: SubagentParamsLike, agents: Ag
1745
1860
  const requestedAgentNames = collectRequestedAgentNames(params);
1746
1861
  const contextSummary = summarizeContextModes(requestedAgentNames.map((name) => contextForAgent(name)));
1747
1862
  const usesFork = contextSummary === "fork" || contextSummary === "mixed";
1748
- return {
1863
+ return omitUndefinedProperties({
1749
1864
  params,
1750
1865
  contextForAgent,
1751
1866
  contextSummary,
1752
1867
  usesFork,
1753
- };
1868
+ });
1754
1869
  }
1755
1870
 
1756
1871
  function resolveExplicitContextPolicy(params: SubagentParamsLike): AgentDefaultContextPolicy {
@@ -1826,7 +1941,8 @@ function resolveForegroundTimeout(params: SubagentParamsLike, defaultTimeoutMs?:
1826
1941
  if (rawTimeout !== undefined && rawMaxRuntime !== undefined && rawTimeout !== rawMaxRuntime) {
1827
1942
  return { error: "timeoutMs and maxRuntimeMs are aliases; provide only one value or use the same value for both." };
1828
1943
  }
1829
- return { timeoutMs: rawTimeout ?? rawMaxRuntime };
1944
+ const timeoutMs = rawTimeout ?? rawMaxRuntime;
1945
+ return timeoutMs === undefined ? {} : { timeoutMs };
1830
1946
  }
1831
1947
 
1832
1948
  function resolveToolBudget(
@@ -1835,7 +1951,7 @@ function resolveToolBudget(
1835
1951
  options?: { minimumHard?: 0 | 1 },
1836
1952
  ): { toolBudget?: ResolvedToolBudget; error?: string } {
1837
1953
  const resolved = validateToolBudgetConfig(raw, label, options);
1838
- return { toolBudget: resolved.budget, error: resolved.error };
1954
+ return { ...(resolved.budget === undefined ? {} : { toolBudget: resolved.budget }), ...(resolved.error === undefined ? {} : { error: resolved.error }) };
1839
1955
  }
1840
1956
 
1841
1957
  function resolveEffectiveToolBudget(input: { stepBudget?: ToolBudgetConfig; runBudget?: ResolvedToolBudget; agentBudget?: ToolBudgetConfig; configBudget?: ToolBudgetConfig }): { toolBudget?: ResolvedToolBudget; error?: string } {
@@ -1869,7 +1985,7 @@ function expandChainParallelCounts(chain: ChainStep[]): { chain?: ChainStep[]; e
1869
1985
  expandedChain.push(step);
1870
1986
  continue;
1871
1987
  }
1872
- const expandedParallel = [];
1988
+ const expandedParallel: ParallelTaskItem[] = [];
1873
1989
  for (let taskIndex = 0; taskIndex < step.parallel.length; taskIndex++) {
1874
1990
  const task = step.parallel[taskIndex]!;
1875
1991
  const rawCount = (task as typeof task & { count?: unknown }).count;
@@ -1892,14 +2008,14 @@ function normalizeRepeatedParallelCounts(params: SubagentParamsLike): { params?:
1892
2008
  if (expandedTasks.error) {
1893
2009
  return { error: buildRequestedModeError(params, expandedTasks.error) };
1894
2010
  }
1895
- return { params: { ...params, tasks: expandedTasks.tasks } };
2011
+ return { params: { ...params, ...(expandedTasks.tasks === undefined ? {} : { tasks: expandedTasks.tasks }) } };
1896
2012
  }
1897
2013
  if (params.chain) {
1898
2014
  const expandedChain = expandChainParallelCounts(params.chain);
1899
2015
  if (expandedChain.error) {
1900
2016
  return { error: buildRequestedModeError(params, expandedChain.error) };
1901
2017
  }
1902
- return { params: { ...params, chain: expandedChain.chain } };
2018
+ return { params: { ...params, ...(expandedChain.chain === undefined ? {} : { chain: expandedChain.chain }) } };
1903
2019
  }
1904
2020
  return { params };
1905
2021
  }
@@ -2005,6 +2121,87 @@ function collectChainThinkingOverrides(
2005
2121
  return thinkingOverrides;
2006
2122
  }
2007
2123
 
2124
+ type StaticLaunchSummary = { agent: string; model?: string; thinking?: string };
2125
+
2126
+ function resolveStaticLaunchSummary(input: {
2127
+ agent: string;
2128
+ index: number;
2129
+ explicitModel?: string;
2130
+ agents: AgentConfig[];
2131
+ parentModel?: ParentModel;
2132
+ availableModels: ModelInfo[];
2133
+ currentProvider?: string;
2134
+ modelScope?: ModelScopeConfig;
2135
+ thinkingOverrideForTask: ForkThinkingOverrideForTask;
2136
+ }): StaticLaunchSummary {
2137
+ const agentConfig = input.agents.find((agent) => agent.name === input.agent);
2138
+ const model = resolveEffectiveSubagentModel(
2139
+ input.explicitModel,
2140
+ agentConfig?.model,
2141
+ input.parentModel,
2142
+ input.availableModels,
2143
+ input.currentProvider,
2144
+ input.modelScope === undefined ? {} : { scope: input.modelScope },
2145
+ );
2146
+ const thinkingOverride = input.thinkingOverrideForTask(input.agent, input.index, model);
2147
+ const thinking = resolveEffectiveThinking(model, thinkingOverride ?? agentConfig?.thinking);
2148
+ return {
2149
+ agent: input.agent,
2150
+ ...(model ? { model } : {}),
2151
+ ...(thinking ? { thinking } : {}),
2152
+ };
2153
+ }
2154
+
2155
+ function collectStaticLaunchSummaries(input: {
2156
+ params: SubagentParamsLike;
2157
+ agents: AgentConfig[];
2158
+ parentModel?: ParentModel;
2159
+ availableModels: ModelInfo[];
2160
+ currentProvider?: string;
2161
+ modelScope?: ModelScopeConfig;
2162
+ thinkingOverrideForTask: ForkThinkingOverrideForTask;
2163
+ dynamicFanoutMaxItems?: number;
2164
+ }): StaticLaunchSummary[] {
2165
+ const summary = (agent: string, index: number, explicitModel?: string) => resolveStaticLaunchSummary({
2166
+ agent,
2167
+ index,
2168
+ explicitModel,
2169
+ agents: input.agents,
2170
+ parentModel: input.parentModel,
2171
+ availableModels: input.availableModels,
2172
+ currentProvider: input.currentProvider,
2173
+ modelScope: input.modelScope,
2174
+ thinkingOverrideForTask: input.thinkingOverrideForTask,
2175
+ });
2176
+ if (input.params.tasks) return input.params.tasks.map((task, index) => summary(task.agent, index, task.model));
2177
+ if (input.params.chain?.length) {
2178
+ const launches: StaticLaunchSummary[] = [];
2179
+ let flatIndex = 0;
2180
+ for (const step of input.params.chain) {
2181
+ if (isParallelStep(step)) {
2182
+ for (const task of step.parallel) {
2183
+ launches.push(summary(task.agent, flatIndex, task.model));
2184
+ flatIndex++;
2185
+ }
2186
+ continue;
2187
+ }
2188
+ if (isDynamicParallelStep(step)) {
2189
+ const maxItems = step.expand.maxItems ?? input.dynamicFanoutMaxItems ?? 0;
2190
+ for (let itemIndex = 0; itemIndex < maxItems; itemIndex++) {
2191
+ launches.push(summary(step.parallel.agent, flatIndex, step.parallel.model));
2192
+ flatIndex++;
2193
+ }
2194
+ continue;
2195
+ }
2196
+ const sequential = step as SequentialStep;
2197
+ launches.push(summary(sequential.agent, flatIndex, sequential.model));
2198
+ flatIndex++;
2199
+ }
2200
+ return launches;
2201
+ }
2202
+ return input.params.agent ? [summary(input.params.agent, 0, input.params.model as string | undefined)] : [];
2203
+ }
2204
+
2008
2205
  function firstChainAgent(chain: ChainStep[]): string | undefined {
2009
2206
  const first = chain[0];
2010
2207
  if (!first) return undefined;
@@ -2032,34 +2229,34 @@ function resolveAsyncEventGoal(workflowTask: string | undefined, rawChain: Chain
2032
2229
  function wrapChainTasksForFork(chain: ChainStep[], contextPolicy: AgentDefaultContextPolicy): ChainStep[] {
2033
2230
  return chain.map((step, stepIndex) => {
2034
2231
  if (isParallelStep(step)) {
2035
- return {
2232
+ return compactOptional<ParallelStep>({
2036
2233
  ...step,
2037
- parallel: step.parallel.map((task) => ({
2234
+ parallel: step.parallel.map((task) => compactOptional<ParallelTaskItem>({
2038
2235
  ...task,
2039
2236
  task: shouldForkAgent(contextPolicy, task.agent)
2040
2237
  ? wrapForkTask(task.task ?? "{previous}")
2041
2238
  : task.task,
2042
2239
  })),
2043
- };
2240
+ });
2044
2241
  }
2045
2242
  if (isDynamicParallelStep(step)) {
2046
- return {
2243
+ return compactOptional<DynamicParallelStep>({
2047
2244
  ...step,
2048
- parallel: {
2245
+ parallel: compactOptional<DynamicParallelStep["parallel"]>({
2049
2246
  ...step.parallel,
2050
2247
  task: shouldForkAgent(contextPolicy, step.parallel.agent)
2051
2248
  ? wrapForkTask(step.parallel.task ?? "{previous}")
2052
2249
  : step.parallel.task,
2053
- },
2054
- };
2250
+ }),
2251
+ });
2055
2252
  }
2056
2253
  const sequential = step as SequentialStep;
2057
- return {
2254
+ return compactOptional<SequentialStep>({
2058
2255
  ...sequential,
2059
2256
  task: shouldForkAgent(contextPolicy, sequential.agent)
2060
2257
  ? wrapForkTask(sequential.task ?? (stepIndex === 0 ? "{task}" : "{previous}"))
2061
2258
  : sequential.task,
2062
- };
2259
+ });
2063
2260
  });
2064
2261
  }
2065
2262
 
@@ -2159,16 +2356,17 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2159
2356
  }
2160
2357
  const id = randomUUID();
2161
2358
  const parentModel = data.parentModel;
2162
- const asyncCtx = {
2359
+ const asyncCtx = compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
2163
2360
  pi: deps.pi,
2164
2361
  cwd: ctx.cwd,
2165
- currentSessionId: deps.state.currentSessionId!,
2166
- parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
2362
+ currentSessionId: data.parentSessionId!,
2363
+ parentSessionId: data.parentPiSessionId,
2167
2364
  currentModelProvider: parentModel?.provider,
2168
2365
  currentModel: parentModel,
2169
2366
  modelScope: data.modelScope,
2170
2367
  interactive: ctx.hasUI,
2171
- };
2368
+ permissions: deps.config.permissions,
2369
+ });
2172
2370
  const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo);
2173
2371
  const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
2174
2372
  const currentProvider = parentModel?.provider;
@@ -2177,7 +2375,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2177
2375
 
2178
2376
  if (hasTasks && params.tasks) {
2179
2377
  const skillOverrides = params.tasks.map((task) => normalizeSkillInput(task.skill));
2180
- const parallelTasks = params.tasks.map((task, index) => ({
2378
+ const parallelTasks = params.tasks.map((task, index) => compactOptional<ParallelTaskItem>({
2181
2379
  agent: task.agent,
2182
2380
  task: shouldForkAgent(contextPolicy, task.agent) ? wrapForkTask(task.task) : task.task,
2183
2381
  cwd: task.cwd,
@@ -2192,12 +2390,12 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2192
2390
  ...(task.agentContract !== undefined ? { agentContract: task.agentContract } : {}),
2193
2391
  ...(task.acceptance !== undefined ? { acceptance: task.acceptance } : {}),
2194
2392
  }));
2195
- return executeAsyncChain(id, {
2196
- chain: [{
2393
+ return executeAsyncChain(id, compactOptional<Parameters<typeof executeAsyncChain>[1]>({
2394
+ chain: [compactOptional<ParallelStep>({
2197
2395
  parallel: parallelTasks,
2198
2396
  concurrency: resolveTopLevelParallelConcurrency(params.concurrency, deps.config.parallel?.concurrency),
2199
2397
  worktree: params.worktree,
2200
- }],
2398
+ })],
2201
2399
  resultMode: "parallel",
2202
2400
  goal: params.tasks[0]?.task ?? "",
2203
2401
  agents,
@@ -2230,7 +2428,9 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2230
2428
  configToolBudget: data.configToolBudget,
2231
2429
  capabilityCeiling: data.capabilityCeiling,
2232
2430
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2233
- });
2431
+ parentWorkflowRunId: params.workflowParentRunId,
2432
+ workflowKey: params.workflowKey,
2433
+ }));
2234
2434
  }
2235
2435
 
2236
2436
  if (hasChain && params.chain) {
@@ -2238,7 +2438,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2238
2438
  const chainSkills = normalized === false ? [] : (normalized ?? []);
2239
2439
  const rawChain = params.chain as ChainStep[];
2240
2440
  const chain = wrapChainTasksForFork(rawChain, contextPolicy);
2241
- return executeAsyncChain(id, {
2441
+ return executeAsyncChain(id, compactOptional<Parameters<typeof executeAsyncChain>[1]>({
2242
2442
  chain,
2243
2443
  task: params.task,
2244
2444
  goal: resolveAsyncEventGoal(params.task, rawChain),
@@ -2273,7 +2473,9 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2273
2473
  configToolBudget: data.configToolBudget,
2274
2474
  capabilityCeiling: data.capabilityCeiling,
2275
2475
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2276
- });
2476
+ parentWorkflowRunId: params.workflowParentRunId,
2477
+ workflowKey: params.workflowKey,
2478
+ }));
2277
2479
  }
2278
2480
 
2279
2481
  if (hasSingle) {
@@ -2291,8 +2493,8 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2291
2493
  const normalizedSkills = normalizeSkillInput(params.skill);
2292
2494
  const skills = normalizedSkills === false ? [] : normalizedSkills;
2293
2495
  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, {
2496
+ const modelOverride = resolveEffectiveSubagentModel(params.model as string | undefined, a.model, parentModel, availableModels, currentProvider, data.modelScope === undefined ? {} : { scope: data.modelScope });
2497
+ return executeAsyncSingle(id, compactOptional<Parameters<typeof executeAsyncSingle>[1]>({
2296
2498
  agent: params.agent!,
2297
2499
  task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(params.task ?? "") : (params.task ?? ""),
2298
2500
  goal: params.task ?? "",
@@ -2331,7 +2533,9 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2331
2533
  usageBudget: data.usageBudget,
2332
2534
  configToolBudget: data.configToolBudget,
2333
2535
  capabilityCeiling: data.capabilityCeiling,
2334
- });
2536
+ parentWorkflowRunId: params.workflowParentRunId,
2537
+ workflowKey: params.workflowKey,
2538
+ }));
2335
2539
  }
2336
2540
 
2337
2541
  return null;
@@ -2364,8 +2568,8 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2364
2568
  const chainSkills = normalized === false ? [] : (normalized ?? []);
2365
2569
  const chain = wrapChainTasksForFork(params.chain as ChainStep[], contextPolicy);
2366
2570
  const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
2367
- const chainCtx = normalizeParentModel(ctx.model) || !data.parentModel ? ctx : { ...ctx, model: data.parentModel };
2368
- const chainResult = await executeChain({
2571
+ const chainCtx = normalizeParentModel(ctx.model) || !data.parentModel ? ctx : ({ ...ctx, model: data.parentModel } as ExtensionContext);
2572
+ const chainResult = await executeChain(compactOptional<Parameters<typeof executeChain>[0]>({
2369
2573
  chain,
2370
2574
  task: params.task,
2371
2575
  agents,
@@ -2394,7 +2598,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2394
2598
  foregroundControl,
2395
2599
  nestedRoute: foregroundControl?.nestedRoute,
2396
2600
  chainSkills,
2397
- chainDir: params.chainDir ?? getProjectChainRunsDir(effectiveCwd),
2601
+ chainDir: params.chainDir ?? getChainRunsDir(effectiveCwd, artifactConfig.dir),
2398
2602
  dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
2399
2603
  maxSubagentDepth: currentMaxSubagentDepth,
2400
2604
  worktreeSetupHook: deps.config.worktreeSetupHook,
@@ -2416,9 +2620,10 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2416
2620
  toolBudget: data.toolBudget,
2417
2621
  usageBudget: data.usageBudget,
2418
2622
  configToolBudget: data.configToolBudget,
2623
+ permissions: deps.config.permissions,
2419
2624
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2420
2625
  capabilityCeiling: data.capabilityCeiling,
2421
- });
2626
+ }));
2422
2627
 
2423
2628
  if (chainResult.requestedAsync) {
2424
2629
  if (!isAsyncAvailable()) {
@@ -2430,20 +2635,21 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2430
2635
  }
2431
2636
  const id = randomUUID();
2432
2637
  const parentModel = data.parentModel;
2433
- const asyncCtx = {
2638
+ const asyncCtx = compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
2434
2639
  pi: deps.pi,
2435
2640
  cwd: ctx.cwd,
2436
- currentSessionId: deps.state.currentSessionId!,
2437
- parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
2641
+ currentSessionId: data.parentSessionId!,
2642
+ parentSessionId: data.parentPiSessionId,
2438
2643
  currentModelProvider: parentModel?.provider,
2439
2644
  currentModel: parentModel,
2440
2645
  modelScope: data.modelScope,
2441
2646
  interactive: ctx.hasUI,
2442
- };
2647
+ permissions: deps.config.permissions,
2648
+ });
2443
2649
  const rawAsyncChain = chainResult.requestedAsync.chain;
2444
2650
  const asyncChain = wrapChainTasksForFork(rawAsyncChain, contextPolicy);
2445
2651
  const firstAgent = firstChainAgent(rawAsyncChain);
2446
- return executeAsyncChain(id, {
2652
+ return executeAsyncChain(id, compactOptional<Parameters<typeof executeAsyncChain>[1]>({
2447
2653
  chain: asyncChain,
2448
2654
  task: params.task,
2449
2655
  goal: resolveAsyncEventGoal(params.task, rawAsyncChain, firstAgent ? shouldForkAgent(contextPolicy, firstAgent) : false),
@@ -2478,15 +2684,17 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
2478
2684
  configToolBudget: data.configToolBudget,
2479
2685
  capabilityCeiling: data.capabilityCeiling,
2480
2686
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2481
- });
2687
+ }));
2482
2688
  }
2483
2689
 
2484
- const rawChainDetails = chainResult.details ? { ...chainResult.details, runId, timeoutMs: data.timeoutMs } : undefined;
2690
+ const rawChainDetails = chainResult.details ? compactOptional<Details>({ ...chainResult.details, runId, timeoutMs: data.timeoutMs }) : undefined;
2485
2691
  if (foregroundControl && rawChainDetails) {
2486
2692
  updateForegroundNestedProjection(foregroundControl);
2487
2693
  attachRootChildrenToSteps(runId, rawChainDetails.results, foregroundControl.nestedChildren);
2488
2694
  rawChainDetails.totalCost = sumResultsCost(rawChainDetails.results);
2489
- rawChainDetails.usageBudget = usageBudgetState(data.usageBudget, rawChainDetails.totalCost);
2695
+ const usageBudget = usageBudgetState(data.usageBudget, rawChainDetails.totalCost);
2696
+ if (usageBudget === undefined) delete rawChainDetails.usageBudget;
2697
+ else rawChainDetails.usageBudget = usageBudget;
2490
2698
  }
2491
2699
  const chainDetails = rawChainDetails ? compactForegroundDetails(rawChainDetails) : undefined;
2492
2700
  if (chainDetails) rememberForegroundRun(deps.state, { runId, mode: "chain", cwd: effectiveCwd, sessionId: data.parentSessionId, results: chainDetails.results, checkpoint: chainDetails.checkpoint });
@@ -2560,6 +2768,7 @@ interface ForegroundParallelRunInput {
2560
2768
  usageBudget?: UsageBudgetConfig;
2561
2769
  toolBudgets: (ResolvedToolBudget | undefined)[];
2562
2770
  agentContract?: AgentContract;
2771
+ permissions?: ExtensionConfig["permissions"];
2563
2772
  }
2564
2773
  function buildParallelModeError(message: string): AgentToolResult<Details> {
2565
2774
  return {
@@ -2581,13 +2790,13 @@ function createParallelWorktreeSetup(
2581
2790
  if (!enabled) return {};
2582
2791
  try {
2583
2792
  return {
2584
- setup: createWorktrees(cwd, runId, tasks.length, {
2793
+ setup: createWorktrees(cwd, runId, tasks.length, omitUndefinedProperties({
2585
2794
  agents: tasks.map((task) => task.agent),
2586
2795
  setupHook: setupHook
2587
- ? { hookPath: setupHook, timeoutMs: setupHookTimeoutMs }
2796
+ ? { hookPath: setupHook, ...(setupHookTimeoutMs === undefined ? {} : { timeoutMs: setupHookTimeoutMs }) }
2588
2797
  : undefined,
2589
2798
  baseDir,
2590
- }),
2799
+ })),
2591
2800
  };
2592
2801
  } catch (error) {
2593
2802
  const message = error instanceof Error ? error.message : String(error);
@@ -2643,39 +2852,41 @@ function finalizeParallelWorktreeHandoff(input: {
2643
2852
  }): { suffix: string; reference?: NonNullable<Details["parallelHandoff"]> } {
2644
2853
  const diffsDir = path.join(input.artifactsDir, "worktree-diffs", input.runId);
2645
2854
  const diffs = diffWorktrees(input.worktreeSetup, input.tasks.map((task) => task.agent), diffsDir);
2646
- const cleanup = cleanupWorktrees(input.worktreeSetup);
2647
2855
  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 } : {}),
2856
+ const manifestPath = parallelHandoffPath(input.artifactsDir, input.runId);
2857
+ const handoff = {
2858
+ manifestPath,
2859
+ runId: input.runId,
2860
+ mode: "parallel" as const,
2861
+ source: "foreground" as const,
2862
+ cwd: input.cwd,
2863
+ stepIndex: 0,
2864
+ flatStartIndex: 0,
2865
+ setup: input.worktreeSetup,
2866
+ diffs,
2867
+ results: input.results.map((result) => ({
2868
+ agent: result.agent,
2869
+ status: resolveSubagentResultStatus(omitUndefinedProperties({
2870
+ exitCode: result.exitCode,
2871
+ interrupted: result.interrupted,
2872
+ detached: result.detached,
2873
+ state: result.stopped ? "stopped" : undefined,
2874
+ processSignal: result.processSignal,
2875
+ timedOut: result.timedOut,
2876
+ stopped: result.stopped,
2877
+ turnBudgetExceeded: result.turnBudgetExceeded,
2677
2878
  })),
2678
- });
2879
+ summary: resultSummaryForIntercom(result),
2880
+ ...(result.artifactPaths?.outputPath ? { outputPath: result.artifactPaths.outputPath } : {}),
2881
+ ...(result.structuredOutput !== undefined ? { structuredOutput: result.structuredOutput } : {}),
2882
+ ...(result.structuredOutputPath ? { structuredOutputPath: result.structuredOutputPath } : {}),
2883
+ ...(result.sessionFile ? { sessionPath: result.sessionFile } : {}),
2884
+ })),
2885
+ };
2886
+ try {
2887
+ writeParallelHandoffGroup(handoff);
2888
+ const cleanup = cleanupWorktrees(input.worktreeSetup, { kind: "preserve", capturedDiffs: diffs, handoffManifestPath: manifestPath });
2889
+ const reference = writeParallelHandoffGroup({ ...handoff, cleanup });
2679
2890
  return {
2680
2891
  suffix: [diffSummary, formatParallelHandoffReference(reference)].filter(Boolean).join("\n\n"),
2681
2892
  reference,
@@ -2755,7 +2966,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
2755
2966
  beginForegroundChild(input.foregroundControl, {
2756
2967
  index,
2757
2968
  agent: task.agent,
2758
- description: input.taskDescriptions[index],
2969
+ ...(input.taskDescriptions[index] === undefined ? {} : { description: input.taskDescriptions[index] }),
2759
2970
  ...(model ? { model } : {}),
2760
2971
  ...(thinking ? { thinking } : {}),
2761
2972
  interrupt: () => {
@@ -2769,7 +2980,8 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
2769
2980
  ? createStructuredOutputRuntime(task.outputSchema, path.join(input.artifactsDir, "structured-output", input.runId))
2770
2981
  : undefined;
2771
2982
  let detachedReceipt = false;
2772
- const result = await runSync(input.ctx.cwd, input.agents, task.agent, taskText, {
2983
+ const result = await runSync(input.ctx.cwd, input.agents, task.agent, taskText, compactOptional<Parameters<typeof runSync>[4]>({
2984
+ permissions: input.permissions,
2773
2985
  parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
2774
2986
  context: input.contextPolicy.contextForAgent(task.agent),
2775
2987
  cwd: taskCwd,
@@ -2833,17 +3045,17 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
2833
3045
  const mergedProgress = input.liveProgress.filter((progress): progress is AgentProgress => progress !== undefined);
2834
3046
  input.onUpdate?.({
2835
3047
  content: progressUpdate.content,
2836
- details: {
3048
+ details: compactOptional<Details>({
2837
3049
  mode: "parallel",
2838
3050
  results: mergedResults,
2839
3051
  progress: mergedProgress,
2840
3052
  controlEvents: progressUpdate.details?.controlEvents,
2841
3053
  totalSteps: input.tasks.length,
2842
- },
3054
+ }),
2843
3055
  });
2844
3056
  }
2845
3057
  : undefined,
2846
- }).then((result) => {
3058
+ })).then((result) => {
2847
3059
  detachedReceipt = result.detached === true;
2848
3060
  return result;
2849
3061
  }).finally(() => {
@@ -2918,7 +3130,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2918
3130
  );
2919
3131
  const toolBudgets: (ResolvedToolBudget | undefined)[] = [];
2920
3132
  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 });
3133
+ const resolved = resolveEffectiveToolBudget(omitUndefinedProperties({ stepBudget: tasks[index]?.toolBudget, runBudget: data.toolBudget, agentBudget: agentConfigs[index]?.toolBudget, configBudget: data.configToolBudget }));
2922
3134
  if (resolved.error) return buildParallelModeError(resolved.error);
2923
3135
  toolBudgets.push(resolved.toolBudget);
2924
3136
  }
@@ -2944,7 +3156,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2944
3156
  ...(task.model !== undefined ? { model: task.model } : {}),
2945
3157
  }));
2946
3158
  const modelOverrides: (string | undefined)[] = tasks.map((_, i) =>
2947
- resolveEffectiveSubagentModel(behaviorOverrides[i]?.model, agentConfigs[i]?.model, parentModel, availableModels, currentProvider, { scope: data.modelScope }),
3159
+ resolveEffectiveSubagentModel(behaviorOverrides[i]?.model, agentConfigs[i]?.model, parentModel, availableModels, currentProvider, data.modelScope === undefined ? {} : { scope: data.modelScope }),
2948
3160
  );
2949
3161
 
2950
3162
  if (params.clarify === true && ctx.hasUI) {
@@ -2979,7 +3191,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
2979
3191
  for (let i = 0; i < result.behaviorOverrides.length; i++) {
2980
3192
  const override = result.behaviorOverrides[i];
2981
3193
  if (override?.model !== undefined) {
2982
- modelOverrides[i] = resolveEffectiveSubagentModel(override.model, agentConfigs[i]?.model, parentModel, availableModels, currentProvider, { scope: data.modelScope });
3194
+ modelOverrides[i] = resolveEffectiveSubagentModel(override.model, agentConfigs[i]?.model, parentModel, availableModels, currentProvider, data.modelScope === undefined ? {} : { scope: data.modelScope });
2983
3195
  behaviorOverrides[i]!.model = override.model;
2984
3196
  }
2985
3197
  if (override?.output !== undefined) behaviorOverrides[i]!.output = override.output;
@@ -3000,20 +3212,21 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3000
3212
  };
3001
3213
  }
3002
3214
  const id = randomUUID();
3003
- const asyncCtx = {
3215
+ const asyncCtx = compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
3004
3216
  pi: deps.pi,
3005
3217
  cwd: ctx.cwd,
3006
- currentSessionId: deps.state.currentSessionId!,
3007
- parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
3218
+ currentSessionId: data.parentSessionId!,
3219
+ parentSessionId: data.parentPiSessionId,
3008
3220
  currentModelProvider: parentModel?.provider,
3009
3221
  currentModel: parentModel,
3010
3222
  modelScope: data.modelScope,
3011
3223
  interactive: ctx.hasUI,
3012
- };
3224
+ permissions: deps.config.permissions,
3225
+ });
3013
3226
  const parallelTasks = tasks.map((t, i) => {
3014
3227
  const taskText = shouldForkAgent(contextPolicy, t.agent) ? wrapForkTask(taskTexts[i]!) : taskTexts[i]!;
3015
3228
  const progress = taskDisallowsFileUpdates(taskText) ? false : behaviorOverrides[i]?.progress;
3016
- return {
3229
+ return compactOptional<ParallelTaskItem>({
3017
3230
  agent: t.agent,
3018
3231
  task: taskText,
3019
3232
  cwd: t.cwd,
@@ -3027,10 +3240,10 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3027
3240
  ...(t.outputSchema !== undefined ? { outputSchema: t.outputSchema } : {}),
3028
3241
  ...(t.acceptance !== undefined ? { acceptance: t.acceptance } : {}),
3029
3242
  ...(t.agentContract !== undefined ? { agentContract: t.agentContract } : {}),
3030
- };
3243
+ });
3031
3244
  });
3032
- return executeAsyncChain(id, {
3033
- chain: [{ parallel: parallelTasks, concurrency: parallelConcurrency, worktree: params.worktree }],
3245
+ return executeAsyncChain(id, compactOptional<Parameters<typeof executeAsyncChain>[1]>({
3246
+ chain: [compactOptional<ParallelStep>({ parallel: parallelTasks, concurrency: parallelConcurrency, worktree: params.worktree })],
3034
3247
  resultMode: "parallel",
3035
3248
  goal: taskTexts[0] ?? "",
3036
3249
  agents,
@@ -3062,7 +3275,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3062
3275
  toolBudget: data.toolBudget,
3063
3276
  configToolBudget: data.configToolBudget,
3064
3277
  globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
3065
- });
3278
+ }));
3066
3279
  }
3067
3280
  }
3068
3281
 
@@ -3090,15 +3303,27 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3090
3303
 
3091
3304
  let worktreeFinalized = false;
3092
3305
  try {
3306
+ if (worktreeSetup) {
3307
+ writePendingParallelHandoff({
3308
+ manifestPath: parallelHandoffPath(artifactsDir, runId),
3309
+ runId,
3310
+ mode: "parallel",
3311
+ source: "foreground",
3312
+ cwd: effectiveCwd,
3313
+ stepIndex: 0,
3314
+ flatStartIndex: 0,
3315
+ setup: worktreeSetup,
3316
+ });
3317
+ }
3093
3318
  const outputBaseDir = path.join(artifactsDir, "outputs", runId);
3094
- const duplicateOutputError = findDuplicateParallelOutputPath({
3319
+ const duplicateOutputError = findDuplicateParallelOutputPath(omitUndefinedProperties({
3095
3320
  tasks,
3096
3321
  behaviors,
3097
3322
  paramsCwd: effectiveCwd,
3098
3323
  ctxCwd: ctx.cwd,
3099
3324
  outputBaseDir,
3100
3325
  worktreeSetup,
3101
- });
3326
+ }));
3102
3327
  if (duplicateOutputError) return buildParallelModeError(duplicateOutputError);
3103
3328
  for (let index = 0; index < tasks.length; index++) {
3104
3329
  const taskCwd = resolveParallelTaskCwd(tasks[index]!, effectiveCwd, worktreeSetup, index);
@@ -3117,8 +3342,9 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3117
3342
  }
3118
3343
 
3119
3344
  const deadlineAt = data.deadlineAt ?? (data.timeoutMs !== undefined ? Date.now() + data.timeoutMs : undefined);
3120
- const results = await runForegroundParallelTasks({
3345
+ const results = await runForegroundParallelTasks(compactOptional<ForegroundParallelRunInput>({
3121
3346
  tasks,
3347
+ permissions: deps.config.permissions,
3122
3348
  taskTexts,
3123
3349
  taskDescriptions,
3124
3350
  agents,
@@ -3166,7 +3392,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3166
3392
  usageBudget: data.usageBudget,
3167
3393
  toolBudgets,
3168
3394
  agentContract: params.agentContract,
3169
- });
3395
+ }));
3170
3396
  for (let i = 0; i < results.length; i++) {
3171
3397
  const run = results[i]!;
3172
3398
  recordRun(run.agent, taskTexts[i]!, run.exitCode, run.progressSummary?.durationMs ?? 0);
@@ -3188,7 +3414,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3188
3414
  }
3189
3415
  const interrupted = results.find((result) => result.interrupted);
3190
3416
  const totalCost = sumResultsCost(results);
3191
- const details = compactForegroundDetails({
3417
+ const details = compactForegroundDetails(compactOptional<Details>({
3192
3418
  mode: "parallel",
3193
3419
  runId,
3194
3420
  timeoutMs: data.timeoutMs,
@@ -3199,7 +3425,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3199
3425
  totalCost,
3200
3426
  usageBudget: usageBudgetState(data.usageBudget, totalCost),
3201
3427
  ...(handoff?.reference ? { parallelHandoff: handoff.reference } : {}),
3202
- });
3428
+ }));
3203
3429
  rememberForegroundRun(deps.state, { runId, mode: "parallel", cwd: effectiveCwd, sessionId: data.parentSessionId, results: details.results });
3204
3430
  if (interrupted) {
3205
3431
  return {
@@ -3216,20 +3442,24 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3216
3442
  };
3217
3443
  }
3218
3444
 
3445
+ const suppressRoutineResultIntercom = shouldSuppressRoutineResultIntercom({ suppressRoutineResultIntercom: params.suppressRoutineResultIntercom, results });
3219
3446
  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
- };
3447
+ if (!suppressRoutineResultIntercom) {
3448
+ const intercomReceipt = await maybeBuildForegroundIntercomReceipt({
3449
+ pi: deps.pi,
3450
+ intercomBridge: data.intercomBridge,
3451
+ runId,
3452
+ mode: "parallel",
3453
+ details,
3454
+ ...(params.workflowParentRunId !== undefined ? { preserveDetailsOutputs: true } : {}),
3455
+ ...(foregroundControl?.nestedChildren?.length ? { nestedChildren: foregroundControl.nestedChildren } : {}),
3456
+ });
3457
+ if (intercomReceipt) {
3458
+ return {
3459
+ content: [{ type: "text", text: intercomReceipt.text }],
3460
+ details: intercomReceipt.details,
3461
+ };
3462
+ }
3233
3463
  }
3234
3464
 
3235
3465
  const worktreeSuffix = handoff?.suffix ?? "";
@@ -3240,8 +3470,8 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3240
3470
  agent: result.agent,
3241
3471
  output: result.truncation?.text || getSingleResultOutput(result),
3242
3472
  exitCode: result.exitCode,
3243
- error: result.error,
3244
- timedOut: result.timedOut,
3473
+ ...(result.error === undefined ? {} : { error: result.error }),
3474
+ ...(result.timedOut === undefined ? {} : { timedOut: result.timedOut }),
3245
3475
  })),
3246
3476
  (i, agent) => `=== Task ${i + 1}: ${agent} ===`,
3247
3477
  );
@@ -3291,7 +3521,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3291
3521
  details: { mode: "single", results: [] },
3292
3522
  };
3293
3523
  }
3294
- const effectiveToolBudget = resolveEffectiveToolBudget({ runBudget: data.toolBudget, agentBudget: agentConfig.toolBudget, configBudget: data.configToolBudget });
3524
+ const effectiveToolBudget = resolveEffectiveToolBudget(omitUndefinedProperties({ runBudget: data.toolBudget, agentBudget: agentConfig.toolBudget, configBudget: data.configToolBudget }));
3295
3525
  if (effectiveToolBudget.error) return toExecutionErrorResult(params, new Error(effectiveToolBudget.error), data.contextPolicy.contextSummary);
3296
3526
 
3297
3527
  const parentModel = data.parentModel;
@@ -3304,7 +3534,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3304
3534
  parentModel,
3305
3535
  availableModels,
3306
3536
  currentProvider,
3307
- { scope: data.modelScope },
3537
+ data.modelScope === undefined ? {} : { scope: data.modelScope },
3308
3538
  );
3309
3539
  let skillOverride: string[] | false | undefined = normalizeSkillInput(params.skill);
3310
3540
  const rawOutput = params.output !== undefined ? params.output : agentConfig.output;
@@ -3314,7 +3544,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3314
3544
  const maxSubagentDepth = resolveChildMaxSubagentDepth(currentMaxSubagentDepth, agentConfig.maxSubagentDepth);
3315
3545
 
3316
3546
  if (params.clarify === true && ctx.hasUI) {
3317
- const behavior = resolveStepBehavior(agentConfig, { output: effectiveOutput, skills: skillOverride });
3547
+ const behavior = resolveStepBehavior(agentConfig, omitUndefinedProperties({ output: effectiveOutput, skills: skillOverride }));
3318
3548
  const availableSkills = discoverAvailableSkills(effectiveCwd);
3319
3549
 
3320
3550
  const result = await ctx.ui.custom<ChainClarifyResult>(
@@ -3341,7 +3571,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3341
3571
 
3342
3572
  task = result.templates[0]!;
3343
3573
  const override = result.behaviorOverrides[0];
3344
- if (override?.model !== undefined) modelOverride = resolveEffectiveSubagentModel(override.model, agentConfig.model, parentModel, availableModels, currentProvider, { scope: data.modelScope });
3574
+ if (override?.model !== undefined) modelOverride = resolveEffectiveSubagentModel(override.model, agentConfig.model, parentModel, availableModels, currentProvider, data.modelScope === undefined ? {} : { scope: data.modelScope });
3345
3575
  if (override?.output !== undefined) effectiveOutput = normalizeSingleOutputOverride(override.output, agentConfig.output);
3346
3576
  if (override?.skills !== undefined) skillOverride = override.skills;
3347
3577
 
@@ -3354,17 +3584,18 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3354
3584
  };
3355
3585
  }
3356
3586
  const id = randomUUID();
3357
- const asyncCtx = {
3587
+ const asyncCtx = compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
3358
3588
  pi: deps.pi,
3359
3589
  cwd: ctx.cwd,
3360
- currentSessionId: deps.state.currentSessionId!,
3361
- parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
3590
+ currentSessionId: data.parentSessionId!,
3591
+ parentSessionId: data.parentPiSessionId,
3362
3592
  currentModelProvider: parentModel?.provider,
3363
3593
  currentModel: parentModel,
3364
3594
  modelScope: data.modelScope,
3365
3595
  interactive: ctx.hasUI,
3366
- };
3367
- return executeAsyncSingle(id, {
3596
+ permissions: deps.config.permissions,
3597
+ });
3598
+ return executeAsyncSingle(id, compactOptional<Parameters<typeof executeAsyncSingle>[1]>({
3368
3599
  agent: params.agent!,
3369
3600
  task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(task) : task,
3370
3601
  goal: task,
@@ -3402,7 +3633,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3402
3633
  toolBudget: effectiveToolBudget.toolBudget,
3403
3634
  usageBudget: data.usageBudget,
3404
3635
  allowZeroToolBudget: data.allowZeroToolBudget && effectiveToolBudget.toolBudget === data.toolBudget,
3405
- });
3636
+ }));
3406
3637
  }
3407
3638
  }
3408
3639
 
@@ -3431,7 +3662,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3431
3662
  const foregroundControl = deps.state.foregroundControls.get(runId);
3432
3663
  if (foregroundControl) {
3433
3664
  const thinking = resolveEffectiveThinking(modelOverride, thinkingOverrideForTask(params.agent!, 0, modelOverride));
3434
- beginForegroundChild(foregroundControl, {
3665
+ beginForegroundChild(foregroundControl, omitUndefinedProperties({
3435
3666
  index: 0,
3436
3667
  agent: params.agent!,
3437
3668
  description: foregroundControl.description,
@@ -3443,7 +3674,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3443
3674
  return true;
3444
3675
  },
3445
3676
  detach: () => detachForeground?.("user request") === true,
3446
- });
3677
+ }));
3447
3678
  }
3448
3679
 
3449
3680
  const forwardSingleUpdate = onUpdate
@@ -3454,9 +3685,10 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3454
3685
  : undefined;
3455
3686
 
3456
3687
  const deadlineAt = data.deadlineAt ?? (data.timeoutMs !== undefined ? Date.now() + data.timeoutMs : undefined);
3457
- let r: Awaited<ReturnType<typeof runSync>>;
3688
+ let r: Awaited<ReturnType<typeof runSync>> | undefined;
3458
3689
  try {
3459
- r = await runSync(ctx.cwd, agents, params.agent!, task, {
3690
+ r = await runSync(ctx.cwd, agents, params.agent!, task, compactOptional<Parameters<typeof runSync>[4]>({
3691
+ permissions: deps.config.permissions,
3460
3692
  parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
3461
3693
  context: data.contextPolicy.contextForAgent(params.agent!),
3462
3694
  cwd: effectiveCwd,
@@ -3498,7 +3730,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3498
3730
  onDetachedExit: (result) => {
3499
3731
  try {
3500
3732
  try {
3501
- updateRememberedForegroundChild(deps.state, { runId, mode: "single", cwd: effectiveCwd, sessionId: data.parentSessionId, index: 0, result, events: deps.pi.events });
3733
+ updateRememberedForegroundChild(deps.state, { runId, mode: "single", cwd: effectiveCwd, sessionId: data.parentSessionId, index: 0, result, events: deps.pi.events, notify: params.workflowParentRunId === undefined });
3502
3734
  } catch {
3503
3735
  // Remembered foreground state is best-effort; run history and cleanup must still complete.
3504
3736
  }
@@ -3522,7 +3754,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3522
3754
  toolBudget: effectiveToolBudget.toolBudget,
3523
3755
  capabilityCeiling: data.capabilityCeiling,
3524
3756
  allowZeroToolBudget: data.allowZeroToolBudget && effectiveToolBudget.toolBudget === data.toolBudget,
3525
- });
3757
+ }));
3526
3758
  } finally {
3527
3759
  // An attached runSync rejection still owns its child and structured runtime.
3528
3760
  // A successful detached receipt transfers both to onDetachedExit while the
@@ -3540,7 +3772,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3540
3772
  if (r.artifactPaths) allArtifactPaths.push(r.artifactPaths);
3541
3773
 
3542
3774
  const fullOutput = getSingleResultOutput(r);
3543
- const finalizedOutput = finalizeSingleOutput({
3775
+ const finalizedOutput = finalizeSingleOutput(omitUndefinedProperties({
3544
3776
  fullOutput,
3545
3777
  truncatedOutput: r.truncation?.text,
3546
3778
  outputPath,
@@ -3549,13 +3781,13 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3549
3781
  savedPath: r.savedOutputPath,
3550
3782
  outputReference: r.outputReference,
3551
3783
  saveError: r.outputSaveError,
3552
- });
3784
+ }));
3553
3785
  if (foregroundControl) {
3554
3786
  updateForegroundNestedProjection(foregroundControl);
3555
3787
  attachRootChildrenToSteps(runId, [r], foregroundControl.nestedChildren);
3556
3788
  }
3557
3789
  const totalCost = sumResultsCost([r]);
3558
- const details = compactForegroundDetails({
3790
+ const details = compactForegroundDetails(compactOptional<Details>({
3559
3791
  mode: "single",
3560
3792
  runId,
3561
3793
  timeoutMs: data.timeoutMs,
@@ -3568,10 +3800,11 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3568
3800
  totalChildUsage: sumResultsUsage([r]),
3569
3801
  totalCost,
3570
3802
  usageBudget: usageBudgetState(data.usageBudget, totalCost),
3571
- });
3803
+ }));
3572
3804
  rememberForegroundRun(deps.state, { runId, mode: "single", cwd: effectiveCwd, sessionId: data.parentSessionId, results: details.results });
3573
3805
 
3574
- if (!r.detached && !r.interrupted) {
3806
+ const suppressRoutineResultIntercom = shouldSuppressRoutineResultIntercom({ suppressRoutineResultIntercom: params.suppressRoutineResultIntercom, results: [r] });
3807
+ if (!r.detached && !r.interrupted && !suppressRoutineResultIntercom) {
3575
3808
  if (foregroundControl) updateForegroundNestedProjection(foregroundControl);
3576
3809
  const intercomReceipt = await maybeBuildForegroundIntercomReceipt({
3577
3810
  pi: deps.pi,
@@ -3579,6 +3812,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3579
3812
  runId,
3580
3813
  mode: "single",
3581
3814
  details,
3815
+ ...(params.workflowParentRunId !== undefined ? { preserveDetailsOutputs: true } : {}),
3582
3816
  ...(foregroundControl?.nestedChildren?.length ? { nestedChildren: foregroundControl.nestedChildren } : {}),
3583
3817
  });
3584
3818
  if (intercomReceipt) {
@@ -3616,7 +3850,8 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3616
3850
  };
3617
3851
  }
3618
3852
 
3619
- function inferExecutionMode(params: SubagentParamsLike): SubagentRunMode {
3853
+ function inferExecutionMode(params: SubagentParamsLike): Details["mode"] {
3854
+ if (params.workflowScript !== undefined) return "workflow";
3620
3855
  if ((params.chain?.length ?? 0) > 0) return "chain";
3621
3856
  if ((params.tasks?.length ?? 0) > 0) return "parallel";
3622
3857
  return "single";
@@ -3633,6 +3868,87 @@ function duplicateSubagentCallResult(params: SubagentParamsLike): AgentToolResul
3633
3868
  };
3634
3869
  }
3635
3870
 
3871
+ function workflowChildResult(key: string, result: AgentToolResult<Details>): WorkflowScriptChildResult {
3872
+ const receiptOutput = result.content.map((part) => part.type === "text" ? part.text : "").filter(Boolean).join("\n");
3873
+ const output = result.details.results.length === 1 && result.details.results[0]?.finalOutput !== undefined
3874
+ ? result.details.results[0].finalOutput
3875
+ : receiptOutput;
3876
+ const artifactPaths = new Set<string>();
3877
+ if (result.details.asyncDir) artifactPaths.add(result.details.asyncDir);
3878
+ if (result.details.parallelHandoff?.path) artifactPaths.add(result.details.parallelHandoff.path);
3879
+ for (const child of result.details.results) {
3880
+ if (child.savedOutputPath) artifactPaths.add(child.savedOutputPath);
3881
+ if (child.outputReference?.path) artifactPaths.add(child.outputReference.path);
3882
+ if (child.sessionFile) artifactPaths.add(child.sessionFile);
3883
+ }
3884
+ const structured = result.details.results.map((child) => child.structuredOutput).filter((value) => value !== undefined);
3885
+ return {
3886
+ key,
3887
+ ok: result.isError !== true,
3888
+ ...(result.details.runId || result.details.asyncId ? { runId: result.details.runId ?? result.details.asyncId } : {}),
3889
+ output,
3890
+ ...(result.isError === true ? { error: receiptOutput || output || "Child run failed." } : {}),
3891
+ ...(structured.length === 1 ? { structuredOutput: structured[0] } : structured.length > 1 ? { structuredOutput: structured } : {}),
3892
+ artifactPaths: [...artifactPaths],
3893
+ results: result.details.results,
3894
+ };
3895
+ }
3896
+
3897
+ function prepareWorkflowChildParams(params: SubagentParamsLike): SubagentParamsLike {
3898
+ if (params.worktree !== true || !params.agent) return params;
3899
+ const {
3900
+ agent,
3901
+ task = "",
3902
+ model,
3903
+ skill,
3904
+ output,
3905
+ outputMode,
3906
+ outputSchema,
3907
+ acceptance,
3908
+ agentContract,
3909
+ toolBudget,
3910
+ ...runParams
3911
+ } = params;
3912
+ return {
3913
+ ...runParams,
3914
+ worktree: true,
3915
+ tasks: [{
3916
+ agent,
3917
+ task,
3918
+ ...(model !== undefined ? { model } : {}),
3919
+ ...(skill !== undefined ? { skill } : {}),
3920
+ ...(output !== undefined ? { output } : {}),
3921
+ ...(outputMode !== undefined ? { outputMode } : {}),
3922
+ ...(outputSchema !== undefined ? { outputSchema } : {}),
3923
+ ...(acceptance !== undefined ? { acceptance } : {}),
3924
+ ...(agentContract !== undefined ? { agentContract } : {}),
3925
+ ...(toolBudget !== undefined ? { toolBudget } : {}),
3926
+ }],
3927
+ };
3928
+ }
3929
+
3930
+ function formatWorkflowValue(value: unknown): string {
3931
+ if (value === undefined) return "(undefined)";
3932
+ if (typeof value === "string") return value;
3933
+ try {
3934
+ return JSON.stringify(value, null, 2);
3935
+ } catch {
3936
+ return String(value);
3937
+ }
3938
+ }
3939
+
3940
+ function workflowChatProgressUpdate(
3941
+ runId: string,
3942
+ chatProgress: WorkflowChatProgressProjection,
3943
+ workflow: NonNullable<Details["workflow"]>,
3944
+ ): AgentToolResult<Details> | undefined {
3945
+ if (chatProgress.mode !== "live-card") return undefined;
3946
+ return {
3947
+ content: [{ type: "text", text: "Workflow running." }],
3948
+ details: { mode: "workflow", runId, results: [], workflow, chatProgress },
3949
+ };
3950
+ }
3951
+
3636
3952
  function omitExecutionModeActionAlias(params: SubagentParamsLike): SubagentParamsLike {
3637
3953
  const action = params.action?.toLowerCase();
3638
3954
  if (action === "single" && (params.agent !== undefined || params.task !== undefined)) {
@@ -3648,6 +3964,36 @@ function omitExecutionModeActionAlias(params: SubagentParamsLike): SubagentParam
3648
3964
  return params;
3649
3965
  }
3650
3966
 
3967
+ function createScheduledOwnerState(source: SubagentState, ownerSessionId: string, ctx: ExtensionContext): SubagentState {
3968
+ const ownerSpawns = source.subagentSpawns?.sessionId === ownerSessionId
3969
+ ? {
3970
+ ...source.subagentSpawns,
3971
+ grantHistory: [...(source.subagentSpawns.grantHistory ?? [])],
3972
+ }
3973
+ : undefined;
3974
+ return {
3975
+ ...source,
3976
+ baseCwd: ctx.cwd,
3977
+ currentSessionId: ownerSessionId,
3978
+ parentSessionFile: ctx.sessionManager.getSessionFile() ?? null,
3979
+ subagentInProgress: false,
3980
+ ...(ownerSpawns ? { subagentSpawns: ownerSpawns } : { subagentSpawns: undefined }),
3981
+ asyncJobs: new Map(),
3982
+ fleetJobs: new Map(),
3983
+ foregroundRuns: new Map(),
3984
+ foregroundControls: new Map(),
3985
+ lastForegroundControlId: null,
3986
+ cleanupTimers: new Map(),
3987
+ lastUiContext: null,
3988
+ poller: null,
3989
+ completionSeen: new Map(),
3990
+ watcher: null,
3991
+ watcherRestartTimer: null,
3992
+ waitSubscriptions: new Map(),
3993
+ workflowControllers: new Map(),
3994
+ };
3995
+ }
3996
+
3651
3997
  export function createSubagentExecutor(deps: ExecutorDeps): {
3652
3998
  execute: (
3653
3999
  id: string,
@@ -3668,34 +4014,320 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3668
4014
  onUpdate: ((r: AgentToolResult<Details>) => void) | undefined,
3669
4015
  ctx: ExtensionContext,
3670
4016
  ) => Promise<AgentToolResult<Details>>;
4017
+ /** Scheduled launches retain their owning context without replacing the live active session. */
4018
+ executeScheduled: (
4019
+ id: string,
4020
+ params: SubagentParamsLike,
4021
+ signal: AbortSignal,
4022
+ ctx: ExtensionContext,
4023
+ ) => Promise<AgentToolResult<Details>>;
3671
4024
  } {
3672
4025
  const delegatedThinkingOverrides = new WeakMap<object, AgentConfig["thinking"]>();
3673
4026
  const delegatedZeroToolBudgets = new WeakSet<object>();
4027
+ const warnedArtifactPackageDirs = new Set<string>();
4028
+ const scheduledOwnerExecutors = new Map<string, ReturnType<typeof createSubagentExecutor>>();
3674
4029
  const execute = async (
3675
4030
  _id: string,
3676
4031
  params: SubagentParamsLike,
3677
4032
  signal: AbortSignal,
3678
4033
  onUpdate: ((r: AgentToolResult<Details>) => void) | undefined,
3679
4034
  ctx: ExtensionContext,
4035
+ preserveActiveSession = false,
3680
4036
  ): Promise<AgentToolResult<Details>> => {
3681
4037
  const delegatedThinkingOverride = delegatedThinkingOverrides.get(params);
3682
4038
  const allowZeroToolBudget = delegatedZeroToolBudgets.has(params);
3683
- deps.state.baseCwd = ctx.cwd;
4039
+ if (!preserveActiveSession) deps.state.baseCwd = ctx.cwd;
3684
4040
  deps.state.foregroundRuns ??= new Map();
3685
4041
  deps.state.foregroundControls ??= new Map();
3686
4042
  deps.state.lastForegroundControlId ??= null;
3687
4043
  const requestParams = omitExecutionModeActionAlias(params);
3688
- const requestCwd = resolveRequestedCwd(ctx.cwd, requestParams.cwd);
3689
- const paramsWithResolvedCwd = requestParams.cwd === undefined ? requestParams : { ...requestParams, cwd: requestCwd };
4044
+ if (requestParams.workflowScript !== undefined) {
4045
+ const invalidMode = requestParams.action !== undefined || requestParams.agent !== undefined || requestParams.step !== undefined || requestParams.tasks !== undefined || requestParams.chain !== undefined;
4046
+ if (invalidMode) {
4047
+ 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: [] } };
4048
+ }
4049
+ if (requestParams.clarify === true) {
4050
+ return { content: [{ type: "text", text: "workflowScript does not support clarify UI." }], isError: true, details: { mode: "workflow", results: [] } };
4051
+ }
4052
+ const timeout = requestParams.timeoutMs ?? requestParams.maxRuntimeMs ?? DEFAULT_FOREGROUND_TIMEOUT_MS;
4053
+ const workflowUsageBudget = validateUsageBudgetConfig(requestParams.usageBudget ?? deps.config.usageBudget, requestParams.usageBudget ? "usageBudget" : "config.usageBudget");
4054
+ if (workflowUsageBudget.error) return buildRequestedModeError(requestParams, workflowUsageBudget.error);
4055
+ const workflowCwd = resolveRequestedCwd(ctx.cwd, requestParams.cwd);
4056
+ const chatProgressResult = resolveWorkflowChatProgress({ requested: requestParams.chatProgress, parentCwd: ctx.cwd, workflowCwd, background: requestParams.async !== false });
4057
+ if (chatProgressResult.error) return { content: [{ type: "text", text: chatProgressResult.error }], isError: true, details: { mode: "workflow", results: [] } };
4058
+ const chatProgress = chatProgressResult.projection!;
4059
+ if (requestParams.async !== false) {
4060
+ const workflowRunId = _id;
4061
+ const asyncDir = path.join(DIRS.async, workflowRunId);
4062
+ const resultPath = path.join(DIRS.results, `${workflowRunId}.json`);
4063
+ const statusPath = path.join(asyncDir, "status.json");
4064
+ const eventsPath = path.join(asyncDir, "events.jsonl");
4065
+ const startedAt = Date.now();
4066
+ const currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
4067
+ fs.mkdirSync(asyncDir, { recursive: true });
4068
+ fs.mkdirSync(DIRS.results, { recursive: true });
4069
+ const controller = new AbortController();
4070
+ deps.state.workflowControllers ??= new Map();
4071
+ deps.state.workflowControllers.set(workflowRunId, controller);
4072
+ let status: AsyncStatus = {
4073
+ runId: workflowRunId,
4074
+ sessionId: currentSessionId ?? undefined,
4075
+ mode: "workflow",
4076
+ state: "running",
4077
+ startedAt,
4078
+ lastUpdate: startedAt,
4079
+ deadlineAt: startedAt + timeout,
4080
+ timeoutMs: timeout,
4081
+ cwd: ctx.cwd,
4082
+ pid: process.pid,
4083
+ steps: [],
4084
+ workflow: { trace: [], emits: [], console: [] },
4085
+ };
4086
+ const appendWorkflowEvent = (event: Record<string, unknown>) => fs.appendFileSync(eventsPath, `${JSON.stringify({ ts: Date.now(), runId: workflowRunId, ...event })}\n`, "utf-8");
4087
+ const persist = () => {
4088
+ status.lastUpdate = Date.now();
4089
+ writeAtomicJson(statusPath, status);
4090
+ const job = deps.state.asyncJobs.get(workflowRunId);
4091
+ if (job) {
4092
+ job.status = status.state;
4093
+ job.updatedAt = status.lastUpdate;
4094
+ if (status.steps) {
4095
+ job.steps = status.steps.map((step, index) => ({ ...step, index }));
4096
+ job.agents = status.steps.map((step) => step.agent);
4097
+ } else {
4098
+ delete job.steps;
4099
+ delete job.agents;
4100
+ }
4101
+ job.workflow = status.workflow;
4102
+ }
4103
+ };
4104
+ 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 };
4105
+ deps.state.asyncJobs.set(workflowRunId, workflowJob);
4106
+ deps.state.fleetJobs ??= new Map();
4107
+ deps.state.fleetJobs.set(workflowRunId, workflowJob);
4108
+ persist();
4109
+ appendWorkflowEvent({ type: "subagent.workflow.started" });
4110
+ const { workflowScript, async: _workflowAsync, chatProgress: _chatProgress, ...workflowRequest } = requestParams;
4111
+ void Promise.resolve().then(async () => {
4112
+ const workflowResults: SingleResult[] = [];
4113
+ 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;
4114
+ const updateTrace = (trace: NonNullable<Details["workflow"]>["trace"]) => {
4115
+ status.workflow = { ...(status.workflow ?? { emits: [], console: [] }), trace };
4116
+ for (const entry of trace.filter((candidate) => candidate.operation === "run")) {
4117
+ const existing = status.steps?.find((step) => step.workflowKey === entry.key);
4118
+ if (entry.state === "reused" && existing) continue;
4119
+ const mapped = entry.state === "started" || entry.state === "reused" ? "running" : entry.state === "completed" ? "completed" : "failed";
4120
+ if (existing) {
4121
+ existing.status = mapped;
4122
+ if (entry.error === undefined) delete existing.error;
4123
+ else existing.error = entry.error;
4124
+ if (entry.durationMs === undefined) delete existing.durationMs;
4125
+ else existing.durationMs = entry.durationMs;
4126
+ } else {
4127
+ status.steps?.push({ agent: entry.key, label: entry.key, workflowKey: entry.key, parentWorkflowRunId: workflowRunId, status: mapped });
4128
+ }
4129
+ }
4130
+ persist();
4131
+ appendWorkflowEvent({ type: "subagent.workflow.trace", trace });
4132
+ };
4133
+ try {
4134
+ const workflow = await runWorkflowScript({
4135
+ script: workflowScript,
4136
+ timeoutMs: timeout,
4137
+ signal: controller.signal,
4138
+ onTrace: updateTrace,
4139
+ onEmit: (emits) => {
4140
+ // Each emit is validated at the host boundary in runWorkflowScript before onEmit fires.
4141
+ status.workflow = { ...(status.workflow ?? { trace: [], console: [] }), emits };
4142
+ persist();
4143
+ appendWorkflowEvent({ type: "subagent.workflow.emit", value: emits.at(-1) });
4144
+ },
4145
+ launch: async (key, childParams, workflowSignal) => {
4146
+ if (workflowUsageBudget.budget && childParams.async === true) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, "workflow usageBudget does not support async runs.run launches."));
4147
+ const budgetState = usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults));
4148
+ if (budgetState?.exhausted) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, usageBudgetExceededMessage(budgetState)));
4149
+ const childRequest = prepareWorkflowChildParams({ ...workflowChildDefaults, ...childParams, workflowParentRunId: workflowRunId, workflowKey: key } as SubagentParamsLike);
4150
+ const result = await execute(randomUUID(), childRequest, workflowSignal, undefined, ctx, preserveActiveSession);
4151
+ workflowResults.push(...result.details.results);
4152
+ const child = workflowChildResult(key, result);
4153
+ if (result.details.asyncId) {
4154
+ const childJob = deps.state.asyncJobs.get(result.details.asyncId);
4155
+ if (childJob) { childJob.parentWorkflowRunId = workflowRunId; childJob.workflowKey = key; }
4156
+ }
4157
+ return child;
4158
+ },
4159
+ status: async (keyOrRunId, workflowSignal) => workflowChildResult(keyOrRunId, await execute(randomUUID(), { action: "status", id: keyOrRunId }, workflowSignal, undefined, ctx, preserveActiveSession)),
4160
+ });
4161
+ const returnPreview = formatWorkflowValue(workflow.value).slice(0, 1_000);
4162
+ const emitPreview = workflow.emits.length > 0 ? ` Emitted: ${workflow.emits.map(formatWorkflowValue).join(", ").slice(0, 1_000)}` : "";
4163
+ const summary = `Workflow completed with ${workflow.children.length} child run(s). Return: ${returnPreview}${emitPreview} Trace: ${workflow.trace.length} event(s).`;
4164
+ const workflowUsage = sumResultsUsage(workflowResults);
4165
+ 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) };
4166
+ persist();
4167
+ appendWorkflowEvent({ type: "subagent.workflow.completed", state: "complete" });
4168
+ 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 });
4169
+ } catch (error) {
4170
+ const partial = error instanceof WorkflowScriptError ? error.partial : { trace: [], emits: [], console: [], children: [] };
4171
+ const stopped = controller.signal.aborted;
4172
+ 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 } });
4173
+ persist();
4174
+ appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, error: status.error });
4175
+ 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 });
4176
+ } finally {
4177
+ deps.state.workflowControllers?.delete(workflowRunId);
4178
+ }
4179
+ });
4180
+ return {
4181
+ content: [{ type: "text", text: formatAsyncStartedMessage(`Async workflow [${workflowRunId}]`, ctx.hasUI === true) }],
4182
+ details: { mode: "workflow", runId: workflowRunId, asyncId: workflowRunId, asyncDir, results: [], chatProgress },
4183
+ };
4184
+ }
4185
+ 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;
4186
+ const workflowResults: SingleResult[] = [];
4187
+ let liveWorkflow: NonNullable<Details["workflow"]> = { trace: [], emits: [], console: [] };
4188
+ const sendWorkflowProgress = () => {
4189
+ const update = workflowChatProgressUpdate(_id, chatProgress, liveWorkflow);
4190
+ if (update) onUpdate?.(update);
4191
+ };
4192
+ try {
4193
+ const workflow = await runWorkflowScript({
4194
+ script: requestParams.workflowScript,
4195
+ timeoutMs: timeout,
4196
+ signal,
4197
+ onTrace: (trace) => {
4198
+ liveWorkflow = { ...liveWorkflow, trace };
4199
+ sendWorkflowProgress();
4200
+ },
4201
+ onEmit: (emits) => {
4202
+ liveWorkflow = { ...liveWorkflow, emits };
4203
+ sendWorkflowProgress();
4204
+ },
4205
+ launch: async (key, childParams, workflowSignal) => {
4206
+ if (workflowUsageBudget.budget && childParams.async === true) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, "workflow usageBudget does not support async runs.run launches."));
4207
+ const budgetState = usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults));
4208
+ if (budgetState?.exhausted) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, usageBudgetExceededMessage(budgetState)));
4209
+ const childRequest = prepareWorkflowChildParams({ ...workflowChildDefaults, ...childParams, workflowParentRunId: _id, workflowKey: key, suppressRoutineResultIntercom: chatProgress.mode === "live-card" } as SubagentParamsLike);
4210
+ const result = await execute(randomUUID(), childRequest, workflowSignal, undefined, ctx, preserveActiveSession);
4211
+ workflowResults.push(...result.details.results);
4212
+ return workflowChildResult(key, result);
4213
+ },
4214
+ status: async (keyOrRunId, workflowSignal) => workflowChildResult(keyOrRunId, await execute(randomUUID(), { action: "status", id: keyOrRunId }, workflowSignal, undefined, ctx, preserveActiveSession)),
4215
+ });
4216
+ 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}` : ""}`);
4217
+ const sections = ["Workflow completed.", `Return:\n${formatWorkflowValue(workflow.value)}`];
4218
+ if (workflow.emits.length > 0) sections.push(`Emitted:\n${workflow.emits.map(formatWorkflowValue).join("\n")}`);
4219
+ if (workflow.console.length > 0) sections.push(`Console:\n${workflow.console.map((entry) => `[${entry.level}] ${entry.text}`).join("\n")}`);
4220
+ if (traceLines.length > 0) sections.push(`Call trace:\n${traceLines.join("\n")}`);
4221
+ return {
4222
+ content: [{ type: "text", text: sections.join("\n\n") }],
4223
+ 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 }),
4224
+ };
4225
+ } catch (error) {
4226
+ const partial = error instanceof WorkflowScriptError ? error.partial : { trace: [], emits: [], console: [], children: [] };
4227
+ const text = error instanceof Error ? error.message : String(error);
4228
+ const traceLines = partial.trace.map((entry) => `- ${entry.operation} ${entry.key}: ${entry.state}${entry.runId ? ` (${entry.runId})` : ""}${entry.error ? ` — ${entry.error}` : ""}`);
4229
+ const sections = [`Workflow failed: ${text}`];
4230
+ if (partial.emits.length > 0) sections.push(`Emitted:\n${partial.emits.map(formatWorkflowValue).join("\n")}`);
4231
+ if (partial.console.length > 0) sections.push(`Console:\n${partial.console.map((entry) => `[${entry.level}] ${entry.text}`).join("\n")}`);
4232
+ if (traceLines.length > 0) sections.push(`Call trace:\n${traceLines.join("\n")}`);
4233
+ return {
4234
+ content: [{ type: "text", text: sections.join("\n\n") }],
4235
+ isError: true,
4236
+ 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 }),
4237
+ };
4238
+ }
4239
+ }
4240
+ const directParams = prepareWorkflowChildParams(requestParams);
4241
+ const requestCwd = resolveRequestedCwd(ctx.cwd, directParams.cwd);
4242
+ const paramsWithResolvedCwd = directParams.cwd === undefined ? directParams : { ...directParams, cwd: requestCwd };
3690
4243
  const action = paramsWithResolvedCwd.action;
4244
+ let requestSessionId = "";
4245
+ let requestPiSessionId: string | undefined;
3691
4246
  let requestParentModel: ParentModel | undefined;
3692
4247
  try {
3693
- requestParentModel = rememberParentModel(deps.state, resolveCurrentSessionId(ctx.sessionManager), ctx.model);
4248
+ requestSessionId = resolveCurrentSessionId(ctx.sessionManager);
4249
+ requestPiSessionId = ctx.sessionManager.getSessionId() ?? undefined;
4250
+ requestParentModel = preserveActiveSession
4251
+ ? normalizeParentModel(ctx.model)
4252
+ : rememberParentModel(deps.state, requestSessionId, ctx.model);
3694
4253
  } catch (error) {
3695
4254
  if (action?.toLowerCase() !== "doctor") throw error;
3696
4255
  requestParentModel = normalizeParentModel(ctx.model);
3697
4256
  }
3698
4257
  if (action) {
4258
+ if (action === "worktree.discard") {
4259
+ if (deps.allowMutatingManagementActions === false) {
4260
+ return { content: [{ type: "text", text: "Action 'worktree.discard' is not available from child-safe subagent fanout mode." }], isError: true, details: { mode: "management", results: [] } };
4261
+ }
4262
+ if (!paramsWithResolvedCwd.handoffPath?.trim()) {
4263
+ return { content: [{ type: "text", text: "worktree.discard requires handoffPath from parallelHandoff.path or async status." }], isError: true, details: { mode: "management", results: [] } };
4264
+ }
4265
+ const decision = resolveAuthorityDecision({ action: "discardWorktree", ...(deps.config.authorityPolicy === undefined ? {} : { policy: deps.config.authorityPolicy }) });
4266
+ if (decision === "forbid") {
4267
+ return { content: [{ type: "text", text: "Authority policy forbids worktree discard." }], isError: true, details: { mode: "management", results: [] } };
4268
+ }
4269
+ let confirmed = decision === "auto";
4270
+ if (decision === "confirm") {
4271
+ 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: [] } };
4272
+ confirmed = await ctx.ui.confirm("Discard preserved subagent worktrees?", `This permanently removes preserved worktrees and temporary branches recorded in:\n${paramsWithResolvedCwd.handoffPath}`);
4273
+ }
4274
+ if (!confirmed) return { content: [{ type: "text", text: "Worktree discard canceled; preserved worktrees were not changed." }], details: { mode: "management", results: [] } };
4275
+ try {
4276
+ const discarded = discardPreservedWorktrees(
4277
+ path.isAbsolute(paramsWithResolvedCwd.handoffPath) ? paramsWithResolvedCwd.handoffPath : path.resolve(requestCwd, paramsWithResolvedCwd.handoffPath),
4278
+ { kind: decision === "confirm" ? "confirmed" : "policy", ...(deps.config.authorityPolicy ? { policy: deps.config.authorityPolicy } : {}) },
4279
+ );
4280
+ return { content: [{ type: "text", text: discarded.text }], details: { mode: "management", results: [] } };
4281
+ } catch (error) {
4282
+ return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, details: { mode: "management", results: [] } };
4283
+ }
4284
+ }
4285
+ if ((HERDR_PROJECT_PANE_ACTIONS as readonly string[]).includes(action)) {
4286
+ if (deps.allowMutatingManagementActions === false && MUTATING_MANAGEMENT_ACTIONS.has(action)) {
4287
+ return { content: [{ type: "text", text: `Action '${action}' is not available from child-safe subagent fanout mode.` }], isError: true, details: { mode: "management", results: [] } };
4288
+ }
4289
+ return handleHerdrProjectPaneAction(action as (typeof HERDR_PROJECT_PANE_ACTIONS)[number], paramsWithResolvedCwd, { cwd: requestCwd, signal });
4290
+ }
4291
+ if ((HERDR_INSPECTOR_ACTIONS as readonly string[]).includes(action)) {
4292
+ if (deps.allowMutatingManagementActions === false && MUTATING_MANAGEMENT_ACTIONS.has(action)) {
4293
+ return { content: [{ type: "text", text: `Action '${action}' is not available from child-safe subagent fanout mode.` }], isError: true, details: { mode: "management", results: [] } };
4294
+ }
4295
+ deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
4296
+ return handleHerdrInspectorAction(action as (typeof HERDR_INSPECTOR_ACTIONS)[number], paramsWithResolvedCwd, {
4297
+ state: deps.state,
4298
+ cwd: requestCwd,
4299
+ ...(deps.config.missions ? { missions: deps.config.missions } : {}),
4300
+ ...(deps.config.authorityPolicy ? { authorityPolicy: deps.config.authorityPolicy } : {}),
4301
+ signal,
4302
+ });
4303
+ }
4304
+ if ((MISSION_ACTIONS as readonly string[]).includes(action)) {
4305
+ if (deps.allowMutatingManagementActions === false && MUTATING_MANAGEMENT_ACTIONS.has(action)) {
4306
+ return {
4307
+ content: [{ type: "text", text: `Action '${action}' is not available from child-safe subagent fanout mode.` }],
4308
+ isError: true,
4309
+ details: { mode: "management", results: [] },
4310
+ };
4311
+ }
4312
+ const currentSessionId = deps.state.currentSessionId ?? ctx.sessionManager.getSessionId() ?? undefined;
4313
+ return handleMissionAction(action as (typeof MISSION_ACTIONS)[number], paramsWithResolvedCwd, {
4314
+ cwd: requestCwd,
4315
+ ...(deps.config.missions ? { config: deps.config.missions } : {}),
4316
+ ...(currentSessionId ? { currentSessionId } : {}),
4317
+ });
4318
+ }
4319
+ const policyAction = action === "stop" ? "stopRun" : action === "steer" ? "steerRun" : action === "schedule.create" ? "scheduleCreate" : undefined;
4320
+ if (policyAction) {
4321
+ const decision = resolveAuthorityDecision({ action: policyAction, ...(deps.config.authorityPolicy === undefined ? {} : { policy: deps.config.authorityPolicy }) });
4322
+ if (decision === "forbid") {
4323
+ return { content: [{ type: "text", text: `Authority policy forbids action '${action}'.` }], isError: true, details: { mode: "management", results: [] } };
4324
+ }
4325
+ if (decision === "confirm") {
4326
+ 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: [] } };
4327
+ const confirmed = await ctx.ui.confirm(`Authorize subagent ${action}?`, `Authority policy requires confirmation before '${action}'.`);
4328
+ if (!confirmed) return { content: [{ type: "text", text: `Action '${action}' canceled; authority was not granted.` }], details: { mode: "management", results: [] } };
4329
+ }
4330
+ }
3699
4331
  if ((WATCHDOG_TOOL_ACTIONS as readonly string[]).includes(action)) {
3700
4332
  if (deps.allowMutatingManagementActions === false && MUTATING_MANAGEMENT_ACTIONS.has(action)) {
3701
4333
  return {
@@ -3740,7 +4372,15 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3740
4372
  details: { mode: "management", results: [], spawnBudget: preview.snapshot },
3741
4373
  };
3742
4374
  }
3743
- const confirmed = await ctx.ui.confirm(
4375
+ const authority = resolveAuthorityDecision({ action: "spawnBudgetGrant", ...(deps.config.authorityPolicy === undefined ? {} : { policy: deps.config.authorityPolicy }) });
4376
+ if (authority === "forbid") {
4377
+ return {
4378
+ content: [{ type: "text", text: "Authority policy forbids spawn budget grants." }],
4379
+ isError: true,
4380
+ details: { mode: "management", results: [], spawnBudget: preview.snapshot },
4381
+ };
4382
+ }
4383
+ const confirmed = authority === "auto" || await ctx.ui.confirm(
3744
4384
  "Grant subagent spawn budget?",
3745
4385
  `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
4386
  );
@@ -3790,7 +4430,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3790
4430
  return {
3791
4431
  content: [{
3792
4432
  type: "text",
3793
- text: buildDoctorReport({
4433
+ text: buildDoctorReport(omitUndefinedProperties({
3794
4434
  cwd: requestCwd,
3795
4435
  config: deps.config,
3796
4436
  state: deps.state,
@@ -3801,13 +4441,13 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3801
4441
  orchestratorTarget,
3802
4442
  sessionError,
3803
4443
  expandTilde: deps.expandTilde,
3804
- }),
4444
+ })),
3805
4445
  }],
3806
4446
  details: { mode: "management", results: [], spawnBudget },
3807
4447
  };
3808
4448
  }
3809
4449
  if (action === "status") {
3810
- deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
4450
+ if (!preserveActiveSession) deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
3811
4451
  const withBudget = (result: AgentToolResult<Details>) => withSpawnBudgetStatus(
3812
4452
  result,
3813
4453
  deps.state,
@@ -3818,11 +4458,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3818
4458
  const nestedScope = nestedResolutionScopeForExecutor(deps);
3819
4459
  const sessionRoots = trustedSessionRootsForStatus(ctx, deps);
3820
4460
  if (paramsWithResolvedCwd.view === "fleet") {
3821
- return withBudget(inspectSubagentStatus(paramsWithResolvedCwd, { state: deps.state, nested: nestedScope, sessionRoots }));
4461
+ return withBudget(inspectSubagentStatus(paramsWithResolvedCwd, omitUndefinedProperties({ state: deps.state, nested: nestedScope, sessionRoots })));
3822
4462
  }
3823
4463
  if (targetRunId) {
3824
4464
  try {
3825
- const resolved = resolveSubagentRunId(targetRunId, { state: deps.state, nested: nestedScope });
4465
+ const resolved = resolveSubagentRunId(targetRunId, omitUndefinedProperties({ state: deps.state, nested: nestedScope }));
3826
4466
  if (resolved?.kind === "foreground") {
3827
4467
  const foreground = getForegroundControl(deps.state, resolved.id);
3828
4468
  if (foreground) {
@@ -3849,7 +4489,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3849
4489
  });
3850
4490
  }
3851
4491
  }
3852
- return withBudget(inspectSubagentStatus(paramsWithResolvedCwd, { state: deps.state, nested: nestedScope, sessionRoots }));
4492
+ return withBudget(inspectSubagentStatus(paramsWithResolvedCwd, omitUndefinedProperties({ state: deps.state, nested: nestedScope, sessionRoots })));
3853
4493
  }
3854
4494
 
3855
4495
  if (action === "approve-checkpoint" || action === "reject-checkpoint") {
@@ -3859,7 +4499,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3859
4499
  return { content: [{ type: "text", text: `action='${action}' requires id or dir.` }], isError: true, details: { mode: "management", results: [] } };
3860
4500
  }
3861
4501
  try {
3862
- const resolved = targetRunId ? resolveSubagentRunId(targetRunId, { state: deps.state, nested: nestedResolutionScopeForExecutor(deps) }) : undefined;
4502
+ const resolved = targetRunId ? resolveSubagentRunId(targetRunId, omitUndefinedProperties({ state: deps.state, nested: nestedResolutionScopeForExecutor(deps) })) : undefined;
3863
4503
  const decision = action === "approve-checkpoint" ? "approved" : "rejected";
3864
4504
  if (resolved?.kind === "foreground") {
3865
4505
  const run = deps.state.foregroundRuns?.get(resolved.id);
@@ -3870,7 +4510,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3870
4510
  return { content: [{ type: "text", text: `Run '${resolved.id}' was not found in the active session.` }], isError: true, details: { mode: "management", results: [] } };
3871
4511
  }
3872
4512
  const decidedAt = Date.now();
3873
- const checkpoint = { ...run.checkpoint, status: decision, ...(decision === "approved" ? { approvedAt: decidedAt } : { rejectedAt: decidedAt }) };
4513
+ const checkpoint: NonNullable<Details["checkpoint"]> = decision === "approved"
4514
+ ? { ...run.checkpoint, status: "approved", approvedAt: decidedAt }
4515
+ : { ...run.checkpoint, status: "rejected", rejectedAt: decidedAt };
3874
4516
  run.checkpoint = checkpoint;
3875
4517
  run.updatedAt = decidedAt;
3876
4518
  return {
@@ -3879,7 +4521,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3879
4521
  };
3880
4522
  }
3881
4523
  const location = paramsWithResolvedCwd.dir
3882
- ? resolveAsyncRunLocation(paramsWithResolvedCwd, ASYNC_DIR, RESULTS_DIR)
4524
+ ? resolveAsyncRunLocation(paramsWithResolvedCwd, DIRS.async, DIRS.results)
3883
4525
  : resolved?.kind === "async"
3884
4526
  ? resolved.location
3885
4527
  : undefined;
@@ -3905,7 +4547,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3905
4547
  }
3906
4548
  }
3907
4549
  if (action === "resume") {
3908
- return resumeAsyncRun({ params: paramsWithResolvedCwd, requestCwd, ctx, deps, parentModel: requestParentModel });
4550
+ return resumeAsyncRun(omitUndefinedProperties({ params: paramsWithResolvedCwd, requestCwd, ctx, deps, parentModel: requestParentModel }));
3909
4551
  }
3910
4552
  if (action === "steer") {
3911
4553
  deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
@@ -3914,9 +4556,13 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3914
4556
  const targetRunId = paramsWithResolvedCwd.runId ?? paramsWithResolvedCwd.id;
3915
4557
  if (paramsWithResolvedCwd.dir) {
3916
4558
  try {
3917
- const location = resolveAsyncRunLocation(paramsWithResolvedCwd, ASYNC_DIR, RESULTS_DIR);
4559
+ const location = resolveAsyncRunLocation(paramsWithResolvedCwd, DIRS.async, DIRS.results);
3918
4560
  const runId = location.resolvedId ?? targetRunId ?? path.basename(location.asyncDir ?? paramsWithResolvedCwd.dir);
3919
- return steerAsyncRun({
4561
+ if (location.asyncDir) {
4562
+ const unsupported = externalRunnerControlError(location.asyncDir, "steer");
4563
+ if (unsupported) return unsupported;
4564
+ }
4565
+ return steerAsyncRun(compactOptional<Parameters<typeof steerAsyncRun>[0]>({
3920
4566
  state: deps.state,
3921
4567
  runId,
3922
4568
  message,
@@ -3928,10 +4574,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3928
4574
  ? {}
3929
4575
  : {
3930
4576
  recover: ({ absoluteDeadlineAt, ...limits }) =>
3931
- resumeAsyncRun({ params: { ...limits, action: "resume", id: runId, message }, requestCwd, ctx, deps, parentModel: requestParentModel, absoluteDeadlineAt }),
4577
+ resumeAsyncRun(omitUndefinedProperties({ params: { ...limits, action: "resume", id: runId, message }, requestCwd, ctx, deps, parentModel: requestParentModel, absoluteDeadlineAt })),
3932
4578
  }
3933
4579
  ),
3934
- });
4580
+ }));
3935
4581
  } catch (error) {
3936
4582
  const text = error instanceof Error ? error.message : String(error);
3937
4583
  return { content: [{ type: "text", text }], isError: true, details: { mode: "management", results: [] } };
@@ -3940,15 +4586,19 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3940
4586
  if (!targetRunId) return { content: [{ type: "text", text: "action='steer' requires id or dir." }], isError: true, details: { mode: "management", results: [] } };
3941
4587
  let resolved: ResolvedSubagentRunId | undefined;
3942
4588
  try {
3943
- resolved = resolveSubagentRunId(targetRunId, { state: deps.state, nested: nestedResolutionScopeForExecutor(deps) });
4589
+ resolved = resolveSubagentRunId(targetRunId, omitUndefinedProperties({ state: deps.state, nested: nestedResolutionScopeForExecutor(deps) }));
3944
4590
  } catch (error) {
3945
4591
  const text = error instanceof Error ? error.message : String(error);
3946
4592
  return { content: [{ type: "text", text }], isError: true, details: { mode: "management", results: [] } };
3947
4593
  }
3948
- if (resolved?.kind === "nested") return steerNestedRun({ target: resolved, message, index: paramsWithResolvedCwd.index, signal });
4594
+ if (resolved?.kind === "nested") return steerNestedRun(omitUndefinedProperties({ target: resolved, message, index: paramsWithResolvedCwd.index, signal }));
3949
4595
  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
4596
  if (resolved?.kind !== "async") return { content: [{ type: "text", text: `No async run found for '${targetRunId}'.` }], isError: true, details: { mode: "management", results: [] } };
3951
- return steerAsyncRun({
4597
+ if (resolved.location.asyncDir) {
4598
+ const unsupported = externalRunnerControlError(resolved.location.asyncDir, "steer");
4599
+ if (unsupported) return unsupported;
4600
+ }
4601
+ return steerAsyncRun(compactOptional<Parameters<typeof steerAsyncRun>[0]>({
3952
4602
  state: deps.state,
3953
4603
  runId: resolved.id,
3954
4604
  message,
@@ -3960,22 +4610,29 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3960
4610
  ? {}
3961
4611
  : {
3962
4612
  recover: ({ absoluteDeadlineAt, ...limits }) =>
3963
- resumeAsyncRun({
4613
+ resumeAsyncRun(omitUndefinedProperties({
3964
4614
  params: { ...limits, action: "resume", id: resolved!.id, message },
3965
4615
  requestCwd,
3966
4616
  ctx,
3967
4617
  deps,
3968
4618
  parentModel: requestParentModel,
3969
4619
  absoluteDeadlineAt,
3970
- }),
4620
+ })),
3971
4621
  }
3972
4622
  ),
3973
- });
4623
+ }));
3974
4624
  }
3975
4625
  if (action === "append-step") {
3976
- return appendStepToAsyncChain({ params: paramsWithResolvedCwd, requestCwd, ctx, deps, parentModel: requestParentModel });
4626
+ return appendStepToAsyncChain(omitUndefinedProperties({ params: paramsWithResolvedCwd, requestCwd, ctx, deps, parentModel: requestParentModel }));
3977
4627
  }
3978
- if (action === "schedule" || action === "schedule-list" || action === "schedule-status" || action === "schedule-cancel") {
4628
+ if (action.startsWith("schedule.")) {
4629
+ if (deps.allowMutatingManagementActions === false && MUTATING_MANAGEMENT_ACTIONS.has(action)) {
4630
+ return {
4631
+ content: [{ type: "text", text: `Action '${action}' is not available from child-safe subagent fanout mode.` }],
4632
+ isError: true,
4633
+ details: { mode: "management", results: [] },
4634
+ };
4635
+ }
3979
4636
  if (!deps.handleScheduledRunAction) {
3980
4637
  return {
3981
4638
  content: [{ type: "text", text: `Action '${action}' is not available in this subagent context.` }],
@@ -3987,11 +4644,17 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3987
4644
  }
3988
4645
  if (action === "stop") {
3989
4646
  const targetRunId = paramsWithResolvedCwd.runId ?? paramsWithResolvedCwd.id;
4647
+ const workflowController = targetRunId ? deps.state.workflowControllers?.get(targetRunId) : undefined;
4648
+ if (workflowController) {
4649
+ workflowController.abort(new Error("Workflow stopped by user."));
4650
+ return { content: [{ type: "text", text: `Stop requested for async workflow ${targetRunId}.` }], details: { mode: "management", results: [] } };
4651
+ }
3990
4652
  let resolved: ResolvedSubagentRunId | undefined;
3991
4653
  if (paramsWithResolvedCwd.dir) {
3992
4654
  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);
4655
+ const location = resolveAsyncRunLocation(paramsWithResolvedCwd, DIRS.async, DIRS.results);
4656
+ const stopResult = stopAsyncRun(deps.state, location.resolvedId ?? targetRunId ?? path.basename(location.asyncDir ?? paramsWithResolvedCwd.dir), deps.kill, location);
4657
+ 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
4658
  } catch (error) {
3996
4659
  const text = error instanceof Error ? error.message : String(error);
3997
4660
  return { content: [{ type: "text", text }], isError: true, details: { mode: "management", results: [] } };
@@ -3999,13 +4662,19 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3999
4662
  }
4000
4663
  if (!targetRunId) return { content: [{ type: "text", text: "action='stop' requires id or dir." }], isError: true, details: { mode: "management", results: [] } };
4001
4664
  try {
4002
- resolved = resolveSubagentRunId(targetRunId, { state: deps.state, nested: nestedResolutionScopeForExecutor(deps) });
4665
+ resolved = resolveSubagentRunId(targetRunId, omitUndefinedProperties({ state: deps.state, nested: nestedResolutionScopeForExecutor(deps) }));
4003
4666
  } catch (error) {
4004
4667
  const message = error instanceof Error ? error.message : String(error);
4005
4668
  return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
4006
4669
  }
4007
4670
  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
4671
  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: [] } };
4672
+ if (resolved?.kind === "async") {
4673
+ const existingStatus = readStatus(resolved.location.asyncDir ?? "");
4674
+ if (existingStatus?.mode === "workflow" && existingStatus.state === "running") {
4675
+ 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: [] } };
4676
+ }
4677
+ }
4009
4678
  const stopResult = stopAsyncRun(
4010
4679
  deps.state,
4011
4680
  resolved?.kind === "async" ? resolved.id : targetRunId,
@@ -4024,7 +4693,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4024
4693
  let resolved: ResolvedSubagentRunId | undefined;
4025
4694
  if (targetRunId) {
4026
4695
  try {
4027
- resolved = resolveSubagentRunId(targetRunId, { state: deps.state, nested: nestedResolutionScopeForExecutor(deps) });
4696
+ resolved = resolveSubagentRunId(targetRunId, omitUndefinedProperties({ state: deps.state, nested: nestedResolutionScopeForExecutor(deps) }));
4028
4697
  } catch (error) {
4029
4698
  const message = error instanceof Error ? error.message : String(error);
4030
4699
  return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
@@ -4036,7 +4705,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4036
4705
  const interrupted = foreground.interrupt();
4037
4706
  if (interrupted) {
4038
4707
  foreground.updatedAt = Date.now();
4039
- foreground.currentActivityState = undefined;
4708
+ delete foreground.currentActivityState;
4040
4709
  return {
4041
4710
  content: [{ type: "text", text: `Interrupt requested for foreground run ${foreground.runId}.` }],
4042
4711
  details: { mode: "management", results: [] },
@@ -4171,7 +4840,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4171
4840
  const spawnPreflight = preflightSpawnBudget(
4172
4841
  deps.state,
4173
4842
  deps.config,
4174
- deps.state.currentSessionId,
4843
+ requestSessionId,
4175
4844
  requestedSpawns,
4176
4845
  );
4177
4846
  if (spawnPreflight.error) return spawnBudgetErrorResult(spawnPreflight.error, foregroundMode);
@@ -4216,6 +4885,17 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4216
4885
  const requestedAsync = effectiveParams.async ?? deps.asyncByDefault;
4217
4886
  const backgroundRequestedWhileClarifying = (hasChain || hasTasks) && requestedAsync && effectiveParams.clarify === true;
4218
4887
  const effectiveAsync = requestedAsync && effectiveParams.clarify !== true;
4888
+ const selectedAgentNames = hasSingle
4889
+ ? [effectiveParams.agent!]
4890
+ : hasTasks
4891
+ ? (effectiveParams.tasks ?? []).map((task) => task.agent)
4892
+ : (effectiveParams.chain ?? []).flatMap((step) => getStepAgents(step as ChainStep));
4893
+ const externalAgent = selectedAgentNames
4894
+ .map((name) => agents.find((agent) => agent.name === name))
4895
+ .find((agent) => agent?.runner?.type === "external-cli");
4896
+ if (externalAgent && (!effectiveAsync || effectiveParams.foregroundOnly === true)) {
4897
+ 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.`);
4898
+ }
4219
4899
  const foregroundTimeout = resolveForegroundTimeout(
4220
4900
  effectiveParams,
4221
4901
  effectiveAsync ? undefined : DEFAULT_FOREGROUND_TIMEOUT_MS,
@@ -4223,12 +4903,17 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4223
4903
  if (foregroundTimeout.error) return buildRequestedModeError(effectiveParams, foregroundTimeout.error);
4224
4904
  const controlConfig = resolveControlConfig(deps.config.control, effectiveParams.control);
4225
4905
 
4226
- const artifactConfig: ArtifactConfig = {
4906
+ const artifactConfig: ArtifactConfig = omitUndefinedProperties({
4227
4907
  ...DEFAULT_ARTIFACT_CONFIG,
4228
4908
  enabled: effectiveParams.artifacts !== false,
4229
4909
  dir: deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir,
4230
- };
4910
+ });
4231
4911
  const artifactsDir = getArtifactsDir(parentSessionFile, effectiveCwd, artifactConfig.dir);
4912
+ if (artifactConfig.dir === "project" && !warnedArtifactPackageDirs.has(effectiveCwd)) {
4913
+ warnedArtifactPackageDirs.add(effectiveCwd);
4914
+ const warning = getProjectArtifactPackagingWarning(effectiveCwd);
4915
+ if (warning) console.warn(`[pi-subagents] ${warning}`);
4916
+ }
4232
4917
 
4233
4918
  let sessionRoot: string;
4234
4919
  if (effectiveParams.sessionDir) {
@@ -4284,15 +4969,48 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4284
4969
  }, contextPolicy.contextSummary))
4285
4970
  : undefined;
4286
4971
 
4972
+ let missionBinding: MissionLaunchBinding | undefined;
4973
+ let missionWarning: string | undefined;
4974
+ const explicitMission = effectiveParams.missionId !== undefined || effectiveParams.mission !== undefined;
4975
+ try {
4976
+ missionBinding = prepareMissionLaunch({
4977
+ params: effectiveParams,
4978
+ projectRoot: effectiveCwd,
4979
+ ...(deps.config.missions ? { config: deps.config.missions } : {}),
4980
+ ownerSessionId: requestSessionId,
4981
+ });
4982
+ } catch (error) {
4983
+ if (explicitMission) return toExecutionErrorResult(effectiveParams, error, contextPolicy.contextSummary);
4984
+ missionWarning = `Mission tracking unavailable: ${error instanceof Error ? error.message : String(error)}`;
4985
+ }
4986
+
4987
+ const attachMission = (result: AgentToolResult<Details>): AgentToolResult<Details> => {
4988
+ if (!missionBinding) return missionWarning ? { ...result, details: { ...result.details, missionWarning } } : result;
4989
+ try {
4990
+ return attachMissionToLaunchResult({ binding: missionBinding, result });
4991
+ } catch (error) {
4992
+ const warning = `Mission tracking unavailable after launch: ${error instanceof Error ? error.message : String(error)}`;
4993
+ if (explicitMission) {
4994
+ return {
4995
+ ...result,
4996
+ isError: true,
4997
+ content: [...result.content, { type: "text", text: warning }],
4998
+ details: { ...result.details, missionWarning: warning },
4999
+ };
5000
+ }
5001
+ return { ...result, details: { ...result.details, missionWarning: warning } };
5002
+ }
5003
+ };
5004
+
4287
5005
  const reservation = reserveSpawnBudget(
4288
5006
  deps.state,
4289
5007
  deps.config,
4290
- deps.state.currentSessionId,
5008
+ requestSessionId,
4291
5009
  requestedSpawns,
4292
5010
  );
4293
- if (reservation.error) return spawnBudgetErrorResult(reservation.error, foregroundMode);
5011
+ if (reservation.error) return attachMission(spawnBudgetErrorResult(reservation.error, foregroundMode));
4294
5012
 
4295
- const execData: ExecutionContextData = {
5013
+ const execData: ExecutionContextData = omitUndefinedProperties({
4296
5014
  params: effectiveParams,
4297
5015
  effectiveCwd,
4298
5016
  ctx,
@@ -4322,18 +5040,19 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4322
5040
  contextPolicy,
4323
5041
  modelScope,
4324
5042
  parentModel: requestParentModel,
4325
- parentSessionId: deps.state.currentSessionId,
4326
- capabilityCeiling: resolveCurrentSubagentCapabilityCeiling(deps.state.currentSessionId ?? undefined),
4327
- };
5043
+ parentSessionId: requestSessionId,
5044
+ parentPiSessionId: requestPiSessionId,
5045
+ capabilityCeiling: resolveCurrentSubagentCapabilityCeiling(requestSessionId),
5046
+ });
4328
5047
 
4329
5048
  const foregroundDescription = effectiveParams.task?.trim()
4330
5049
  || effectiveParams.tasks?.[0]?.task?.trim()
4331
5050
  || (effectiveParams.chain ? firstRawChainTask(effectiveParams.chain)?.trim() : undefined);
4332
5051
  const foregroundControl: ForegroundRunControl | undefined = effectiveAsync
4333
5052
  ? undefined
4334
- : {
5053
+ : compactOptional<ForegroundRunControl>({
4335
5054
  runId,
4336
- ...(deps.state.currentSessionId ? { sessionId: deps.state.currentSessionId } : {}),
5055
+ sessionId: requestSessionId,
4337
5056
  mode: foregroundMode,
4338
5057
  startedAt: Date.now(),
4339
5058
  updatedAt: Date.now(),
@@ -4347,7 +5066,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4347
5066
  schedulingOwners: 1,
4348
5067
  nestedRoute,
4349
5068
  interrupt: undefined,
4350
- };
5069
+ });
4351
5070
  if (foregroundControl) {
4352
5071
  deps.state.foregroundControls.set(runId, foregroundControl);
4353
5072
  deps.state.lastForegroundControlId = runId;
@@ -4367,21 +5086,33 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4367
5086
  const errorText = result?.isError
4368
5087
  ? result.content.find((item) => item.type === "text")?.text
4369
5088
  : 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] : [];
5089
+ let startedLaunches: StaticLaunchSummary[];
5090
+ try {
5091
+ startedLaunches = collectStaticLaunchSummaries({
5092
+ params: effectiveParams,
5093
+ agents,
5094
+ parentModel: requestParentModel,
5095
+ availableModels: ctx.modelRegistry.getAvailable().map(toModelInfo),
5096
+ currentProvider: requestParentModel?.provider,
5097
+ modelScope,
5098
+ thinkingOverrideForTask: forkThinkingOverrideForTask,
5099
+ dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
5100
+ });
5101
+ } catch (error) {
5102
+ console.error("Failed to resolve nested foreground launch metadata:", error);
5103
+ startedLaunches = selectedAgentNames.map((agent) => ({ agent }));
5104
+ }
5105
+ const agentsForSummary = startedLaunches.map((launch) => launch.agent);
4375
5106
  const leafIntercomTarget = intercomBridge.active && agentsForSummary[0]
4376
5107
  ? resolveSubagentIntercomTarget(runId, agentsForSummary[0], 0)
4377
5108
  : undefined;
4378
5109
  try {
4379
- writeNestedEvent(inheritedNestedRoute, {
5110
+ writeNestedEvent(inheritedNestedRoute, compactOptional<Parameters<typeof writeNestedEvent>[1]>({
4380
5111
  type,
4381
5112
  ts: now,
4382
5113
  parentRunId: nestedParentAddress.parentRunId,
4383
5114
  parentStepIndex: nestedParentAddress.parentStepIndex,
4384
- child: {
5115
+ child: compactOptional<NestedRunSummary>({
4385
5116
  id: runId,
4386
5117
  parentRunId: nestedParentAddress.parentRunId,
4387
5118
  parentStepIndex: nestedParentAddress.parentStepIndex,
@@ -4395,19 +5126,32 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4395
5126
  state,
4396
5127
  agent: agentsForSummary[0],
4397
5128
  agents: agentsForSummary,
5129
+ ...(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 } : {}),
5130
+ ...(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
5131
  startedAt: foregroundControl?.startedAt ?? now,
4399
5132
  ...(state !== "running" ? { endedAt: now } : {}),
4400
5133
  lastUpdate: now,
4401
5134
  ...(details?.totalCost ? { totalCost: details.totalCost } : {}),
4402
5135
  ...(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
- });
5136
+ ...(type === "subagent.nested.started"
5137
+ ? { steps: startedLaunches.map((launch) => ({
5138
+ agent: launch.agent,
5139
+ status: "running" as const,
5140
+ ...(launch.model ? { model: launch.model } : {}),
5141
+ ...(launch.thinking ? { thinking: launch.thinking } : {}),
5142
+ })) }
5143
+ : details?.results.length
5144
+ ? { steps: details.results.map((child) => ({
5145
+ agent: child.agent,
5146
+ status: child.interrupted || child.detached ? "paused" as const : child.exitCode === 0 ? "complete" as const : "failed" as const,
5147
+ ...(child.model ? { model: child.model } : {}),
5148
+ ...(child.thinking ? { thinking: child.thinking } : {}),
5149
+ ...(child.sessionFile ? { sessionFile: child.sessionFile } : {}),
5150
+ ...(child.error ? { error: child.error } : {}),
5151
+ })) }
5152
+ : {}),
5153
+ }),
5154
+ }));
4411
5155
  } catch (error) {
4412
5156
  console.error("Failed to emit nested foreground status event:", error);
4413
5157
  }
@@ -4416,7 +5160,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4416
5160
  let nestedForegroundStarted = false;
4417
5161
  try {
4418
5162
  const asyncResult = runAsyncPath(execData, deps);
4419
- if (asyncResult) return withResolvedContext(withForkThinkingNotes(asyncResult, forkThinkingDowngrades), contextPolicy.contextSummary);
5163
+ if (asyncResult) return attachMission(withResolvedContext(withForkThinkingNotes(asyncResult, forkThinkingDowngrades), contextPolicy.contextSummary));
4420
5164
  if (foregroundControl) {
4421
5165
  writeNestedForegroundEvent("subagent.nested.started");
4422
5166
  nestedForegroundStarted = true;
@@ -4424,22 +5168,22 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4424
5168
  if (hasChain && effectiveParams.chain) {
4425
5169
  const result = await runChainPath(execData, deps);
4426
5170
  writeNestedForegroundEvent("subagent.nested.completed", result);
4427
- return withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary);
5171
+ return attachMission(withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary));
4428
5172
  }
4429
5173
  if (hasTasks && effectiveParams.tasks) {
4430
5174
  const result = await runParallelPath(execData, deps);
4431
5175
  writeNestedForegroundEvent("subagent.nested.completed", result);
4432
- return withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary);
5176
+ return attachMission(withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary));
4433
5177
  }
4434
5178
  if (hasSingle) {
4435
5179
  const result = await runSinglePath(execData, deps);
4436
5180
  writeNestedForegroundEvent("subagent.nested.completed", result);
4437
- return withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary);
5181
+ return attachMission(withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary));
4438
5182
  }
4439
5183
  } catch (error) {
4440
5184
  const errorResult = withForkThinkingNotes(toExecutionErrorResult(effectiveParams, error, contextPolicy.contextSummary), forkThinkingDowngrades);
4441
5185
  if (nestedForegroundStarted) writeNestedForegroundEvent("subagent.nested.completed", errorResult);
4442
- return errorResult;
5186
+ return attachMission(errorResult);
4443
5187
  } finally {
4444
5188
  if (foregroundControl) {
4445
5189
  settleForegroundSchedulingOwner(foregroundControl);
@@ -4497,5 +5241,23 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4497
5241
  return execute(id, delegatedParams, signal, onUpdate, ctx);
4498
5242
  };
4499
5243
 
4500
- return { execute: executeWithSingleDispatchGuard, executeDelegated };
5244
+ const executeScheduled = (
5245
+ id: string,
5246
+ params: SubagentParamsLike,
5247
+ signal: AbortSignal,
5248
+ ctx: ExtensionContext,
5249
+ ) => {
5250
+ const ownerSessionId = resolveCurrentSessionId(ctx.sessionManager);
5251
+ let ownerExecutor = scheduledOwnerExecutors.get(ownerSessionId);
5252
+ if (!ownerExecutor) {
5253
+ ownerExecutor = createSubagentExecutor({
5254
+ ...deps,
5255
+ state: createScheduledOwnerState(deps.state, ownerSessionId, ctx),
5256
+ });
5257
+ scheduledOwnerExecutors.set(ownerSessionId, ownerExecutor);
5258
+ }
5259
+ return ownerExecutor.execute(id, params, signal, undefined, ctx);
5260
+ };
5261
+
5262
+ return { execute: executeWithSingleDispatchGuard, executeDelegated, executeScheduled };
4501
5263
  }