pi-subagents 0.50.0 → 0.52.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 (109) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/agents/oracle.md +3 -1
  3. package/agents/reviewer.md +1 -0
  4. package/agents/scout.md +2 -2
  5. package/agents/worker.md +2 -1
  6. package/async-retention-discovery-worker.mjs +180 -0
  7. package/docs/agents.md +40 -2
  8. package/docs/configuration.md +30 -12
  9. package/docs/extension-api.md +42 -1
  10. package/docs/models.md +2 -0
  11. package/docs/observability.md +41 -5
  12. package/docs/tool-reference.md +38 -39
  13. package/docs/workflows.md +171 -3
  14. package/package.json +4 -2
  15. package/skills/pi-subagents/SKILL.md +6 -4
  16. package/skills/pi-subagents/references/constraints-and-recipes.md +12 -6
  17. package/skills/pi-subagents/references/execution-controls.md +29 -15
  18. package/skills/pi-subagents/references/management-authoring-rpc.md +3 -3
  19. package/skills/pi-subagents/references/prompting-and-roles.md +4 -2
  20. package/src/agents/agent-management.ts +124 -351
  21. package/src/agents/agents.ts +157 -31
  22. package/src/agents/skills.ts +1 -1
  23. package/src/api/external-job-provider.ts +185 -0
  24. package/src/api/preflight.ts +36 -8
  25. package/src/api/shared-types.ts +2 -0
  26. package/src/extension/config.ts +3 -3
  27. package/src/extension/doctor.ts +3 -6
  28. package/src/extension/fanout-child.ts +2 -2
  29. package/src/extension/index.ts +166 -88
  30. package/src/extension/public-execution.ts +31 -2
  31. package/src/extension/schemas.ts +12 -35
  32. package/src/extension/tool-description.ts +35 -24
  33. package/src/inspectors/herdr/actions.ts +2 -2
  34. package/src/inspectors/herdr/inspector-runner.ts +2 -1
  35. package/src/inspectors/herdr/project-panes.ts +2 -2
  36. package/src/intercom/native-supervisor-channel.ts +32 -10
  37. package/src/missions/lifecycle.ts +6 -1
  38. package/src/missions/store.ts +4 -9
  39. package/src/missions/workflow-state.ts +2 -2
  40. package/src/profiles/profiles.ts +3 -1
  41. package/src/runs/background/active-run-index.ts +31 -8
  42. package/src/runs/background/async-execution.ts +68 -40
  43. package/src/runs/background/async-job-tracker.ts +20 -4
  44. package/src/runs/background/async-resume.ts +47 -15
  45. package/src/runs/background/async-retention.ts +886 -0
  46. package/src/runs/background/async-status.ts +39 -53
  47. package/src/runs/background/chain-append.ts +3 -33
  48. package/src/runs/background/completion-dedupe.ts +5 -1
  49. package/src/runs/background/completion-replay.ts +22 -12
  50. package/src/runs/background/control-channel.ts +14 -68
  51. package/src/runs/background/fleet-view.ts +68 -19
  52. package/src/runs/background/index-segment.ts +59 -0
  53. package/src/runs/background/inspect-rpc.ts +443 -0
  54. package/src/runs/background/notify.ts +31 -5
  55. package/src/runs/background/result-files.ts +158 -90
  56. package/src/runs/background/result-watcher.ts +116 -20
  57. package/src/runs/background/resume-guidance.ts +8 -5
  58. package/src/runs/background/retained-children.ts +13 -3
  59. package/src/runs/background/run-id-query.ts +7 -0
  60. package/src/runs/background/run-id-resolver.ts +11 -9
  61. package/src/runs/background/run-status.ts +27 -18
  62. package/src/runs/background/scheduled-runs.ts +24 -4
  63. package/src/runs/background/stale-run-reconciler.ts +6 -3
  64. package/src/runs/background/steering.ts +11 -1
  65. package/src/runs/background/subagent-runner.ts +253 -140
  66. package/src/runs/background/subagent-wait.ts +8 -8
  67. package/src/runs/background/terminal-run-index.ts +129 -0
  68. package/src/runs/background/wait-completions.ts +21 -4
  69. package/src/runs/background/wait-subscriptions.ts +81 -2
  70. package/src/runs/foreground/async-steering-action.ts +21 -14
  71. package/src/runs/foreground/execution.ts +3 -1
  72. package/src/runs/foreground/subagent-executor.ts +666 -1579
  73. package/src/runs/foreground/workflow-detach-reconcile.ts +194 -0
  74. package/src/runs/foreground/workflow-foreground-steering.ts +6 -5
  75. package/src/runs/shared/acceptance.ts +4 -4
  76. package/src/runs/shared/chain-outputs.ts +1 -3
  77. package/src/runs/shared/external-job-bridge.ts +444 -0
  78. package/src/runs/shared/external-job-runner.ts +286 -0
  79. package/src/runs/shared/mcp-direct-tool-allowlist.ts +14 -0
  80. package/src/runs/shared/model-fallback.ts +98 -38
  81. package/src/runs/shared/orca-progress-tabs.ts +84 -22
  82. package/src/runs/shared/parallel-handoff.ts +46 -4
  83. package/src/runs/shared/parallel-utils.ts +4 -15
  84. package/src/runs/shared/permissions.ts +5 -1
  85. package/src/runs/shared/pi-args.ts +8 -1
  86. package/src/runs/shared/session-lease.ts +0 -6
  87. package/src/runs/shared/subagent-control.ts +26 -4
  88. package/src/runs/shared/subagent-prompt-runtime.ts +12 -6
  89. package/src/runs/shared/workflow-graph.ts +1 -23
  90. package/src/runs/shared/worktree.ts +14 -2
  91. package/src/shared/atomic-json.ts +22 -2
  92. package/src/shared/capacity-resilient-json.ts +102 -0
  93. package/src/shared/completion-owner.ts +14 -0
  94. package/src/shared/file-system-retry.ts +49 -1
  95. package/src/shared/fork-context.ts +42 -0
  96. package/src/shared/prompt-resources.ts +0 -40
  97. package/src/shared/settings.ts +3 -27
  98. package/src/shared/types.ts +71 -26
  99. package/src/shared/utils.ts +8 -0
  100. package/src/shared/watch-strategy.ts +10 -0
  101. package/src/slash/slash-bridge.ts +2 -1
  102. package/src/slash/slash-commands.ts +29 -3
  103. package/src/tui/fleet.ts +63 -15
  104. package/src/watchdog/change-signature.ts +1 -1
  105. package/src/watchdog/lsp-diagnostics.ts +1 -0
  106. package/src/workflows/chat-progress.ts +2 -2
  107. package/src/workflows/scripted-workflow.ts +220 -67
  108. package/src/runs/foreground/chain-clarify.ts +0 -1354
  109. package/src/runs/foreground/chain-execution.ts +0 -1581
@@ -3,12 +3,12 @@ import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
5
5
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
6
- import { resolveAgentName, type AgentConfig, type AgentScope } from "../../agents/agents.ts";
6
+ import { findBlockingAgentDiagnostic, resolveAgentName, type AgentConfig, type AgentDiscoveryDiagnostic, type AgentScope } from "../../agents/agents.ts";
7
7
  import { getArtifactsDir, getChainRunsDir, getProjectArtifactPackagingWarning, getProjectSubagentsDir } from "../../shared/artifacts.ts";
8
8
  import { writeAtomicJson } from "../../shared/atomic-json.ts";
9
- import { ChainClarifyComponent, type ChainClarifyResult } from "./chain-clarify.ts";
9
+ import { createCapacityResilientJsonWriter } from "../../shared/capacity-resilient-json.ts";
10
+ import { isRetryableFileSystemError, isStorageCapacityError } from "../../shared/file-system-retry.ts";
10
11
  import { resolveEffectiveThinking, toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
11
- import { executeChain } from "./chain-execution.ts";
12
12
  import {
13
13
  beginForegroundChild,
14
14
  finishForegroundChild,
@@ -28,7 +28,7 @@ import { normalizePublicSubagentExecution } from "../../extension/public-executi
28
28
  import { runSync } from "./execution.ts";
29
29
  import { handleWatchdogToolAction, WATCHDOG_TOOL_ACTIONS } from "../../watchdog/tool-actions.ts";
30
30
  import type { MainWatchdogRuntime } from "../../watchdog/runtime.ts";
31
- import { buildModelCandidates, normalizeParentModel, resolveEffectiveSubagentModel, resolveModelCandidate, type ParentModel } from "../shared/model-fallback.ts";
31
+ import { buildModelCandidates, inheritsParentModel, normalizeParentModel, resolveEffectiveSubagentModel, resolveModelCandidate, type ParentModel } from "../shared/model-fallback.ts";
32
32
  import { formatRetainedChildren, listRetainedChildren } from "../background/retained-children.ts";
33
33
  import type { ModelScopeConfig } from "../shared/model-scope.ts";
34
34
  import { aggregateParallelOutputs } from "../shared/parallel-utils.ts";
@@ -55,13 +55,15 @@ import {
55
55
  import { discoverAvailableSkills, normalizeSkillInput } from "../../agents/skills.ts";
56
56
  import { buildAsyncRunnerSteps, DEFAULT_ASYNC_TIMEOUT_MS, executeAsyncChain, executeAsyncSingle, formatAsyncStartedMessage, isAsyncAvailable, workflowAwaitedAsyncResultPath } from "../background/async-execution.ts";
57
57
  import { updateActiveRunIndex } from "../background/active-run-index.ts";
58
+ import { steeringReceipt } from "../background/steering.ts";
58
59
  import { acquireActiveAsyncCapacity, ActiveAsyncCapacityError, getActiveAsyncCapacitySnapshot, resolveMaxActiveAsyncRunsPerSession, transferActiveAsyncCapacity, type ActiveAsyncCapacityHandle } from "../background/active-async-capacity.ts";
59
60
  import { isScheduledRunAction, type ScheduledRunAction } from "../background/scheduled-runs.ts";
60
61
  import { enqueueChainAppendRequest, readPendingChainAppendRequests, runnerStepOutputNames } from "../background/chain-append.ts";
61
62
  import { ChainOutputValidationError, validateChainOutputBindingsWithContext } from "../shared/chain-outputs.ts";
62
63
  import { normalizeGateAcceptance, validateExecutionAcceptance } from "../shared/acceptance.ts";
63
- import { createForkContextResolver, forkedChildRequiresThinkingOff } from "../../shared/fork-context.ts";
64
+ import { canPreferFork, createForkContextResolver, forkedChildRequiresThinkingOff, resolveSubagentLaunchContext } from "../../shared/fork-context.ts";
64
65
  import { resolveCurrentSessionId } from "../../shared/session-identity.ts";
66
+ import { currentCompletionOwnerId } from "../../shared/completion-owner.ts";
65
67
  import { applyIntercomBridgeToAgent, INTERCOM_BRIDGE_MARKER, resolveIntercomBridge, resolveIntercomSessionTarget, resolveSubagentIntercomTarget, type IntercomBridgeState } from "../../intercom/intercom-bridge.ts";
66
68
  import { formatControlIntercomMessage, formatControlNoticeMessage, resolveControlConfig, shouldNotifyControlEvent } from "../shared/subagent-control.ts";
67
69
  import { resolveTurnBudgetConfig } from "../shared/turn-budget.ts";
@@ -86,10 +88,10 @@ import {
86
88
  resolveSubagentResultStatus,
87
89
  stripDetailsOutputsForIntercomReceipt,
88
90
  } from "../../intercom/result-intercom.ts";
89
- import { applySteeringRecoveryAgentConfig, buildRevivedAsyncTask, resolveAsyncResumeTarget, resolveAsyncRunLocation } from "../background/async-resume.ts";
90
- import { deliverCheckpointDecisionRequest, deliverInterruptRequest, readRevivalBriefs, requestAsyncSteer, type SteerDeliveryMode } from "../background/control-channel.ts";
91
+ import { applySteeringRecoveryAgentConfig, asyncReviveRequiresRecoveryDescriptor, buildRevivedAsyncTask, resolveAsyncResumeTarget, resolveAsyncRunLocation } from "../background/async-resume.ts";
92
+ import { deliverInterruptRequest, readRevivalBriefs, requestAsyncSteer, type SteerDeliveryMode } from "../background/control-channel.ts";
91
93
  import { updateSteeringTarget, waitForSteeringAction } from "../background/steering.ts";
92
- import { steerAsyncRun } from "./async-steering-action.ts";
94
+ import { canQueueRetainedAsyncFollowUp, steerAsyncRun } from "./async-steering-action.ts";
93
95
  import {
94
96
  removeWorkflowForegroundSteeringRoute,
95
97
  resolveWorkflowForegroundSteeringTarget,
@@ -99,6 +101,7 @@ import {
99
101
  } from "./workflow-foreground-steering.ts";
100
102
  import { stopAsyncRun } from "./async-stop-action.ts";
101
103
  import { dismissRecoveredWorkflow } from "./async-dismiss-action.ts";
104
+ import { promotePausedWorkflowIfSettled, reconcileDetachedWorkflowChildCompletion } from "./workflow-detach-reconcile.ts";
102
105
  import { reconcileAsyncRun } from "../background/stale-run-reconciler.ts";
103
106
  import { resolveAsyncRootResultPath, waitForImportedAsyncRoot } from "../background/chain-root-attachment.ts";
104
107
  import { resultFilePath, writeAsyncResultFile } from "../background/result-files.ts";
@@ -115,8 +118,7 @@ import { createMissionWorkflowState } from "../../missions/workflow-state.ts";
115
118
  import { resolveAuthorityDecision } from "../../policy/authority.ts";
116
119
  import { handleHerdrInspectorAction, HERDR_INSPECTOR_ACTIONS } from "../../inspectors/herdr/actions.ts";
117
120
  import { handleHerdrProjectPaneAction, HERDR_PROJECT_PANE_ACTIONS } from "../../inspectors/herdr/project-panes.ts";
118
- import { previewSimpleWorkflowRun, runWorkflowScript, WorkflowScriptError, type WorkflowScriptChildResult } from "../../workflows/scripted-workflow.ts";
119
- import { renderWorkflowPrompt } from "../../shared/prompt-resources.ts";
121
+ import { previewSimpleWorkflowRun, runWorkflowScript, WorkflowScriptError, type WorkflowScriptChildResult, type WorkflowSteerOptions, type WorkflowSteerResult } from "../../workflows/scripted-workflow.ts";
120
122
  import { resolveWorkflowChatProgress, type WorkflowChatProgressProjection } from "../../workflows/chat-progress.ts";
121
123
  import {
122
124
  cleanupWorktrees,
@@ -170,10 +172,11 @@ import {
170
172
  resolveCurrentMaxSubagentDepth,
171
173
  resolveMaxSubagentSpawnsPerRun,
172
174
  wrapForkTask,
175
+ type ScheduleOrigin,
173
176
  } from "../../shared/types.ts";
174
177
 
175
178
  const MUTATING_MANAGEMENT_ACTIONS = new Set(["create", "update", "delete", "eject", "disable", "enable", "reset", "grant-spawn-budget", "watchdog.configure", "mission.create", "mission.update", "mission.resolve-decision", "mission.attach-run", "mission.close", "inspector.open", "inspector.close", "project.open", "project.close", "worktree.discard", "refine", "refine.rollback", "dismiss", "schedule.create", "schedule.pause", "schedule.resume", "schedule.run", "schedule.run-due", "schedule.delete"]);
176
- const DESTRUCTIVE_MANAGEMENT_ACTIONS = new Set(["delete", "eject", "disable", "reset", "mission.close", "worktree.discard", "refine.rollback", "inspector.close", "project.close", "stop", "interrupt", "reject-checkpoint", "schedule.delete"]);
179
+ const DESTRUCTIVE_MANAGEMENT_ACTIONS = new Set(["delete", "eject", "disable", "reset", "mission.close", "worktree.discard", "refine.rollback", "inspector.close", "project.close", "stop", "interrupt", "schedule.delete"]);
177
180
 
178
181
  function editDistance(left: string, right: string): number {
179
182
  const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
@@ -286,11 +289,15 @@ export interface SubagentParamsLike {
286
289
  mode?: SteerDeliveryMode;
287
290
  workflowScript?: string;
288
291
  chatProgress?: "auto" | "off" | "live-card";
292
+ isolation?: "none" | "worktree";
289
293
  step?: ChainStep;
290
294
  /** Internal workflow ownership metadata; not part of the public schema. */
291
295
  workflowParentRunId?: string;
292
296
  workflowKey?: string;
297
+ /** Set by the scheduler so this run's completion can name the schedule that produced it. */
298
+ scheduleOrigin?: ScheduleOrigin;
293
299
  workflowChildAsyncId?: string;
300
+ workflowParentDeadlineAt?: number;
294
301
  suppressRoutineResultIntercom?: boolean;
295
302
  /** Internal inherited cumulative run-tree budget. */
296
303
  runFanoutBudget?: RunFanoutBudgetDescriptor;
@@ -339,8 +346,6 @@ export interface SubagentParamsLike {
339
346
  acceptance?: AcceptanceInput;
340
347
  gate?: string;
341
348
  agentContract?: AgentContract;
342
- schedule?: string;
343
- scheduleName?: string;
344
349
  at?: string;
345
350
  every?: string;
346
351
  on?: string | number;
@@ -378,13 +383,15 @@ interface ExecutorDeps {
378
383
  tempArtifactsDir: string;
379
384
  getSubagentSessionRoot: (parentSessionFile: string | null) => string;
380
385
  expandTilde: (p: string) => string;
381
- discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[]; modelScope?: ModelScopeConfig };
386
+ discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[]; agentDiagnostics?: AgentDiscoveryDiagnostic[]; modelScope?: ModelScopeConfig };
382
387
  allowMutatingManagementActions?: boolean;
388
+ activateSupervisorTransport?: () => void;
389
+ refreshResultDelivery?: () => void;
383
390
  kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
384
391
  }
385
392
 
386
- type ForkSessionFileForTask = (agentName: string, idx?: number, modelOverride?: string) => string | undefined;
387
- type ForkThinkingOverrideForTask = (agentName: string, idx?: number, modelOverride?: string) => AgentConfig["thinking"] | undefined;
393
+ type ForkSessionFileForTask = (agentName: string, idx?: number, modelOverride?: string, modelOverrideFromParent?: boolean) => string | undefined;
394
+ type ForkThinkingOverrideForTask = (agentName: string, idx?: number, modelOverride?: string, modelOverrideFromParent?: boolean) => AgentConfig["thinking"] | undefined;
388
395
 
389
396
  interface ExecutionContextData {
390
397
  params: SubagentParamsLike;
@@ -462,6 +469,7 @@ function promptAuditRedoParams(value: unknown, rewrittenTask: string): SubagentP
462
469
  delete params.workflowParentRunId;
463
470
  delete params.workflowKey;
464
471
  delete params.workflowChildAsyncId;
472
+ delete params.workflowParentDeadlineAt;
465
473
  delete params.suppressRoutineResultIntercom;
466
474
  if (params.worktree === true && Array.isArray(params.tasks)) delete params.cwd;
467
475
  return params;
@@ -553,7 +561,7 @@ function countRequestedSubagentSpawns(params: SubagentParamsLike, config: Extens
553
561
  function staticRunFanoutPaths(params: SubagentParamsLike): string[] {
554
562
  if (params.tasks) return params.tasks.map((_, index) => `tasks[${index}]`);
555
563
  if (params.chain) return params.chain.flatMap((step, stepIndex) => {
556
- if (isDynamicParallelStep(step) || "checkpoint" in step) return [];
564
+ if (isDynamicParallelStep(step)) return [];
557
565
  if (isParallelStep(step)) return step.parallel.map((_, itemIndex) => `chain[${stepIndex}].parallel[${itemIndex}]`);
558
566
  return [`chain[${stepIndex}]`];
559
567
  });
@@ -627,7 +635,7 @@ function foregroundChildActivityFromProgress(progress: SingleResult["progress"]
627
635
  };
628
636
  }
629
637
 
630
- function rememberForegroundRun(state: SubagentState, input: { runId: string; mode: "single" | "parallel" | "chain"; cwd: string; sessionId: string | null; results: SingleResult[]; checkpoint?: Details["checkpoint"] }): void {
638
+ function rememberForegroundRun(state: SubagentState, input: { runId: string; mode: "single" | "parallel" | "chain"; cwd: string; sessionId: string | null; results: SingleResult[] }): void {
631
639
  state.foregroundRuns ??= new Map();
632
640
  const previous = state.foregroundRuns.get(input.runId);
633
641
  const updatedAt = Date.now();
@@ -637,7 +645,6 @@ function rememberForegroundRun(state: SubagentState, input: { runId: string; mod
637
645
  cwd: input.cwd,
638
646
  ...(input.sessionId ? { sessionId: input.sessionId } : {}),
639
647
  updatedAt,
640
- ...(input.checkpoint ? { checkpoint: input.checkpoint } : {}),
641
648
  children: input.results.map((result, index) => {
642
649
  const child = {
643
650
  agent: result.agent,
@@ -950,11 +957,48 @@ function emitAdvisoryControlEvent(pi: ExtensionAPI, channel: string, payload: un
950
957
  }
951
958
  }
952
959
 
960
+ function persistAsyncWorkflowControlEvent(input: {
961
+ job: AsyncJobState;
962
+ event: ControlEvent;
963
+ controlConfig: ResolvedControlConfig;
964
+ intercomBridge: IntercomBridgeState;
965
+ childIntercomTarget?: string;
966
+ }): void {
967
+ const channels = input.event.type === "active_long_running"
968
+ ? input.controlConfig.notifyChannels.filter((channel) => channel !== "intercom")
969
+ : input.controlConfig.notifyChannels;
970
+ if (channels.length === 0) return;
971
+ const record = {
972
+ ts: Date.now(),
973
+ runId: input.job.asyncId,
974
+ type: "subagent.control",
975
+ event: input.event,
976
+ channels,
977
+ childIntercomTarget: input.childIntercomTarget,
978
+ noticeText: formatControlNoticeMessage(input.event, input.childIntercomTarget),
979
+ ...(input.intercomBridge.active && input.intercomBridge.orchestratorTarget && channels.includes("intercom")
980
+ ? {
981
+ intercom: {
982
+ to: input.intercomBridge.orchestratorTarget,
983
+ message: formatControlIntercomMessage(input.event, input.childIntercomTarget),
984
+ },
985
+ }
986
+ : {}),
987
+ };
988
+ try {
989
+ fs.appendFileSync(path.join(input.job.asyncDir, "events.jsonl"), `${JSON.stringify(record)}\n`, "utf-8");
990
+ } catch (error) {
991
+ if (!isStorageCapacityError(error)) throw error;
992
+ console.error("Failed to append async workflow control event while storage is full:", error);
993
+ }
994
+ }
995
+
953
996
  function emitControlNotification(input: {
954
997
  pi: ExtensionAPI;
955
998
  controlConfig: ResolvedControlConfig;
956
999
  intercomBridge: IntercomBridgeState;
957
1000
  event: ControlEvent;
1001
+ source?: "foreground" | "async";
958
1002
  }): void {
959
1003
  if (!shouldNotifyControlEvent(input.controlConfig, input.event)) return;
960
1004
  const childIntercomTarget = input.intercomBridge.active
@@ -962,7 +1006,7 @@ function emitControlNotification(input: {
962
1006
  : undefined;
963
1007
  const payload = {
964
1008
  event: input.event,
965
- source: "foreground" as const,
1009
+ source: input.source ?? "foreground",
966
1010
  childIntercomTarget,
967
1011
  noticeText: formatControlNoticeMessage(input.event, childIntercomTarget),
968
1012
  };
@@ -995,9 +1039,9 @@ function interruptAsyncRun(
995
1039
  };
996
1040
  }
997
1041
  const activeSteps = status.steps?.filter((step) => step.status === "running") ?? [];
998
- if (activeSteps.length > 0 && activeSteps.every((step) => step.runner?.type === "external-cli")) {
1042
+ if (activeSteps.length > 0 && activeSteps.every((step) => step.runner?.type === "external-cli" || step.runner?.type === "external-job")) {
999
1043
  return {
1000
- content: [{ type: "text", text: `Interrupt is unsupported for one-shot external CLI async run ${target.asyncId}; use stop instead.` }],
1044
+ content: [{ type: "text", text: `Interrupt is unsupported for external async run ${target.asyncId}; use stop instead.` }],
1001
1045
  isError: true,
1002
1046
  details: { mode: "management", results: [] },
1003
1047
  };
@@ -1214,7 +1258,7 @@ function appendStepToAsyncChain(input: {
1214
1258
  const startIndex = (status.chainStepCount ?? status.steps?.length ?? 0) + pendingAppendRequests.reduce((total, request) => total + request.steps.length, 0);
1215
1259
  const appendPaths = chain.flatMap((step, localIndex) => {
1216
1260
  const absoluteIndex = startIndex + localIndex;
1217
- if (isDynamicParallelStep(step) || "checkpoint" in step) return [];
1261
+ if (isDynamicParallelStep(step)) return [];
1218
1262
  if (isParallelStep(step)) return step.parallel.map((_, itemIndex) => `chain[${absoluteIndex}].parallel[${itemIndex}]`);
1219
1263
  return [`chain[${absoluteIndex}]`];
1220
1264
  });
@@ -1372,12 +1416,12 @@ async function directNestedAsyncSteer(input: { target: ResolvedSubagentRunId & {
1372
1416
  const targets = targetIndexes.map((index) => ({ index, state: steps[index]?.status === "pending" ? "scheduled" as const : "pending" as const }));
1373
1417
  if (targets.every((target) => target.state === "scheduled")) {
1374
1418
  const scheduled = { requestId, state: "scheduled" as const, deliveryStatus: "queued" as const, sourceRunId: run.id, targets };
1375
- return { content: [{ type: "text", text: `Steering scheduled for nested async run ${run.id} (request ${requestId}).` }], details: { mode: "management", results: [], steering: scheduled } };
1419
+ return { content: [{ type: "text", text: steeringReceipt(input.message, `Steering scheduled for nested async run ${run.id} (request ${requestId}).`) }], details: { mode: "management", results: [], steering: scheduled } };
1376
1420
  }
1377
1421
  const waited = await waitForSteeringAction(omitUndefinedProperties({ asyncDir, sourceRunId: run.id, requestId, timeoutMs: 3_000, signal: input.signal }));
1378
1422
  const result = waited ?? { requestId, state: "pending" as const, deliveryStatus: "queued" as const, sourceRunId: run.id, targets };
1379
1423
  const stateText = result.state === "failed" ? "failed" : result.state === "partial" ? "partial" : result.deliveryStatus === "queued" ? "queued" : result.state === "delivered" ? "delivered" : "pending";
1380
- 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 } };
1424
+ return { content: [{ type: "text", text: steeringReceipt(input.message, `Steering ${stateText} for nested async run ${run.id} (request ${requestId}).`) }], ...(result.state === "failed" || result.state === "partial" ? { isError: true } : {}), details: { mode: "management", results: [], steering: result } };
1381
1425
  }
1382
1426
 
1383
1427
  async function interruptNestedRun(target: ResolvedSubagentRunId & { kind: "nested" }): Promise<AgentToolResult<Details>> {
@@ -1409,10 +1453,10 @@ async function steerNestedRun(input: { target: ResolvedSubagentRunId & { kind: "
1409
1453
 
1410
1454
  function externalRunnerControlError(asyncDir: string, action: "steer" | "resume"): AgentToolResult<Details> | undefined {
1411
1455
  const status = readStatus(asyncDir);
1412
- if (!status?.steps?.length || !status.steps.every((step) => step.runner?.type === "external-cli")) return undefined;
1456
+ if (!status?.steps?.length || !status.steps.every((step) => step.runner?.type === "external-cli" || step.runner?.type === "external-job")) return undefined;
1413
1457
  const message = action === "steer"
1414
- ? "One-shot external CLI runners do not accept live steer messages."
1415
- : "One-shot external CLI runners do not persist sessions and cannot be resumed.";
1458
+ ? "External runners do not accept live steer messages."
1459
+ : "External runners do not persist Pi sessions and cannot be resumed.";
1416
1460
  return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
1417
1461
  }
1418
1462
 
@@ -1565,7 +1609,7 @@ async function resumeAsyncRun(input: {
1565
1609
  details: { mode: "chain", results: [] },
1566
1610
  };
1567
1611
  }
1568
- const runId = randomUUID().slice(0, 8);
1612
+ const runId = randomUUID();
1569
1613
  const topLevelResume = depth === 0 && !resolveInheritedNestedRouteFromEnv() && !input.params.workflowParentRunId;
1570
1614
  let activeAsyncCapacity: ActiveAsyncCapacityHandle | undefined;
1571
1615
  try {
@@ -1657,10 +1701,10 @@ async function resumeAsyncRun(input: {
1657
1701
  if (!revivalSessionFile) {
1658
1702
  return { content: [{ type: "text", text: `Async child '${target.runId}' has no persisted session file to resume.` }], isError: true, details: { mode: "management", results: [] } };
1659
1703
  }
1660
- if (target.source === "async" && !recoveryDescriptor) {
1704
+ if (target.source === "async" && asyncReviveRequiresRecoveryDescriptor(target)) {
1661
1705
  return { content: [{ type: "text", text: `Async child '${target.runId}' is missing its required run fan-out recovery identity. Start a new run instead.` }], isError: true, details: { mode: "management", results: [] } };
1662
1706
  }
1663
- const runId = randomUUID().slice(0, 8);
1707
+ const runId = randomUUID();
1664
1708
  const topLevelResume = depth === 0 && !resolveInheritedNestedRouteFromEnv() && !input.params.workflowParentRunId;
1665
1709
  let activeAsyncCapacity: ActiveAsyncCapacityHandle | undefined;
1666
1710
  try {
@@ -1722,6 +1766,7 @@ async function resumeAsyncRun(input: {
1722
1766
  ...(input.deps.state.currentSessionId ? { parentSessionId: input.deps.state.currentSessionId } : {}),
1723
1767
  },
1724
1768
  modelOverride: recoveryDescriptor?.model ?? target.model,
1769
+ modelOverrideFromParent: recoveryDescriptor?.modelOverrideFromParent,
1725
1770
  thinkingOverride: recoveryDescriptor?.thinking ?? target.thinking,
1726
1771
  outputBaseDir: resolveSingleRunOutputBaseDir(input.deps, artifactsDir, runId),
1727
1772
  maxSubagentDepth: recoveryDescriptor?.maxSubagentDepth ?? resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth),
@@ -1729,6 +1774,7 @@ async function resumeAsyncRun(input: {
1729
1774
  worktreeSetupHook: input.deps.config.worktreeSetupHook,
1730
1775
  worktreeSetupHookTimeoutMs: input.deps.config.worktreeSetupHookTimeoutMs,
1731
1776
  worktreeBaseDir: input.deps.config.worktreeBaseDir,
1777
+ worktree: input.params.worktree === true,
1732
1778
  controlConfig: recoveryDescriptor?.controlConfig ?? resolveControlConfig(input.deps.config.control, input.params.control),
1733
1779
  intercomBridge: input.params.intercomBridge ?? recoveryDescriptor?.intercomBridge,
1734
1780
  controlIntercomTarget: intercomBridge.active ? intercomBridge.orchestratorTarget : undefined,
@@ -1772,7 +1818,7 @@ async function resumeAsyncRun(input: {
1772
1818
  const sourceStatus = readStatus(sourceAsyncDir);
1773
1819
  if (sourceStatus?.steering) {
1774
1820
  for (const brief of queuedBriefs) updateSteeringTarget(sourceStatus.steering, brief.request.id, target.index, "delivered", Date.now());
1775
- writeAtomicJson(path.join(sourceAsyncDir, "status.json"), sourceStatus);
1821
+ createCapacityResilientJsonWriter({ keepAlive: true }).write(path.join(sourceAsyncDir, "status.json"), sourceStatus);
1776
1822
  }
1777
1823
  }
1778
1824
 
@@ -1870,14 +1916,32 @@ function formatFailedSingleRunOutput(result: SingleResult, displayOutput: string
1870
1916
  return lines.join("\n");
1871
1917
  }
1872
1918
 
1873
- function createForegroundControlNotifier(data: Pick<ExecutionContextData, "controlConfig" | "intercomBridge">, deps: Pick<ExecutorDeps, "pi" | "state">): (event: ControlEvent) => void {
1919
+ function createForegroundControlNotifier(data: Pick<ExecutionContextData, "controlConfig" | "intercomBridge" | "params">, deps: Pick<ExecutorDeps, "pi" | "state">): (event: ControlEvent) => void {
1874
1920
  return (event) => {
1875
1921
  applyControlEventToRememberedForegroundRun(deps.state, event);
1922
+ const parentWorkflowRunId = data.params.workflowParentRunId;
1923
+ const asyncWorkflow = typeof parentWorkflowRunId === "string" ? deps.state.asyncJobs.get(parentWorkflowRunId) : undefined;
1924
+ const workflowKey = typeof data.params.workflowKey === "string" && data.params.workflowKey.trim()
1925
+ ? data.params.workflowKey.trim()
1926
+ : undefined;
1927
+ const enriched = workflowKey && !event.workflowKey ? { ...event, workflowKey } : event;
1928
+ if (asyncWorkflow) {
1929
+ persistAsyncWorkflowControlEvent({
1930
+ job: asyncWorkflow,
1931
+ event: enriched,
1932
+ controlConfig: data.controlConfig,
1933
+ intercomBridge: data.intercomBridge,
1934
+ childIntercomTarget: data.intercomBridge.active
1935
+ ? resolveSubagentIntercomTarget(enriched.runId, enriched.agent, enriched.index)
1936
+ : undefined,
1937
+ });
1938
+ }
1876
1939
  emitControlNotification({
1877
1940
  pi: deps.pi,
1878
1941
  controlConfig: data.controlConfig,
1879
1942
  intercomBridge: data.intercomBridge,
1880
- event,
1943
+ event: enriched,
1944
+ source: asyncWorkflow ? "async" : "foreground",
1881
1945
  });
1882
1946
  };
1883
1947
  }
@@ -1963,16 +2027,19 @@ async function maybeBuildForegroundIntercomReceipt(input: {
1963
2027
  };
1964
2028
  }
1965
2029
 
1966
- function canonicalizeAgentName(name: string, agents: AgentConfig[]): { name?: string; error?: string } {
2030
+ function canonicalizeAgentName(name: string, agents: AgentConfig[], diagnostics?: AgentDiscoveryDiagnostic[]): { name?: string; error?: string } {
1967
2031
  const resolved = resolveAgentName(name, agents);
2032
+ const candidates = resolved.error ? agents.filter((agent) => resolveAgentName(name, [agent]).agent) : resolved.agent;
2033
+ const diagnostic = findBlockingAgentDiagnostic(name, candidates, diagnostics);
2034
+ if (diagnostic) return { error: `Agent '${name}' has invalid configuration: ${diagnostic.error}` };
1968
2035
  if (resolved.error) return { error: resolved.error };
1969
2036
  if (!resolved.agent) return { error: `Unknown agent: ${name}` };
1970
2037
  return { name: resolved.agent.name };
1971
2038
  }
1972
2039
 
1973
- function canonicalizeExecutionParams(params: SubagentParamsLike, agents: AgentConfig[]): { params?: SubagentParamsLike; error?: string } {
2040
+ function canonicalizeExecutionParams(params: SubagentParamsLike, agents: AgentConfig[], diagnostics?: AgentDiscoveryDiagnostic[]): { params?: SubagentParamsLike; error?: string } {
1974
2041
  const resolve = (name: string, location?: string): { name?: string; error?: string } => {
1975
- const result = canonicalizeAgentName(name, agents);
2042
+ const result = canonicalizeAgentName(name, agents, diagnostics);
1976
2043
  return result.error && location ? { error: `${result.error} (${location})` } : result;
1977
2044
  };
1978
2045
  if (params.agent) {
@@ -2176,13 +2243,21 @@ interface AgentDefaultContextPolicy {
2176
2243
  usesFork: boolean;
2177
2244
  }
2178
2245
 
2179
- function resolveAgentDefaultContextPolicy(params: SubagentParamsLike, agents: AgentConfig[]): AgentDefaultContextPolicy {
2180
- if (params.context !== undefined) {
2181
- return resolveExplicitContextPolicy(params);
2182
- }
2246
+ function resolveAgentDefaultContextPolicy(
2247
+ params: SubagentParamsLike,
2248
+ agents: AgentConfig[],
2249
+ defaultSubagentContext: ExtensionConfig["defaultSubagentContext"],
2250
+ canUseDefaultFork = false,
2251
+ ): AgentDefaultContextPolicy {
2252
+ if (params.context !== undefined) return resolveExplicitContextPolicy(params);
2183
2253
  const byName = new Map(agents.map((agent) => [agent.name, agent]));
2184
2254
  const contextForAgent = (agentName: string): ContextMode =>
2185
- byName.get(agentName)?.defaultContext === "fork" ? "fork" : "fresh";
2255
+ resolveSubagentLaunchContext({
2256
+ explicitContext: params.context,
2257
+ agentDefaultContext: byName.get(agentName)?.defaultContext,
2258
+ defaultSubagentContext,
2259
+ canUseImplicitFork: canUseDefaultFork,
2260
+ });
2186
2261
  const requestedAgentNames = collectRequestedAgentNames(params);
2187
2262
  const contextSummary = summarizeContextModes(requestedAgentNames.map((name) => contextForAgent(name)));
2188
2263
  const usesFork = contextSummary === "fork" || contextSummary === "mixed";
@@ -2195,7 +2270,10 @@ function resolveAgentDefaultContextPolicy(params: SubagentParamsLike, agents: Ag
2195
2270
  }
2196
2271
 
2197
2272
  function resolveExplicitContextPolicy(params: SubagentParamsLike): AgentDefaultContextPolicy {
2198
- const context = params.context === "fork" ? "fork" : "fresh";
2273
+ const context = resolveSubagentLaunchContext({
2274
+ explicitContext: params.context,
2275
+ canUseImplicitFork: false,
2276
+ });
2199
2277
  return {
2200
2278
  params,
2201
2279
  contextForAgent: () => context,
@@ -2235,12 +2313,16 @@ function applySingleAgentLaunchDefaults(params: SubagentParamsLike, agents: Agen
2235
2313
  if ((params.chain?.length ?? 0) > 0 || (params.tasks?.length ?? 0) > 0 || !params.agent) return params;
2236
2314
  const agent = agents.find((candidate) => candidate.name === params.agent);
2237
2315
  if (!agent) return params;
2316
+ const parentTimeoutMs = params.timeoutMs === undefined && params.maxRuntimeMs === undefined && agent.defaultTimeoutMs === undefined && params.workflowParentDeadlineAt !== undefined
2317
+ ? Math.max(1, params.workflowParentDeadlineAt - Date.now())
2318
+ : undefined;
2238
2319
  return {
2239
2320
  ...params,
2240
2321
  ...(params.async === undefined && agent.defaultAsync !== undefined ? { async: agent.defaultAsync } : {}),
2241
2322
  ...(params.timeoutMs === undefined && params.maxRuntimeMs === undefined && agent.defaultTimeoutMs !== undefined
2242
2323
  ? { timeoutMs: agent.defaultTimeoutMs }
2243
2324
  : {}),
2325
+ ...(parentTimeoutMs !== undefined ? { timeoutMs: parentTimeoutMs } : {}),
2244
2326
  ...(params.turnBudget === undefined && agent.defaultTurnBudget !== undefined
2245
2327
  ? { turnBudget: agent.defaultTurnBudget }
2246
2328
  : {}),
@@ -2511,7 +2593,7 @@ function resolveStaticLaunchSummary(input: {
2511
2593
  thinkingOverrideForTask: ForkThinkingOverrideForTask;
2512
2594
  }): StaticLaunchSummary {
2513
2595
  const agentConfig = input.agents.find((agent) => agent.name === input.agent);
2514
- const externalRunner = agentConfig?.runner?.type === "external-cli";
2596
+ const externalRunner = agentConfig?.runner?.type === "external-cli" || agentConfig?.runner?.type === "external-job";
2515
2597
  const model = externalRunner
2516
2598
  ? undefined
2517
2599
  : resolveEffectiveSubagentModel(
@@ -2704,27 +2786,6 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2704
2786
  const hasSingle = !hasChain && !hasTasks && Boolean(params.agent);
2705
2787
  if (!effectiveAsync) return null;
2706
2788
 
2707
- if (hasChain && params.chain) {
2708
- const chainWorktreeTaskCwdError = buildChainWorktreeTaskCwdError(params.chain as ChainStep[], effectiveCwd);
2709
- if (chainWorktreeTaskCwdError) {
2710
- return {
2711
- content: [{ type: "text", text: chainWorktreeTaskCwdError }],
2712
- isError: true,
2713
- details: { mode: "chain" as const, results: [] },
2714
- };
2715
- }
2716
- }
2717
-
2718
- if (hasTasks && params.tasks) {
2719
- const maxParallelTasks = resolveTopLevelParallelMaxTasks(deps.config.parallel?.maxTasks);
2720
- if (params.tasks.length > maxParallelTasks) {
2721
- return buildParallelModeError(`Max ${maxParallelTasks} tasks`);
2722
- }
2723
- if (params.worktree) {
2724
- const worktreeTaskCwdError = buildParallelWorktreeTaskCwdError(params.tasks, effectiveCwd);
2725
- if (worktreeTaskCwdError) return buildParallelModeError(worktreeTaskCwdError);
2726
- }
2727
- }
2728
2789
 
2729
2790
  if (!isAsyncAvailable()) {
2730
2791
  return {
@@ -2752,118 +2813,6 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2752
2813
  const controlIntercomTarget = intercomBridge.active ? intercomBridge.orchestratorTarget : undefined;
2753
2814
  const childIntercomTarget = intercomBridge.active ? (agent: string, index: number) => resolveSubagentIntercomTarget(id, agent, index) : undefined;
2754
2815
 
2755
- if (hasTasks && params.tasks) {
2756
- const skillOverrides = params.tasks.map((task) => normalizeSkillInput(task.skill));
2757
- const parallelTasks = params.tasks.map((task, index) => compactOptional<ParallelTaskItem>({
2758
- agent: task.agent,
2759
- task: shouldForkAgent(contextPolicy, task.agent) ? wrapForkTask(task.task) : task.task,
2760
- cwd: task.cwd,
2761
- ...(task.model !== undefined ? { model: task.model } : {}),
2762
- ...(skillOverrides[index] !== undefined ? { skill: skillOverrides[index] } : {}),
2763
- ...(task.output !== undefined && task.output !== true ? { output: task.output } : {}),
2764
- ...(task.outputMode !== undefined ? { outputMode: task.outputMode } : {}),
2765
- ...(task.reads !== undefined && task.reads !== true ? { reads: task.reads } : {}),
2766
- ...(task.progress !== undefined ? { progress: task.progress } : {}),
2767
- ...(task.toolBudget !== undefined ? { toolBudget: task.toolBudget } : {}),
2768
- ...(task.outputSchema !== undefined ? { outputSchema: task.outputSchema } : {}),
2769
- ...(task.agentContract !== undefined ? { agentContract: task.agentContract } : {}),
2770
- ...(task.acceptance !== undefined ? { acceptance: task.acceptance } : {}),
2771
- }));
2772
- return executeAsyncChain(id, compactOptional<Parameters<typeof executeAsyncChain>[1]>({
2773
- chain: [compactOptional<ParallelStep>({
2774
- parallel: parallelTasks,
2775
- concurrency: resolveTopLevelParallelConcurrency(params.concurrency, deps.config.parallel?.concurrency),
2776
- worktree: params.worktree,
2777
- })],
2778
- resultMode: "parallel",
2779
- goal: params.tasks[0]?.task ?? "",
2780
- agents,
2781
- ctx: asyncCtx,
2782
- availableModels,
2783
- cwd: effectiveCwd,
2784
- maxOutput: params.maxOutput,
2785
- artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
2786
- artifactConfig,
2787
- shareEnabled,
2788
- sessionRoot,
2789
- chainSkills: [],
2790
- activeAsyncCapacity: data.activeAsyncCapacity,
2791
- sessionFilesByFlatIndex: params.tasks.map((task, index) => sessionFileForTask(task.agent, index, task.model)),
2792
- thinkingOverridesByFlatIndex: params.tasks.map((task, index) => thinkingOverrideForTask(task.agent, index, task.model)),
2793
- contextForAgent: contextPolicy.contextForAgent,
2794
- maxSubagentDepth: currentMaxSubagentDepth,
2795
- waitToolEnabled: deps.waitToolEnabled,
2796
- worktreeSetupHook: deps.config.worktreeSetupHook,
2797
- worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
2798
- worktreeBaseDir: deps.config.worktreeBaseDir,
2799
- controlConfig,
2800
- agentContract: params.agentContract,
2801
- controlIntercomTarget,
2802
- childIntercomTarget,
2803
- nestedRoute,
2804
- timeoutMs: data.timeoutMs,
2805
- turnBudget: data.turnBudget,
2806
- toolBudget: data.toolBudget,
2807
- usageBudget: data.usageBudget,
2808
- configToolBudget: data.configToolBudget,
2809
- callToolTimeoutMs: data.params?.toolTimeoutMs,
2810
- configToolTimeoutMs: data.configToolTimeoutMs,
2811
- capabilityCeiling: data.capabilityCeiling,
2812
- runFanoutBudget: data.runFanoutBudget,
2813
- globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2814
- parentWorkflowRunId: params.workflowParentRunId,
2815
- workflowKey: params.workflowKey,
2816
- }));
2817
- }
2818
-
2819
- if (hasChain && params.chain) {
2820
- const normalized = normalizeSkillInput(params.skill);
2821
- const chainSkills = normalized === false ? [] : (normalized ?? []);
2822
- const rawChain = params.chain as ChainStep[];
2823
- const chain = wrapChainTasksForFork(rawChain, contextPolicy);
2824
- return executeAsyncChain(id, compactOptional<Parameters<typeof executeAsyncChain>[1]>({
2825
- chain,
2826
- task: params.task,
2827
- goal: resolveAsyncEventGoal(params.task, rawChain),
2828
- agents,
2829
- ctx: asyncCtx,
2830
- availableModels,
2831
- cwd: effectiveCwd,
2832
- maxOutput: params.maxOutput,
2833
- artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
2834
- artifactConfig,
2835
- shareEnabled,
2836
- sessionRoot,
2837
- chainSkills,
2838
- activeAsyncCapacity: data.activeAsyncCapacity,
2839
- sessionFilesByFlatIndex: collectChainSessionFiles(chain, sessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems),
2840
- thinkingOverridesByFlatIndex: collectChainThinkingOverrides(chain, thinkingOverrideForTask, deps.config.chain?.dynamicFanout?.maxItems),
2841
- contextForAgent: contextPolicy.contextForAgent,
2842
- dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
2843
- maxSubagentDepth: currentMaxSubagentDepth,
2844
- waitToolEnabled: deps.waitToolEnabled,
2845
- worktreeSetupHook: deps.config.worktreeSetupHook,
2846
- worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
2847
- worktreeBaseDir: deps.config.worktreeBaseDir,
2848
- controlConfig,
2849
- agentContract: params.agentContract,
2850
- controlIntercomTarget,
2851
- childIntercomTarget,
2852
- nestedRoute,
2853
- timeoutMs: data.timeoutMs,
2854
- turnBudget: data.turnBudget,
2855
- toolBudget: data.toolBudget,
2856
- usageBudget: data.usageBudget,
2857
- configToolBudget: data.configToolBudget,
2858
- callToolTimeoutMs: data.params?.toolTimeoutMs,
2859
- configToolTimeoutMs: data.configToolTimeoutMs,
2860
- capabilityCeiling: data.capabilityCeiling,
2861
- runFanoutBudget: data.runFanoutBudget,
2862
- globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2863
- parentWorkflowRunId: params.workflowParentRunId,
2864
- workflowKey: params.workflowKey,
2865
- }));
2866
- }
2867
2816
 
2868
2817
  if (hasSingle) {
2869
2818
  const a = agents.find((x) => x.name === params.agent);
@@ -2880,12 +2829,13 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2880
2829
  const normalizedSkills = normalizeSkillInput(params.skill);
2881
2830
  const skills = normalizedSkills === false ? [] : normalizedSkills;
2882
2831
  const maxSubagentDepth = resolveChildMaxSubagentDepth(currentMaxSubagentDepth, a.maxSubagentDepth);
2883
- const externalRunnerWithoutExplicitModel = a.runner?.type === "external-cli"
2832
+ const externalRunnerWithoutExplicitModel = (a.runner?.type === "external-cli" || a.runner?.type === "external-job")
2884
2833
  && params.model === undefined
2885
2834
  && (a.model === undefined || (a.modelSource?.type === "subagents.defaultModel" && a.model === a.modelSource.model));
2886
- const modelOverride = a.runner?.type === "external-cli"
2835
+ const modelOverride = a.runner?.type === "external-cli" || a.runner?.type === "external-job"
2887
2836
  ? params.model ?? (externalRunnerWithoutExplicitModel ? undefined : a.model)
2888
2837
  : resolveEffectiveSubagentModel(params.model as string | undefined, a.model, parentModel, availableModels, currentProvider, data.modelScope === undefined ? {} : { scope: data.modelScope });
2838
+ const modelOverrideFromParent = inheritsParentModel(params.model as string | undefined, a.model, parentModel);
2889
2839
  return executeAsyncSingle(id, compactOptional<Parameters<typeof executeAsyncSingle>[1]>({
2890
2840
  agent: params.agent!,
2891
2841
  task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(params.task ?? "") : (params.task ?? ""),
@@ -2901,7 +2851,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2901
2851
  shareEnabled,
2902
2852
  activeAsyncCapacity: data.activeAsyncCapacity,
2903
2853
  sessionRoot,
2904
- sessionFile: sessionFileForTask(params.agent!, 0, modelOverride),
2854
+ sessionFile: sessionFileForTask(params.agent!, 0, modelOverride, modelOverrideFromParent),
2905
2855
  context: contextPolicy.contextForAgent(params.agent!),
2906
2856
  skills,
2907
2857
  output: effectiveOutput,
@@ -2909,9 +2859,11 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2909
2859
  ...(params.reads !== undefined ? { reads: params.reads } : {}),
2910
2860
  outputBaseDir: resolveSingleRunOutputBaseDir(deps, artifactsDir, id),
2911
2861
  modelOverride,
2912
- thinkingOverride: externalRunnerWithoutExplicitModel ? undefined : thinkingOverrideForTask(params.agent!, 0, modelOverride),
2862
+ modelOverrideFromParent,
2863
+ thinkingOverride: externalRunnerWithoutExplicitModel ? undefined : thinkingOverrideForTask(params.agent!, 0, modelOverride, modelOverrideFromParent),
2913
2864
  maxSubagentDepth,
2914
2865
  waitToolEnabled: deps.waitToolEnabled,
2866
+ ...(params.worktree === true ? { worktree: true } : {}),
2915
2867
  worktreeSetupHook: deps.config.worktreeSetupHook,
2916
2868
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
2917
2869
  worktreeBaseDir: deps.config.worktreeBaseDir,
@@ -2940,308 +2892,11 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2940
2892
  return null;
2941
2893
  }
2942
2894
 
2943
- async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Promise<AgentToolResult<Details>> {
2944
- const {
2945
- params,
2946
- effectiveCwd,
2947
- agents,
2948
- ctx,
2949
- signal,
2950
- runId,
2951
- shareEnabled,
2952
- sessionDirForIndex,
2953
- sessionFileForIndex,
2954
- sessionFileForTask,
2955
- thinkingOverrideForTask,
2956
- artifactsDir,
2957
- artifactConfig,
2958
- onUpdate,
2959
- sessionRoot,
2960
- controlConfig,
2961
- contextPolicy,
2962
- } = data;
2963
- const onControlEvent = createForegroundControlNotifier(data, deps);
2964
- const childIntercomTarget = data.intercomBridge.active ? resolveSubagentIntercomTarget : undefined;
2965
- const foregroundControl = deps.state.foregroundControls.get(runId);
2966
- const normalized = normalizeSkillInput(params.skill);
2967
- const chainSkills = normalized === false ? [] : (normalized ?? []);
2968
- const chain = wrapChainTasksForFork(params.chain as ChainStep[], contextPolicy);
2969
- const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
2970
- const chainCtx = normalizeParentModel(ctx.model) || !data.parentModel ? ctx : ({ ...ctx, model: data.parentModel } as ExtensionContext);
2971
- const chainResult = await executeChain(compactOptional<Parameters<typeof executeChain>[0]>({
2972
- chain,
2973
- task: params.task,
2974
- agents,
2975
- ctx: chainCtx,
2976
- modelScope: data.modelScope,
2977
- intercomEvents: deps.pi.events,
2978
- signal,
2979
- runId,
2980
- cwd: effectiveCwd,
2981
- shareEnabled,
2982
- sessionDirForIndex,
2983
- sessionFileForIndex,
2984
- sessionFileForTask,
2985
- thinkingOverrideForTask,
2986
- contextForAgent: contextPolicy.contextForAgent,
2987
- artifactsDir,
2988
- artifactConfig,
2989
- includeProgress: params.includeProgress,
2990
- clarify: params.clarify,
2991
- onUpdate,
2992
- onControlEvent,
2993
- controlConfig,
2994
- agentContract: params.agentContract,
2995
- childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(runId, agent, index) : undefined,
2996
- orchestratorIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
2997
- foregroundControl,
2998
- nestedRoute: foregroundControl?.nestedRoute,
2999
- chainSkills,
3000
- chainDir: params.chainDir ?? getChainRunsDir(effectiveCwd, artifactConfig.dir),
3001
- dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
3002
- maxSubagentDepth: currentMaxSubagentDepth,
3003
- worktreeSetupHook: deps.config.worktreeSetupHook,
3004
- worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
3005
- worktreeBaseDir: deps.config.worktreeBaseDir,
3006
- timeoutMs: data.timeoutMs,
3007
- deadlineAt: data.deadlineAt,
3008
- turnBudget: data.turnBudget,
3009
- onDetachedExit: (index, result) => {
3010
- try {
3011
- updateRememberedForegroundChild(deps.state, { runId, mode: "chain", cwd: effectiveCwd, sessionId: data.parentSessionId, index, result, events: deps.pi.events });
3012
- } finally {
3013
- removeForegroundControlIfIdle(deps.state, runId);
3014
- }
3015
- },
3016
- onForegroundChildSettled: () => {
3017
- removeForegroundControlIfIdle(deps.state, runId);
3018
- },
3019
- toolBudget: data.toolBudget,
3020
- usageBudget: data.usageBudget,
3021
- configToolBudget: data.configToolBudget,
3022
- callToolTimeoutMs: data.params?.toolTimeoutMs,
3023
- configToolTimeoutMs: data.configToolTimeoutMs,
3024
- permissions: deps.config.permissions,
3025
- globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
3026
- capabilityCeiling: data.capabilityCeiling,
3027
- runFanoutBudget: data.runFanoutBudget,
3028
- }));
3029
-
3030
- if (chainResult.requestedAsync) {
3031
- if (!isAsyncAvailable()) {
3032
- return {
3033
- content: [{ type: "text", text: "Background mode requires upstream jiti for TypeScript execution but it could not be found. Ensure the pi-subagents package dependencies are installed." }],
3034
- isError: true,
3035
- details: { mode: "chain" as const, results: [] },
3036
- };
3037
- }
3038
- const id = data.asyncRunId;
3039
- const capacity = acquireClarifiedBackgroundCapacity(data, deps, "chain", id);
3040
- if (capacity.errorResult) return capacity.errorResult;
3041
- const parentModel = data.parentModel;
3042
- const asyncCtx = compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
3043
- pi: deps.pi,
3044
- cwd: ctx.cwd,
3045
- currentSessionId: data.parentSessionId!,
3046
- parentSessionId: data.parentPiSessionId,
3047
- currentModelProvider: parentModel?.provider,
3048
- currentModel: parentModel,
3049
- modelScope: data.modelScope,
3050
- interactive: ctx.hasUI,
3051
- permissions: deps.config.permissions,
3052
- });
3053
- const rawAsyncChain = chainResult.requestedAsync.chain;
3054
- const asyncChain = wrapChainTasksForFork(rawAsyncChain, contextPolicy);
3055
- const firstAgent = firstChainAgent(rawAsyncChain);
3056
- try {
3057
- return executeAsyncChain(id, compactOptional<Parameters<typeof executeAsyncChain>[1]>({
3058
- chain: asyncChain,
3059
- task: params.task,
3060
- goal: resolveAsyncEventGoal(params.task, rawAsyncChain, firstAgent ? shouldForkAgent(contextPolicy, firstAgent) : false),
3061
- agents,
3062
- ctx: asyncCtx,
3063
- availableModels: ctx.modelRegistry.getAvailable().map(toModelInfo),
3064
- cwd: effectiveCwd,
3065
- maxOutput: params.maxOutput,
3066
- artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
3067
- artifactConfig,
3068
- shareEnabled,
3069
- sessionRoot,
3070
- chainSkills: chainResult.requestedAsync.chainSkills,
3071
- activeAsyncCapacity: capacity.activeAsyncCapacity,
3072
- sessionFilesByFlatIndex: collectChainSessionFiles(asyncChain, sessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems),
3073
- thinkingOverridesByFlatIndex: collectChainThinkingOverrides(asyncChain, thinkingOverrideForTask, deps.config.chain?.dynamicFanout?.maxItems),
3074
- contextForAgent: contextPolicy.contextForAgent,
3075
- dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
3076
- maxSubagentDepth: currentMaxSubagentDepth,
3077
- waitToolEnabled: deps.waitToolEnabled,
3078
- worktreeSetupHook: deps.config.worktreeSetupHook,
3079
- worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
3080
- worktreeBaseDir: deps.config.worktreeBaseDir,
3081
- controlConfig,
3082
- agentContract: params.agentContract,
3083
- controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
3084
- childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
3085
- nestedRoute: data.nestedRoute,
3086
- timeoutMs: data.timeoutMs,
3087
- turnBudget: data.turnBudget,
3088
- toolBudget: data.toolBudget,
3089
- usageBudget: data.usageBudget,
3090
- configToolBudget: data.configToolBudget,
3091
- callToolTimeoutMs: data.params?.toolTimeoutMs,
3092
- configToolTimeoutMs: data.configToolTimeoutMs,
3093
- capabilityCeiling: data.capabilityCeiling,
3094
- runFanoutBudget: data.runFanoutBudget,
3095
- globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
3096
- }));
3097
- } finally {
3098
- finalizeClarifiedBackgroundCapacity(data, deps, capacity.activeAsyncCapacity);
3099
- }
3100
- }
3101
-
3102
- const rawChainDetails = chainResult.details ? compactOptional<Details>({ ...chainResult.details, runId, timeoutMs: data.timeoutMs }) : undefined;
3103
- if (foregroundControl && rawChainDetails) {
3104
- updateForegroundNestedProjection(foregroundControl);
3105
- attachRootChildrenToSteps(runId, rawChainDetails.results, foregroundControl.nestedChildren);
3106
- rawChainDetails.totalCost = sumResultsCost(rawChainDetails.results);
3107
- const usageBudget = usageBudgetState(data.usageBudget, rawChainDetails.totalCost);
3108
- if (usageBudget === undefined) delete rawChainDetails.usageBudget;
3109
- else rawChainDetails.usageBudget = usageBudget;
3110
- }
3111
- const chainDetails = rawChainDetails ? compactForegroundDetails(rawChainDetails) : undefined;
3112
- if (chainDetails) rememberForegroundRun(deps.state, { runId, mode: "chain", cwd: effectiveCwd, sessionId: data.parentSessionId, results: chainDetails.results, checkpoint: chainDetails.checkpoint });
3113
- const intercomReceipt = chainDetails && !chainDetails.results.some((result) => result.interrupted || result.detached)
3114
- ? await maybeBuildForegroundIntercomReceipt({
3115
- pi: deps.pi,
3116
- intercomBridge: data.intercomBridge,
3117
- runId,
3118
- mode: "chain",
3119
- details: chainDetails,
3120
- ...(foregroundControl?.nestedChildren?.length ? { nestedChildren: foregroundControl.nestedChildren } : {}),
3121
- })
3122
- : null;
3123
- if (intercomReceipt) {
3124
- return {
3125
- ...chainResult,
3126
- content: [{ type: "text", text: intercomReceipt.text }],
3127
- details: intercomReceipt.details,
3128
- };
3129
- }
3130
-
3131
- return chainDetails ? { ...chainResult, details: chainDetails } : chainResult;
3132
- }
3133
-
3134
- interface ForegroundParallelRunInput {
3135
- tasks: TaskParam[];
3136
- taskTexts: string[];
3137
- taskDescriptions: string[];
3138
- agents: AgentConfig[];
3139
- ctx: ExtensionContext;
3140
- state: SubagentState;
3141
- intercomEvents: IntercomEventBus;
3142
- parentSessionId: string | null;
3143
- capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
3144
- runFanoutBudget: RunFanoutBudgetDescriptor;
3145
- signal: AbortSignal;
3146
- runId: string;
3147
- sessionDirForIndex: (idx?: number) => string | undefined;
3148
- sessionFileForIndex: (idx?: number) => string | undefined;
3149
- sessionFileForTask: ForkSessionFileForTask;
3150
- thinkingOverrideForTask: ForkThinkingOverrideForTask;
3151
- shareEnabled: boolean;
3152
- artifactConfig: ArtifactConfig;
3153
- artifactsDir: string;
3154
- outputBaseDir: string;
3155
- maxOutput?: MaxOutputConfig;
3156
- paramsCwd: string;
3157
- progressDir: string;
3158
- maxSubagentDepths: number[];
3159
- waitToolEnabled?: boolean;
3160
- availableModels: ModelInfo[];
3161
- modelScope?: ModelScopeConfig;
3162
- parentModel?: ParentModel;
3163
- modelOverrides: (string | undefined)[];
3164
- behaviors: Array<ReturnType<typeof resolveStepBehavior>>;
3165
- firstProgressIndex: number;
3166
- controlConfig: ResolvedControlConfig;
3167
- contextPolicy: AgentDefaultContextPolicy;
3168
- onControlEvent?: (event: ControlEvent) => void;
3169
- childIntercomTarget?: (agent: string, index: number) => string | undefined;
3170
- orchestratorIntercomTarget?: string;
3171
- foregroundControl?: SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never;
3172
- concurrencyLimit: number;
3173
- globalSemaphore?: Semaphore;
3174
- liveResults: (SingleResult | undefined)[];
3175
- liveProgress: (AgentProgress | undefined)[];
3176
- onUpdate?: (r: AgentToolResult<Details>) => void;
3177
- worktreeSetup?: WorktreeSetup;
3178
- timeoutMs?: number;
3179
- deadlineAt?: number;
3180
- /** Per-call per-tool timeout, resolved separately for each task's agent. */
3181
- toolTimeoutMs?: number;
3182
- /** Raw global config.toolTimeoutMs, used by each task's resolver. */
3183
- configToolTimeoutMs?: number;
3184
- turnBudget?: ResolvedTurnBudget;
3185
- usageBudget?: UsageBudgetConfig;
3186
- toolBudgets: (ResolvedToolBudget | undefined)[];
3187
- agentContract?: AgentContract;
3188
- permissions?: ExtensionConfig["permissions"];
3189
- }
3190
- function buildParallelModeError(message: string): AgentToolResult<Details> {
3191
- return {
3192
- content: [{ type: "text", text: message }],
3193
- isError: true,
3194
- details: { mode: "parallel" as const, results: [] },
3195
- };
3196
- }
3197
-
3198
- function acquireClarifiedBackgroundCapacity(
3199
- data: ExecutionContextData,
3200
- deps: ExecutorDeps,
3201
- mode: "single" | "parallel" | "chain",
3202
- asyncRunId: string,
3203
- ): { activeAsyncCapacity?: ActiveAsyncCapacityHandle; errorResult?: AgentToolResult<Details> } {
3204
- if (!data.topLevelAsyncCapacityEligible || !data.parentSessionId) return {};
3205
- const liveWorkflowRunIds = new Set(deps.state.workflowControllers?.keys() ?? []);
3206
- const activeLimit = resolveMaxActiveAsyncRunsPerSession(deps.config.maxActiveAsyncRunsPerSession);
3207
- try {
3208
- const activeAsyncCapacity = acquireActiveAsyncCapacity({
3209
- sessionId: data.parentSessionId,
3210
- limit: activeLimit,
3211
- runId: asyncRunId,
3212
- kind: "runner",
3213
- asyncDir: path.join(DIRS.async, asyncRunId),
3214
- }, { liveWorkflowRunIds });
3215
- deps.state.activeAsyncCapacity = getActiveAsyncCapacitySnapshot(data.parentSessionId, activeLimit, { liveWorkflowRunIds });
3216
- return { activeAsyncCapacity };
3217
- } catch (error) {
3218
- if (error instanceof ActiveAsyncCapacityError) {
3219
- deps.state.activeAsyncCapacity = error.snapshot;
3220
- return { errorResult: { content: [{ type: "text", text: error.message }], isError: true, details: { mode, results: [], activeAsyncCapacity: error.snapshot } } };
3221
- }
3222
- throw error;
3223
- }
3224
- }
3225
-
3226
- function finalizeClarifiedBackgroundCapacity(
3227
- data: ExecutionContextData,
3228
- deps: ExecutorDeps,
3229
- activeAsyncCapacity: ActiveAsyncCapacityHandle | undefined,
3230
- ): void {
3231
- if (!activeAsyncCapacity || !data.parentSessionId) return;
3232
- if (!activeAsyncCapacity.owner.runnerStartedAt) activeAsyncCapacity.rollback();
3233
- deps.state.activeAsyncCapacity = getActiveAsyncCapacitySnapshot(
3234
- data.parentSessionId,
3235
- resolveMaxActiveAsyncRunsPerSession(deps.config.maxActiveAsyncRunsPerSession),
3236
- { liveWorkflowRunIds: new Set(deps.state.workflowControllers?.keys() ?? []) },
3237
- );
3238
- }
3239
-
3240
- function createParallelWorktreeSetup(
2895
+ function createSingleWorktreeSetup(
3241
2896
  enabled: boolean | undefined,
3242
2897
  cwd: string,
3243
2898
  runId: string,
3244
- tasks: TaskParam[],
2899
+ agent: string,
3245
2900
  setupHook: ExtensionConfig["worktreeSetupHook"],
3246
2901
  setupHookTimeoutMs: ExtensionConfig["worktreeSetupHookTimeoutMs"],
3247
2902
  baseDir: ExtensionConfig["worktreeBaseDir"],
@@ -3249,8 +2904,8 @@ function createParallelWorktreeSetup(
3249
2904
  if (!enabled) return {};
3250
2905
  try {
3251
2906
  return {
3252
- setup: createWorktrees(cwd, runId, tasks.length, omitUndefinedProperties({
3253
- agents: tasks.map((task) => task.agent),
2907
+ setup: createWorktrees(cwd, runId, 1, omitUndefinedProperties({
2908
+ agents: [agent],
3254
2909
  setupHook: setupHook
3255
2910
  ? { hookPath: setupHook, ...(setupHookTimeoutMs === undefined ? {} : { timeoutMs: setupHookTimeoutMs }) }
3256
2911
  : undefined,
@@ -3259,27 +2914,25 @@ function createParallelWorktreeSetup(
3259
2914
  };
3260
2915
  } catch (error) {
3261
2916
  const message = error instanceof Error ? error.message : String(error);
3262
- return { errorResult: buildParallelModeError(message) };
2917
+ return { errorResult: { content: [{ type: "text", text: message }], isError: true, details: { mode: "single", results: [] } } };
3263
2918
  }
3264
2919
  }
3265
2920
 
3266
- function buildParallelWorktreeTaskCwdError(
3267
- tasks: ReadonlyArray<{ agent: string; cwd?: string }>,
3268
- sharedCwd: string,
3269
- ): string | undefined {
3270
- const conflict = findWorktreeTaskCwdConflict(tasks, sharedCwd);
3271
- if (!conflict) return undefined;
3272
- return formatWorktreeTaskCwdConflict(conflict, sharedCwd);
3273
- }
3274
-
3275
2921
  function resolveConfiguredSingleRunOutputBaseDir(deps: ExecutorDeps): string | undefined {
3276
2922
  return deps.config.singleRunOutputBaseDir
3277
2923
  ? path.resolve(deps.expandTilde(deps.config.singleRunOutputBaseDir))
3278
2924
  : undefined;
3279
2925
  }
3280
2926
 
2927
+ export function sanitizeRunPathSegment(value: string, maxBytes = 120): string {
2928
+ const sanitized = value.trim().replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^_+|_+$/g, "");
2929
+ if (!sanitized) return "unknown";
2930
+ if (Buffer.byteLength(sanitized, "utf-8") <= maxBytes) return sanitized;
2931
+ return sanitized.slice(0, maxBytes).replace(/_+$/, "") || "unknown";
2932
+ }
2933
+
3281
2934
  function resolveSingleRunOutputBaseDir(deps: ExecutorDeps, artifactsDir: string, runId: string): string {
3282
- return resolveConfiguredSingleRunOutputBaseDir(deps) ?? path.join(artifactsDir, "outputs", runId);
2935
+ return resolveConfiguredSingleRunOutputBaseDir(deps) ?? path.join(artifactsDir, "outputs", sanitizeRunPathSegment(runId));
3283
2936
  }
3284
2937
 
3285
2938
  function resolveWorkflowAggregateOutputPath(
@@ -3298,826 +2951,190 @@ function workflowChildDefaultOutput(aggregateOutputPath: string | undefined, art
3298
2951
  const parsed = path.parse(aggregateOutputPath);
3299
2952
  return path.join(parsed.dir, `${parsed.name}.${workflowKey}${parsed.ext || ".md"}`);
3300
2953
  }
3301
- return path.join(artifactsDir, "outputs", workflowRunId, `${workflowKey}.md`);
2954
+ return path.join(artifactsDir, "outputs", sanitizeRunPathSegment(workflowRunId), `${workflowKey}.md`);
3302
2955
  }
3303
2956
 
3304
- function writeWorkflowAggregateOutput(outputPath: string | undefined, text: string): string | undefined {
2957
+ function writeWorkflowAggregateOutput(outputPath: string | undefined, text: string, producedChildOutputPaths: ReadonlySet<string>): string | undefined {
3305
2958
  if (!outputPath) return undefined;
3306
2959
  try {
2960
+ if (producedChildOutputPaths.has(outputPath)) return undefined;
3307
2961
  fs.mkdirSync(path.dirname(outputPath), { recursive: true });
3308
2962
  fs.writeFileSync(outputPath, text, "utf-8");
3309
2963
  return undefined;
3310
2964
  } catch (error) {
3311
2965
  return `Output file error: ${outputPath}\n${error instanceof Error ? error.message : String(error)}`;
3312
2966
  }
3313
- }
3314
-
3315
- function appendWorkflowOutputWarning(text: string, warning: string | undefined): string {
3316
- return warning ? `${text}\n\n${warning}` : text;
3317
- }
3318
-
3319
- function resolveWorkflowChildOutputPath(input: {
3320
- ctxCwd: string;
3321
- workflowCwd: string;
3322
- artifactsDir: string;
3323
- workflowRunId: string;
3324
- aggregateOutputPath?: string;
3325
- configuredOutputBaseDir?: string;
3326
- discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[] };
3327
- workflowAgentScope?: unknown;
3328
- key: string;
3329
- params: Record<string, unknown>;
3330
- }): string | undefined {
3331
- const rawOutput = input.params.output;
3332
- const hasExplicitOutput = typeof rawOutput === "string" || typeof rawOutput === "boolean";
3333
- const childCwd = typeof input.params.cwd === "string" ? resolveChildCwd(input.workflowCwd, input.params.cwd) : input.workflowCwd;
3334
- const agentScope = resolveExecutionAgentScope(input.params.agentScope ?? input.workflowAgentScope);
3335
- const agents = input.discoverAgents(childCwd, agentScope).agents;
3336
- const agent = typeof input.params.agent === "string" ? resolveAgentName(input.params.agent, agents).agent : undefined;
3337
- const agentOutput = typeof agent?.output === "string" ? agent.output : undefined;
3338
- const output = rawOutput === true || rawOutput === "true"
3339
- ? agentOutput
3340
- : hasExplicitOutput
3341
- ? rawOutput
3342
- : workflowChildDefaultOutput(input.aggregateOutputPath, input.artifactsDir, input.workflowRunId, input.key);
3343
- return resolveSingleOutputPath(output, input.ctxCwd, childCwd, input.configuredOutputBaseDir);
3344
- }
3345
-
3346
- function workflowChildOutputClaims(input: {
3347
- ctxCwd: string;
3348
- workflowCwd: string;
3349
- artifactsDir: string;
3350
- workflowRunId: string;
3351
- aggregateOutputPath?: string;
3352
- configuredOutputBaseDir?: string;
3353
- discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[] };
3354
- workflowAgentScope?: unknown;
3355
- claimedOutputPaths: Map<string, string>;
3356
- entries: Array<{ key: string; params: Record<string, unknown> }>;
3357
- }): { error?: string; claims?: Map<string, string> } {
3358
- const claims = new Map(input.claimedOutputPaths);
3359
- for (const { key, params } of input.entries) {
3360
- const resolved = resolveWorkflowChildOutputPath({ ...input, key, params });
3361
- if (!resolved) continue;
3362
- if (input.aggregateOutputPath && resolved === input.aggregateOutputPath) return { error: `Workflow child '${key}' output resolves to the workflow aggregate output path: ${resolved}. Use a distinct child output path.` };
3363
- const previous = claims.get(resolved);
3364
- if (previous) return { error: `Workflow children '${previous}' and '${key}' resolve output to the same path: ${resolved}. Use distinct child output paths.` };
3365
- claims.set(resolved, key);
3366
- }
3367
- return { claims };
3368
- }
3369
-
3370
- function applyWorkflowChildOutputClaims(target: Map<string, string>, claims: Map<string, string>): void {
3371
- target.clear();
3372
- for (const [resolved, key] of claims) target.set(resolved, key);
3373
- }
3374
-
3375
- function prepareWorkflowChildLaunchParams(input: {
3376
- workflowDefaults: SubagentParamsLike;
3377
- childParams: Record<string, unknown>;
3378
- parentWorkflowRunId: string;
3379
- workflowKey: string;
3380
- ctxCwd: string;
3381
- workflowCwd: string;
3382
- artifactsDir: string;
3383
- aggregateOutputPath?: string;
3384
- configuredOutputBaseDir?: string;
3385
- discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[] };
3386
- workflowAgentScope?: unknown;
3387
- options?: { missionDetached?: boolean; suppressRoutineResultIntercom?: boolean; runFanoutBudget?: RunFanoutBudgetDescriptor };
3388
- }): SubagentParamsLike {
3389
- let childParams = input.childParams;
3390
- if (input.childParams.output === undefined && input.childParams.resume === undefined) {
3391
- childParams = { ...input.childParams, output: workflowChildDefaultOutput(input.aggregateOutputPath, input.artifactsDir, input.parentWorkflowRunId, input.workflowKey) };
3392
- } else if (input.childParams.resume === undefined) {
3393
- const resolvedOutput = resolveWorkflowChildOutputPath({ ctxCwd: input.ctxCwd, workflowCwd: input.workflowCwd, artifactsDir: input.artifactsDir, workflowRunId: input.parentWorkflowRunId, aggregateOutputPath: input.aggregateOutputPath, configuredOutputBaseDir: input.configuredOutputBaseDir, discoverAgents: input.discoverAgents, workflowAgentScope: input.workflowAgentScope, key: input.workflowKey, params: input.childParams });
3394
- if (resolvedOutput) childParams = { ...input.childParams, output: resolvedOutput };
3395
- }
3396
- return prepareWorkflowLaunchParams(input.workflowDefaults, childParams, input.parentWorkflowRunId, input.workflowKey, input.options);
3397
- }
3398
-
3399
- function buildChainWorktreeTaskCwdError(chain: ChainStep[], sharedCwd: string): string | undefined {
3400
- for (let stepIndex = 0; stepIndex < chain.length; stepIndex++) {
3401
- const step = chain[stepIndex]!;
3402
- if (!isParallelStep(step) || !step.worktree) continue;
3403
- const stepCwd = resolveChildCwd(sharedCwd, step.cwd);
3404
- const conflict = findWorktreeTaskCwdConflict(step.parallel, stepCwd);
3405
- if (!conflict) continue;
3406
- const detail = formatWorktreeTaskCwdConflict(conflict, stepCwd);
3407
- return `parallel chain step ${stepIndex + 1}: ${detail}`;
3408
- }
3409
- return undefined;
3410
- }
3411
-
3412
- function resolveParallelTaskCwd(
3413
- task: TaskParam,
3414
- paramsCwd: string,
3415
- worktreeSetup: WorktreeSetup | undefined,
3416
- index: number,
3417
- ): string {
3418
- if (worktreeSetup) return worktreeSetup.worktrees[index]!.agentCwd;
3419
- return resolveChildCwd(paramsCwd, task.cwd);
3420
- }
3421
-
3422
- function finalizeParallelWorktreeHandoff(input: {
3423
- worktreeSetup: WorktreeSetup;
3424
- artifactsDir: string;
3425
- runId: string;
3426
- cwd: string;
3427
- tasks: TaskParam[];
3428
- results: SingleResult[];
3429
- }): { suffix: string; reference?: NonNullable<Details["parallelHandoff"]> } {
3430
- const diffsDir = path.join(input.artifactsDir, "worktree-diffs", input.runId);
3431
- const diffs = diffWorktrees(input.worktreeSetup, input.tasks.map((task) => task.agent), diffsDir);
3432
- const diffSummary = formatWorktreeDiffSummary(diffs);
3433
- const manifestPath = parallelHandoffPath(input.artifactsDir, input.runId);
3434
- const handoff = {
3435
- manifestPath,
3436
- runId: input.runId,
3437
- mode: "parallel" as const,
3438
- source: "foreground" as const,
3439
- cwd: input.cwd,
3440
- stepIndex: 0,
3441
- flatStartIndex: 0,
3442
- setup: input.worktreeSetup,
3443
- diffs,
3444
- results: input.results.map((result) => ({
3445
- agent: result.agent,
3446
- status: resolveSubagentResultStatus(omitUndefinedProperties({
3447
- exitCode: result.exitCode,
3448
- interrupted: result.interrupted,
3449
- detached: result.detached,
3450
- state: result.stopped ? "stopped" : undefined,
3451
- processSignal: result.processSignal,
3452
- timedOut: result.timedOut,
3453
- stopped: result.stopped,
3454
- turnBudgetExceeded: result.turnBudgetExceeded,
3455
- })),
3456
- summary: resultSummaryForIntercom(result),
3457
- ...(result.artifactPaths?.outputPath ? { outputPath: result.artifactPaths.outputPath } : {}),
3458
- ...(result.structuredOutput !== undefined ? { structuredOutput: result.structuredOutput } : {}),
3459
- ...(result.structuredOutputPath ? { structuredOutputPath: result.structuredOutputPath } : {}),
3460
- ...(result.sessionFile ? { sessionPath: result.sessionFile } : {}),
3461
- })),
3462
- };
3463
- try {
3464
- writeParallelHandoffGroup(handoff);
3465
- const cleanup = cleanupWorktrees(input.worktreeSetup, { kind: "preserve", capturedDiffs: diffs, handoffManifestPath: manifestPath });
3466
- const reference = writeParallelHandoffGroup({ ...handoff, cleanup });
3467
- return {
3468
- suffix: [diffSummary, formatParallelHandoffReference(reference)].filter(Boolean).join("\n\n"),
3469
- reference,
3470
- };
3471
- } catch (error) {
3472
- return { suffix: [diffSummary, formatParallelHandoffError(error)].filter(Boolean).join("\n\n") };
3473
- }
3474
- }
3475
-
3476
- function findDuplicateParallelOutputPath(input: {
3477
- tasks: TaskParam[];
3478
- behaviors: ResolvedStepBehavior[];
3479
- paramsCwd: string;
3480
- ctxCwd: string;
3481
- outputBaseDir: string;
3482
- worktreeSetup?: WorktreeSetup;
3483
- }): string | undefined {
3484
- const seen = new Map<string, { index: number; agent: string }>();
3485
- for (let index = 0; index < input.tasks.length; index++) {
3486
- const behavior = input.behaviors[index];
3487
- if (!behavior?.output) continue;
3488
- const task = input.tasks[index]!;
3489
- const taskCwd = resolveParallelTaskCwd(task, input.paramsCwd, input.worktreeSetup, index);
3490
- const outputPath = resolveSingleOutputPath(behavior.output, input.ctxCwd, taskCwd, input.outputBaseDir);
3491
- if (!outputPath) continue;
3492
- const previous = seen.get(outputPath);
3493
- if (previous) {
3494
- return `Parallel tasks ${previous.index + 1} (${previous.agent}) and ${index + 1} (${task.agent}) resolve output to the same path: ${outputPath}. Use distinct output paths.`;
3495
- }
3496
- seen.set(outputPath, { index, agent: task.agent });
3497
- }
3498
- return undefined;
3499
- }
3500
-
3501
- async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Promise<SingleResult[]> {
3502
- // Pre-warm fork session files sequentially before concurrent dispatch to avoid
3503
- // races where multiple workers simultaneously try to branch the same parent session.
3504
- for (let i = 0; i < input.tasks.length; i++) {
3505
- input.sessionFileForTask(input.tasks[i]!.agent, i, input.modelOverrides[i]);
3506
- }
3507
- const completedResults: SingleResult[] = [];
3508
- if (input.foregroundControl) retainForegroundSchedulingOwner(input.foregroundControl);
3509
- return mapConcurrent(input.tasks, input.concurrencyLimit, async (task, index) => {
3510
- const budgetState = usageBudgetState(input.usageBudget, sumResultsCost(completedResults));
3511
- if (budgetState?.exhausted) {
3512
- return {
3513
- index,
3514
- agent: task.agent,
3515
- task: input.taskTexts[index] ?? "(skipped)",
3516
- exitCode: 1,
3517
- messages: [],
3518
- usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
3519
- error: usageBudgetExceededMessage(budgetState),
3520
- skipped: true,
3521
- } as SingleResult;
3522
- }
3523
- const behavior = input.behaviors[index];
3524
- const effectiveSkills = behavior?.skills;
3525
- const taskCwd = resolveParallelTaskCwd(task, input.paramsCwd, input.worktreeSetup, index);
3526
- const readInstructions = behavior
3527
- ? buildChainInstructions({ ...behavior, output: false, progress: false }, taskCwd, false)
3528
- : { prefix: "", suffix: "" };
3529
- const progressInstructions = behavior
3530
- ? buildChainInstructions({ ...behavior, output: false, reads: false }, input.progressDir, index === input.firstProgressIndex)
3531
- : { prefix: "", suffix: "" };
3532
- const outputPath = resolveSingleOutputPath(behavior?.output, input.ctx.cwd, taskCwd, input.outputBaseDir);
3533
- const agentConfig = input.agents.find((agent) => agent.name === task.agent);
3534
- const taskText = injectSingleOutputInstruction(
3535
- `${readInstructions.prefix}${input.taskTexts[index]!}${progressInstructions.suffix}`,
3536
- outputPath,
3537
- agentConfig,
3538
- );
3539
- const interruptController = new AbortController();
3540
- if (input.foregroundControl) {
3541
- const model = input.modelOverrides[index];
3542
- const thinking = resolveEffectiveThinking(model, input.thinkingOverrideForTask(task.agent, index, model));
3543
- beginForegroundChild(input.foregroundControl, {
3544
- index,
3545
- agent: task.agent,
3546
- authoredTask: input.taskDescriptions[index] ?? "",
3547
- effectivePrompt: taskText,
3548
- cwd: taskCwd,
3549
- ...(outputPath ? { outputPath } : {}),
3550
- rerun: { params: compactOptional<Record<string, unknown>>({
3551
- agent: task.agent,
3552
- task: input.taskDescriptions[index] ?? "",
3553
- async: false,
3554
- cwd: input.worktreeSetup ? undefined : taskCwd,
3555
- worktree: input.worktreeSetup ? true : false,
3556
- context: input.contextPolicy.contextForAgent(task.agent),
3557
- share: input.shareEnabled || undefined,
3558
- maxOutput: input.maxOutput,
3559
- timeoutMs: input.timeoutMs,
3560
- toolTimeoutMs: input.toolTimeoutMs,
3561
- configToolTimeoutMs: input.configToolTimeoutMs,
3562
- turnBudget: input.turnBudget,
3563
- ...(model ? { model } : {}),
3564
- ...(effectiveSkills !== undefined ? { skill: effectiveSkills } : {}),
3565
- ...(outputPath ? { output: outputPath } : {}),
3566
- ...(behavior?.outputMode !== undefined ? { outputMode: behavior.outputMode } : {}),
3567
- ...(behavior?.reads !== undefined ? { reads: behavior.reads } : {}),
3568
- ...(behavior?.progress !== undefined ? { progress: behavior.progress } : {}),
3569
- ...(task.toolBudget !== undefined ? { toolBudget: task.toolBudget } : {}),
3570
- ...(task.outputSchema !== undefined ? { outputSchema: task.outputSchema } : {}),
3571
- ...(task.acceptance !== undefined ? { acceptance: task.acceptance } : {}),
3572
- ...(task.agentContract !== undefined || input.agentContract !== undefined ? { agentContract: task.agentContract ?? input.agentContract } : {}),
3573
- }) },
3574
- description: `${task.agent} child`,
3575
- ...(model ? { model } : {}),
3576
- ...(thinking ? { thinking } : {}),
3577
- interrupt: () => {
3578
- if (interruptController.signal.aborted) return false;
3579
- interruptController.abort();
3580
- return true;
3581
- },
3582
- });
3583
- }
3584
- const structuredRuntime = task.outputSchema
3585
- ? createStructuredOutputRuntime(task.outputSchema, path.join(input.artifactsDir, "structured-output", input.runId))
3586
- : undefined;
3587
- let detachedReceipt = false;
3588
- const result = await runSync(input.ctx.cwd, input.agents, task.agent, taskText, compactOptional<Parameters<typeof runSync>[4]>({
3589
- permissions: input.permissions,
3590
- parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
3591
- llmIntentArbiter: createTaskMutationArbiter(input.ctx),
3592
- ...workflowForegroundSteeringLaunchOptions(input.foregroundControl, index),
3593
- context: input.contextPolicy.contextForAgent(task.agent),
3594
- runFanoutBudget: { ...input.runFanoutBudget, parentPath: `${input.runFanoutBudget.parentPath ? `${input.runFanoutBudget.parentPath}/` : ""}tasks[${index}]` },
3595
- cwd: taskCwd,
3596
- signal: input.signal,
3597
- interruptSignal: interruptController.signal,
3598
- allowIntercomDetach: agentConfig?.systemPrompt?.includes(INTERCOM_BRIDGE_MARKER) === true,
3599
- intercomEvents: input.intercomEvents,
3600
- runId: input.runId,
3601
- index,
3602
- sessionDir: input.sessionDirForIndex(index),
3603
- sessionFile: input.sessionFileForTask(task.agent, index, input.modelOverrides[index]),
3604
- share: input.shareEnabled,
3605
- artifactsDir: input.artifactConfig.enabled ? input.artifactsDir : undefined,
3606
- artifactConfig: input.artifactConfig,
3607
- maxOutput: input.maxOutput,
3608
- outputPath,
3609
- outputMode: behavior?.outputMode,
3610
- maxSubagentDepth: input.maxSubagentDepths[index],
3611
- waitToolEnabled: input.waitToolEnabled,
3612
- capabilityCeiling: input.capabilityCeiling,
3613
- controlConfig: input.controlConfig,
3614
- onControlEvent: input.onControlEvent,
3615
- onDetachedExit: (result) => {
3616
- try {
3617
- updateRememberedForegroundChild(input.state, { runId: input.runId, mode: "parallel", cwd: taskCwd, sessionId: input.parentSessionId, index, result, events: input.intercomEvents });
3618
- } finally {
3619
- try {
3620
- if (input.foregroundControl) finishForegroundChild(input.foregroundControl, index);
3621
- } finally {
3622
- removeForegroundControlIfIdle(input.state, input.runId);
3623
- }
3624
- }
3625
- },
3626
- intercomSessionName: input.childIntercomTarget?.(task.agent, index),
3627
- orchestratorIntercomTarget: input.orchestratorIntercomTarget,
3628
- nestedRoute: input.foregroundControl?.nestedRoute,
3629
- modelOverride: input.modelOverrides[index],
3630
- thinkingOverride: input.thinkingOverrideForTask(task.agent, index, input.modelOverrides[index]),
3631
- availableModels: input.availableModels,
3632
- preferredModelProvider: input.parentModel?.provider,
3633
- modelScope: input.modelScope,
3634
- skills: effectiveSkills === false ? [] : effectiveSkills,
3635
- structuredOutput: structuredRuntime,
3636
- agentContract: task.agentContract ?? input.agentContract,
3637
- acceptance: task.acceptance,
3638
- acceptanceContext: { mode: "parallel" },
3639
- onEffectivePrompt: input.foregroundControl ? (prompt) => updateLiveEffectivePrompt(input.foregroundControl!, index, prompt) : undefined,
3640
- timeoutMs: input.timeoutMs,
3641
- toolTimeoutMs: input.toolTimeoutMs,
3642
- configToolTimeoutMs: input.configToolTimeoutMs,
3643
- deadlineAt: input.deadlineAt,
3644
- turnBudget: input.turnBudget,
3645
- toolBudget: input.toolBudgets[index],
3646
- onUpdate: input.onUpdate
3647
- ? (progressUpdate) => {
3648
- const stepResults = progressUpdate.details?.results || [];
3649
- const stepProgress = progressUpdate.details?.progress || [];
3650
- if (input.foregroundControl && stepProgress.length > 0) {
3651
- updateForegroundChild(input.foregroundControl, index, stepProgress[0]);
3652
- }
3653
- if (stepResults.length > 0) input.liveResults[index] = stepResults[0];
3654
- if (stepProgress.length > 0) input.liveProgress[index] = stepProgress[0];
3655
- const mergedResults = input.liveResults.filter((result): result is SingleResult => result !== undefined);
3656
- const mergedProgress = input.liveProgress.filter((progress): progress is AgentProgress => progress !== undefined);
3657
- input.onUpdate?.({
3658
- content: progressUpdate.content,
3659
- details: compactOptional<Details>({
3660
- mode: "parallel",
3661
- results: mergedResults,
3662
- progress: mergedProgress,
3663
- controlEvents: progressUpdate.details?.controlEvents,
3664
- totalSteps: input.tasks.length,
3665
- }),
3666
- });
3667
- }
3668
- : undefined,
3669
- })).then((result) => {
3670
- detachedReceipt = result.detached === true;
3671
- return result;
3672
- }).finally(() => {
3673
- // mapConcurrent rejects before siblings settle, so every attached child
3674
- // attempts idle removal after releasing its own control. Detached receipts
3675
- // transfer both responsibilities to the authoritative exit callback.
3676
- if (!detachedReceipt) {
3677
- if (input.foregroundControl) finishForegroundChild(input.foregroundControl, index);
3678
- removeForegroundControlIfIdle(input.state, input.runId);
3679
- }
3680
- });
3681
- completedResults.push(result);
3682
- return result;
3683
- }, input.globalSemaphore, () => {
3684
- if (input.foregroundControl) settleForegroundSchedulingOwner(input.foregroundControl);
3685
- removeForegroundControlIfIdle(input.state, input.runId);
3686
- });
3687
- }
3688
-
3689
- async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps): Promise<AgentToolResult<Details>> {
3690
- const {
3691
- params,
3692
- effectiveCwd,
3693
- agents,
3694
- ctx,
3695
- signal,
3696
- runId,
3697
- sessionDirForIndex,
3698
- sessionFileForIndex,
3699
- sessionFileForTask,
3700
- thinkingOverrideForTask,
3701
- shareEnabled,
3702
- artifactConfig,
3703
- artifactsDir,
3704
- backgroundRequestedWhileClarifying,
3705
- onUpdate,
3706
- sessionRoot,
3707
- controlConfig,
3708
- contextPolicy,
3709
- } = data;
3710
- const onControlEvent = createForegroundControlNotifier(data, deps);
3711
- const childIntercomTarget = data.intercomBridge.active ? resolveSubagentIntercomTarget : undefined;
3712
- const allProgress: AgentProgress[] = [];
3713
- const allArtifactPaths: ArtifactPaths[] = [];
3714
- const tasks = params.tasks!;
3715
- const maxParallelTasks = resolveTopLevelParallelMaxTasks(deps.config.parallel?.maxTasks);
3716
- const parallelConcurrency = resolveTopLevelParallelConcurrency(params.concurrency, deps.config.parallel?.concurrency);
3717
-
3718
- if (tasks.length > maxParallelTasks)
3719
- return {
3720
- content: [{ type: "text", text: `Max ${maxParallelTasks} tasks` }],
3721
- isError: true,
3722
- details: { mode: "parallel" as const, results: [] },
3723
- };
3724
-
3725
- const agentConfigs: AgentConfig[] = [];
3726
- for (const t of tasks) {
3727
- const config = agents.find((a) => a.name === t.agent);
3728
- if (!config) {
3729
- return {
3730
- content: [{ type: "text", text: `Unknown agent: ${t.agent}` }],
3731
- isError: true,
3732
- details: { mode: "parallel" as const, results: [] },
3733
- };
3734
- }
3735
- agentConfigs.push(config);
3736
- }
3737
-
3738
- const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
3739
- const maxSubagentDepths = agentConfigs.map((config) =>
3740
- resolveChildMaxSubagentDepth(currentMaxSubagentDepth, config.maxSubagentDepth),
3741
- );
3742
- const toolBudgets: (ResolvedToolBudget | undefined)[] = [];
3743
- for (let index = 0; index < tasks.length; index++) {
3744
- const resolved = resolveEffectiveToolBudget(omitUndefinedProperties({ stepBudget: tasks[index]?.toolBudget, runBudget: data.toolBudget, agentBudget: agentConfigs[index]?.toolBudget, configBudget: data.configToolBudget }));
3745
- if (resolved.error) return buildParallelModeError(resolved.error);
3746
- toolBudgets.push(resolved.toolBudget);
3747
- }
3748
-
3749
- if (params.worktree) {
3750
- const worktreeTaskCwdError = buildParallelWorktreeTaskCwdError(tasks, effectiveCwd);
3751
- if (worktreeTaskCwdError) return buildParallelModeError(worktreeTaskCwdError);
3752
- }
3753
-
3754
- const parentModel = data.parentModel;
3755
- const currentProvider = parentModel?.provider;
3756
- const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo);
3757
- let taskTexts = tasks.map((t) => t.task);
3758
- const skillOverrides: (string[] | false | undefined)[] = tasks.map((t) =>
3759
- normalizeSkillInput(t.skill),
3760
- );
3761
- const behaviorOverrides: StepOverrides[] = tasks.map((task, index) => ({
3762
- ...(task.output !== undefined && task.output !== true ? { output: task.output } : {}),
3763
- ...(task.outputMode !== undefined ? { outputMode: task.outputMode } : {}),
3764
- ...(task.reads !== undefined && task.reads !== true ? { reads: task.reads } : {}),
3765
- ...(task.progress !== undefined ? { progress: task.progress } : {}),
3766
- ...(skillOverrides[index] !== undefined ? { skills: skillOverrides[index] } : {}),
3767
- ...(task.model !== undefined ? { model: task.model } : {}),
3768
- }));
3769
- const modelOverrides: (string | undefined)[] = tasks.map((_, i) =>
3770
- agentConfigs[i]?.runner?.type === "external-cli"
3771
- ? undefined
3772
- : resolveEffectiveSubagentModel(behaviorOverrides[i]?.model, agentConfigs[i]?.model, parentModel, availableModels, currentProvider, data.modelScope === undefined ? {} : { scope: data.modelScope }),
3773
- );
3774
-
3775
- if (params.clarify === true && ctx.hasUI) {
3776
- const behaviors = agentConfigs.map((c, i) =>
3777
- resolveStepBehavior(c, behaviorOverrides[i]!),
3778
- );
3779
- const availableSkills = discoverAvailableSkills(effectiveCwd);
3780
-
3781
- const result = await ctx.ui.custom<ChainClarifyResult>(
3782
- (tui, theme, _kb, done) =>
3783
- new ChainClarifyComponent(
3784
- tui, theme,
3785
- agentConfigs,
3786
- taskTexts,
3787
- "",
3788
- undefined,
3789
- behaviors,
3790
- availableModels,
3791
- currentProvider,
3792
- availableSkills,
3793
- done,
3794
- "parallel",
3795
- ),
3796
- { overlay: true, overlayOptions: { anchor: "center", width: 84, maxHeight: "80%" } },
3797
- );
3798
-
3799
- if (!result || !result.confirmed) {
3800
- return { content: [{ type: "text", text: "Cancelled" }], details: { mode: "parallel", results: [] } };
3801
- }
3802
-
3803
- taskTexts = result.templates;
3804
- for (let i = 0; i < result.behaviorOverrides.length; i++) {
3805
- const override = result.behaviorOverrides[i];
3806
- if (override?.model !== undefined) {
3807
- modelOverrides[i] = agentConfigs[i]?.runner?.type === "external-cli"
3808
- ? undefined
3809
- : resolveEffectiveSubagentModel(override.model, agentConfigs[i]?.model, parentModel, availableModels, currentProvider, data.modelScope === undefined ? {} : { scope: data.modelScope });
3810
- behaviorOverrides[i]!.model = override.model;
3811
- }
3812
- if (override?.output !== undefined) behaviorOverrides[i]!.output = override.output;
3813
- if (override?.reads !== undefined) behaviorOverrides[i]!.reads = override.reads;
3814
- if (override?.progress !== undefined) behaviorOverrides[i]!.progress = override.progress;
3815
- if (override?.skills !== undefined) {
3816
- skillOverrides[i] = override.skills;
3817
- behaviorOverrides[i]!.skills = override.skills;
3818
- }
3819
- }
3820
-
3821
- if (result.runInBackground) {
3822
- if (!isAsyncAvailable()) {
3823
- return {
3824
- content: [{ type: "text", text: "Background mode requires upstream jiti for TypeScript execution but it could not be found. Ensure the pi-subagents package dependencies are installed." }],
3825
- isError: true,
3826
- details: { mode: "parallel" as const, results: [] },
3827
- };
3828
- }
3829
- const id = data.asyncRunId;
3830
- const capacity = acquireClarifiedBackgroundCapacity(data, deps, "parallel", id);
3831
- if (capacity.errorResult) return capacity.errorResult;
3832
- const asyncCtx = compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
3833
- pi: deps.pi,
3834
- cwd: ctx.cwd,
3835
- currentSessionId: data.parentSessionId!,
3836
- parentSessionId: data.parentPiSessionId,
3837
- currentModelProvider: parentModel?.provider,
3838
- currentModel: parentModel,
3839
- modelScope: data.modelScope,
3840
- interactive: ctx.hasUI,
3841
- permissions: deps.config.permissions,
3842
- });
3843
- const parallelTasks = tasks.map((t, i) => {
3844
- const taskText = shouldForkAgent(contextPolicy, t.agent) ? wrapForkTask(taskTexts[i]!) : taskTexts[i]!;
3845
- const progress = taskDisallowsFileUpdates(taskText) ? false : behaviorOverrides[i]?.progress;
3846
- return compactOptional<ParallelTaskItem>({
3847
- agent: t.agent,
3848
- task: taskText,
3849
- cwd: t.cwd,
3850
- ...(behaviorOverrides[i]?.model !== undefined ? { model: behaviorOverrides[i]!.model } : {}),
3851
- ...(skillOverrides[i] !== undefined ? { skill: skillOverrides[i] } : {}),
3852
- ...(behaviorOverrides[i]?.output !== undefined ? { output: behaviorOverrides[i]!.output } : {}),
3853
- ...(behaviorOverrides[i]?.outputMode !== undefined ? { outputMode: behaviorOverrides[i]!.outputMode } : {}),
3854
- ...(behaviorOverrides[i]?.reads !== undefined ? { reads: behaviorOverrides[i]!.reads } : {}),
3855
- ...(progress !== undefined ? { progress } : {}),
3856
- ...(t.toolBudget !== undefined ? { toolBudget: t.toolBudget } : {}),
3857
- ...(t.outputSchema !== undefined ? { outputSchema: t.outputSchema } : {}),
3858
- ...(t.acceptance !== undefined ? { acceptance: t.acceptance } : {}),
3859
- ...(t.agentContract !== undefined ? { agentContract: t.agentContract } : {}),
3860
- });
3861
- });
3862
- try {
3863
- return executeAsyncChain(id, compactOptional<Parameters<typeof executeAsyncChain>[1]>({
3864
- chain: [compactOptional<ParallelStep>({ parallel: parallelTasks, concurrency: parallelConcurrency, worktree: params.worktree })],
3865
- resultMode: "parallel",
3866
- goal: taskTexts[0] ?? "",
3867
- agents,
3868
- ctx: asyncCtx,
3869
- availableModels,
3870
- cwd: effectiveCwd,
3871
- maxOutput: params.maxOutput,
3872
- artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
3873
- artifactConfig,
3874
- shareEnabled,
3875
- sessionRoot,
3876
- chainSkills: [],
3877
- activeAsyncCapacity: capacity.activeAsyncCapacity,
3878
- sessionFilesByFlatIndex: tasks.map((task, index) => sessionFileForTask(task.agent, index, modelOverrides[index])),
3879
- thinkingOverridesByFlatIndex: tasks.map((task, index) => thinkingOverrideForTask(task.agent, index, modelOverrides[index])),
3880
- contextForAgent: contextPolicy.contextForAgent,
3881
- maxSubagentDepth: currentMaxSubagentDepth,
3882
- waitToolEnabled: deps.waitToolEnabled,
3883
- worktreeSetupHook: deps.config.worktreeSetupHook,
3884
- worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
3885
- worktreeBaseDir: deps.config.worktreeBaseDir,
3886
- controlConfig,
3887
- agentContract: params.agentContract,
3888
- controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
3889
- childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
3890
- nestedRoute: data.nestedRoute,
3891
- timeoutMs: data.timeoutMs,
3892
- turnBudget: data.turnBudget,
3893
- usageBudget: data.usageBudget,
3894
- toolBudget: data.toolBudget,
3895
- configToolBudget: data.configToolBudget,
3896
- callToolTimeoutMs: data.params?.toolTimeoutMs,
3897
- configToolTimeoutMs: data.configToolTimeoutMs,
3898
- globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
3899
- runFanoutBudget: data.runFanoutBudget,
3900
- }));
3901
- } finally {
3902
- finalizeClarifiedBackgroundCapacity(data, deps, capacity.activeAsyncCapacity);
3903
- }
3904
- }
3905
- }
3906
-
3907
- const behaviors = tasks.map((task, index) => {
3908
- let behavior = suppressProgressForReadOnlyTask(resolveStepBehavior(agentConfigs[index]!, behaviorOverrides[index]!), taskTexts[index]);
3909
- if (behaviorOverrides[index]?.output === undefined && typeof behavior.output === "string" && !path.isAbsolute(behavior.output)) {
3910
- behavior = { ...behavior, output: path.join("parallel-0", `${index}-${task.agent}`, behavior.output) };
3911
- }
3912
- return behavior;
3913
- });
3914
- const firstProgressIndex = behaviors.findIndex((behavior) => behavior.progress);
3915
- const liveResults: (SingleResult | undefined)[] = new Array(tasks.length).fill(undefined);
3916
- const liveProgress: (AgentProgress | undefined)[] = new Array(tasks.length).fill(undefined);
3917
- const foregroundControl = deps.state.foregroundControls.get(runId);
3918
- const { setup: worktreeSetup, errorResult } = createParallelWorktreeSetup(
3919
- params.worktree,
3920
- effectiveCwd,
3921
- runId,
3922
- tasks,
3923
- deps.config.worktreeSetupHook,
3924
- deps.config.worktreeSetupHookTimeoutMs,
3925
- deps.config.worktreeBaseDir,
3926
- );
3927
- if (errorResult) return errorResult;
3928
-
3929
- let worktreeCleanupHandled = false;
3930
- let pendingHandoff: Details["parallelHandoff"];
3931
- try {
3932
- if (worktreeSetup) {
3933
- pendingHandoff = writePendingParallelHandoff({
3934
- manifestPath: parallelHandoffPath(artifactsDir, runId),
3935
- runId,
3936
- mode: "parallel",
3937
- source: "foreground",
3938
- cwd: effectiveCwd,
3939
- stepIndex: 0,
3940
- flatStartIndex: 0,
3941
- setup: worktreeSetup,
3942
- });
3943
- }
3944
- const outputBaseDir = path.join(artifactsDir, "outputs", runId);
3945
- const duplicateOutputError = findDuplicateParallelOutputPath(omitUndefinedProperties({
3946
- tasks,
3947
- behaviors,
3948
- paramsCwd: effectiveCwd,
3949
- ctxCwd: ctx.cwd,
3950
- outputBaseDir,
3951
- worktreeSetup,
3952
- }));
3953
- if (duplicateOutputError) return buildParallelModeError(duplicateOutputError);
3954
- for (let index = 0; index < tasks.length; index++) {
3955
- const taskCwd = resolveParallelTaskCwd(tasks[index]!, effectiveCwd, worktreeSetup, index);
3956
- const outputPath = resolveSingleOutputPath(behaviors[index]?.output, ctx.cwd, taskCwd, outputBaseDir);
3957
- const validationError = validateFileOnlyOutputMode(behaviors[index]?.outputMode, outputPath, `Parallel task ${index + 1} (${tasks[index]!.agent})`);
3958
- if (validationError) return buildParallelModeError(validationError);
3959
- }
3960
-
3961
- const parallelProgressPrecreated = firstProgressIndex !== -1;
3962
- const parallelProgressDir = path.join(artifactsDir, "progress", runId);
3963
- if (parallelProgressPrecreated) writeInitialProgressFile(parallelProgressDir);
3964
-
3965
- const taskDescriptions = taskTexts.map((taskText) => taskText.trim());
3966
- for (let i = 0; i < taskTexts.length; i++) {
3967
- if (shouldForkAgent(contextPolicy, tasks[i]!.agent)) taskTexts[i] = wrapForkTask(taskTexts[i]!);
3968
- }
3969
-
3970
- const deadlineAt = data.deadlineAt ?? (data.timeoutMs !== undefined ? Date.now() + data.timeoutMs : undefined);
3971
- const results = await runForegroundParallelTasks(compactOptional<ForegroundParallelRunInput>({
3972
- tasks,
3973
- permissions: deps.config.permissions,
3974
- taskTexts,
3975
- taskDescriptions,
3976
- agents,
3977
- ctx,
3978
- state: deps.state,
3979
- intercomEvents: deps.pi.events,
3980
- parentSessionId: data.parentSessionId,
3981
- capabilityCeiling: data.capabilityCeiling,
3982
- runFanoutBudget: data.runFanoutBudget,
3983
- signal,
3984
- runId,
3985
- sessionDirForIndex,
3986
- sessionFileForIndex,
3987
- sessionFileForTask,
3988
- thinkingOverrideForTask,
3989
- shareEnabled,
3990
- artifactConfig,
3991
- artifactsDir,
3992
- outputBaseDir,
3993
- maxOutput: params.maxOutput,
3994
- paramsCwd: effectiveCwd,
3995
- progressDir: parallelProgressDir,
3996
- availableModels,
3997
- modelScope: data.modelScope,
3998
- parentModel,
3999
- modelOverrides,
4000
- behaviors,
4001
- firstProgressIndex: parallelProgressPrecreated ? -1 : firstProgressIndex,
4002
- controlConfig,
4003
- contextPolicy,
4004
- onControlEvent,
4005
- childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(runId, agent, index) : undefined,
4006
- orchestratorIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
4007
- foregroundControl,
4008
- concurrencyLimit: parallelConcurrency,
4009
- globalSemaphore: new Semaphore(deps.config.globalConcurrencyLimit ?? DEFAULT_GLOBAL_CONCURRENCY_LIMIT),
4010
- maxSubagentDepths,
4011
- waitToolEnabled: deps.waitToolEnabled,
4012
- liveResults,
4013
- liveProgress,
4014
- onUpdate,
4015
- worktreeSetup,
4016
- timeoutMs: data.timeoutMs,
4017
- deadlineAt,
4018
- toolTimeoutMs: data.params?.toolTimeoutMs,
4019
- configToolTimeoutMs: data.configToolTimeoutMs,
4020
- turnBudget: data.turnBudget,
4021
- usageBudget: data.usageBudget,
4022
- toolBudgets,
4023
- agentContract: params.agentContract,
4024
- }));
4025
- for (let i = 0; i < results.length; i++) {
4026
- const run = results[i]!;
4027
- recordRun(run.agent, taskTexts[i]!, run.exitCode, run.progressSummary?.durationMs ?? 0);
4028
- }
2967
+ }
4029
2968
 
4030
- for (const result of results) {
4031
- if (result.progress) allProgress.push(result.progress);
4032
- if (result.artifactPaths) allArtifactPaths.push(result.artifactPaths);
4033
- }
2969
+ function appendWorkflowOutputWarning(text: string, warning: string | undefined): string {
2970
+ return warning ? `${text}\n\n${warning}` : text;
2971
+ }
4034
2972
 
4035
- if (foregroundControl) {
4036
- updateForegroundNestedProjection(foregroundControl);
4037
- attachRootChildrenToSteps(runId, results, foregroundControl.nestedChildren);
4038
- }
4039
- const detached = results.find((result) => result.detached);
4040
- let handoff: ReturnType<typeof finalizeParallelWorktreeHandoff> | undefined;
4041
- if (worktreeSetup) {
4042
- worktreeCleanupHandled = true;
4043
- handoff = detached
4044
- ? { suffix: pendingHandoff ? formatParallelHandoffReference(pendingHandoff) : "", reference: pendingHandoff }
4045
- : finalizeParallelWorktreeHandoff({ worktreeSetup, artifactsDir, runId, cwd: effectiveCwd, tasks, results });
4046
- }
4047
- const interrupted = results.find((result) => result.interrupted);
4048
- const totalCost = sumResultsCost(results);
4049
- const details = compactForegroundDetails(compactOptional<Details>({
4050
- mode: "parallel",
4051
- runId,
4052
- timeoutMs: data.timeoutMs,
4053
- results,
4054
- progress: params.includeProgress ? allProgress : undefined,
4055
- artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
4056
- totalChildUsage: sumResultsUsage(results),
4057
- totalCost,
4058
- usageBudget: usageBudgetState(data.usageBudget, totalCost),
4059
- ...(handoff?.reference ? { parallelHandoff: handoff.reference } : {}),
4060
- }));
4061
- rememberForegroundRun(deps.state, { runId, mode: "parallel", cwd: effectiveCwd, sessionId: data.parentSessionId, results: details.results });
4062
- if (interrupted) {
4063
- return {
4064
- content: [{ type: "text", text: `Parallel run paused after interrupt (${interrupted.agent}). Waiting for explicit next action.` }],
4065
- details,
4066
- };
4067
- }
4068
- if (detached) {
4069
- const handoffSuffix = handoff?.suffix ? `\n\n${handoff.suffix}` : "";
4070
- return {
4071
- content: [{ type: "text", text: `Parallel run detached for intercom coordination (${detached.agent}). Reply to the supervisor request first, then wait with subagent_wait({ id: "${runId}" }). Use subagent({ action: "status", id: "${runId}" }) to recover the result; do not resume or launch a replacement while it remains detached.${handoffSuffix}` }],
4072
- details,
4073
- };
4074
- }
2973
+ function resolveWorkflowChildOutputPath(input: {
2974
+ ctxCwd: string;
2975
+ workflowCwd: string;
2976
+ artifactsDir: string;
2977
+ workflowRunId: string;
2978
+ aggregateOutputPath?: string;
2979
+ configuredOutputBaseDir?: string;
2980
+ discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[] };
2981
+ workflowAgentScope?: unknown;
2982
+ state?: SubagentState;
2983
+ key: string;
2984
+ params: Record<string, unknown>;
2985
+ }): { path?: string; inherited: boolean } {
2986
+ if (typeof input.params.resume === "string") {
2987
+ if (!input.state) return { path: undefined, inherited: false };
2988
+ const index = input.params.index;
2989
+ const target = resolveResumeTarget({
2990
+ id: input.params.resume.trim(),
2991
+ ...(typeof index === "number" && Number.isInteger(index) ? { index } : {}),
2992
+ }, input.state);
2993
+ return { path: "recoveryDescriptor" in target ? target.recoveryDescriptor?.outputPath : undefined, inherited: false };
2994
+ }
2995
+ const rawOutput = input.params.output;
2996
+ const hasExplicitOutput = typeof rawOutput === "string" || typeof rawOutput === "boolean";
2997
+ const childCwd = typeof input.params.cwd === "string" ? resolveChildCwd(input.workflowCwd, input.params.cwd) : input.workflowCwd;
2998
+ const agentScope = resolveExecutionAgentScope(input.params.agentScope ?? input.workflowAgentScope);
2999
+ const agents = input.discoverAgents(childCwd, agentScope).agents;
3000
+ const agent = typeof input.params.agent === "string" ? resolveAgentName(input.params.agent, agents).agent : undefined;
3001
+ const agentOutput = typeof agent?.output === "string" ? agent.output : undefined;
3002
+ const output = rawOutput === true || rawOutput === "true"
3003
+ ? agentOutput
3004
+ : hasExplicitOutput
3005
+ ? rawOutput
3006
+ : input.aggregateOutputPath
3007
+ ? workflowChildDefaultOutput(input.aggregateOutputPath, input.artifactsDir, input.workflowRunId, input.key)
3008
+ : agentOutput;
3009
+ return {
3010
+ path: resolveSingleOutputPath(output, input.ctxCwd, childCwd, input.configuredOutputBaseDir),
3011
+ inherited: !hasExplicitOutput && !input.aggregateOutputPath && agentOutput !== undefined,
3012
+ };
3013
+ }
4075
3014
 
4076
- const suppressRoutineResultIntercom = shouldSuppressRoutineResultIntercom({ suppressRoutineResultIntercom: params.suppressRoutineResultIntercom, results });
4077
- if (foregroundControl) updateForegroundNestedProjection(foregroundControl);
4078
- if (!suppressRoutineResultIntercom) {
4079
- const intercomReceipt = await maybeBuildForegroundIntercomReceipt({
4080
- pi: deps.pi,
4081
- intercomBridge: data.intercomBridge,
4082
- runId,
4083
- mode: "parallel",
4084
- details,
4085
- ...(params.workflowParentRunId !== undefined ? { preserveDetailsOutputs: true } : {}),
4086
- ...(foregroundControl?.nestedChildren?.length ? { nestedChildren: foregroundControl.nestedChildren } : {}),
4087
- });
4088
- if (intercomReceipt) {
4089
- return {
4090
- content: [{ type: "text", text: intercomReceipt.text }],
4091
- details: intercomReceipt.details,
4092
- };
4093
- }
3015
+ function workflowChildOutputClaims(input: {
3016
+ ctxCwd: string;
3017
+ workflowCwd: string;
3018
+ artifactsDir: string;
3019
+ workflowRunId: string;
3020
+ aggregateOutputPath?: string;
3021
+ configuredOutputBaseDir?: string;
3022
+ discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[] };
3023
+ workflowAgentScope?: unknown;
3024
+ state: SubagentState;
3025
+ claimedOutputPaths: Map<string, string>;
3026
+ entries: Array<{ key: string; params: Record<string, unknown> }>;
3027
+ }): { error?: string; claims?: Map<string, string>; overrides?: Map<string, string> } {
3028
+ const resolvedEntries = input.entries.map(({ key, params }) => ({
3029
+ key,
3030
+ ...resolveWorkflowChildOutputPath({ ...input, key, params }),
3031
+ }));
3032
+ const paths = new Map<string, number>();
3033
+ for (const claimedPath of input.claimedOutputPaths.keys()) paths.set(claimedPath, 1);
3034
+ for (const { path: resolved } of resolvedEntries) {
3035
+ if (resolved) paths.set(resolved, (paths.get(resolved) ?? 0) + 1);
3036
+ }
3037
+ const overrides = new Map<string, string>();
3038
+ for (const entry of resolvedEntries) {
3039
+ if (entry.inherited && entry.path && (paths.get(entry.path) ?? 0) > 1) {
3040
+ const output = workflowChildDefaultOutput(input.aggregateOutputPath, input.artifactsDir, input.workflowRunId, entry.key);
3041
+ overrides.set(entry.key, output);
3042
+ entry.path = output;
4094
3043
  }
3044
+ }
3045
+ const claims = new Map(input.claimedOutputPaths);
3046
+ for (const { key, path: resolved } of resolvedEntries) {
3047
+ if (!resolved) continue;
3048
+ const previous = claims.get(resolved);
3049
+ if (previous) return { error: `Workflow children '${previous}' and '${key}' resolve output to the same path: ${resolved}. Use distinct child output paths.` };
3050
+ claims.set(resolved, key);
3051
+ }
3052
+ return { claims, overrides };
3053
+ }
4095
3054
 
4096
- const worktreeSuffix = handoff?.suffix ?? "";
4097
- const ok = results.filter((result) => result.exitCode === 0).length;
4098
- const downgradeNote = backgroundRequestedWhileClarifying ? " (background requested, but clarify kept this run foreground)" : "";
4099
- const aggregatedOutput = aggregateParallelOutputs(
4100
- results.map((result) => ({
4101
- agent: result.agent,
4102
- output: result.truncation?.text || getSingleResultOutput(result),
4103
- exitCode: result.exitCode,
4104
- ...(result.error === undefined ? {} : { error: result.error }),
4105
- ...(result.timedOut === undefined ? {} : { timedOut: result.timedOut }),
4106
- })),
4107
- (i, agent) => `=== Task ${i + 1}: ${agent} ===`,
4108
- );
3055
+ function applyWorkflowChildOutputClaims(target: Map<string, string>, claims: Map<string, string>): void {
3056
+ target.clear();
3057
+ for (const [resolved, key] of claims) target.set(resolved, key);
3058
+ }
4109
3059
 
4110
- const summary = `${ok}/${results.length} succeeded${downgradeNote}`;
4111
- const fullContent = worktreeSuffix
4112
- ? `${summary}\n\n${aggregatedOutput}\n\n${worktreeSuffix}`
4113
- : `${summary}\n\n${aggregatedOutput}`;
3060
+ function prepareWorkflowChildLaunchParams(input: {
3061
+ workflowDefaults: SubagentParamsLike;
3062
+ childParams: Record<string, unknown>;
3063
+ parentWorkflowRunId: string;
3064
+ workflowKey: string;
3065
+ ctxCwd: string;
3066
+ workflowCwd: string;
3067
+ artifactsDir: string;
3068
+ aggregateOutputPath?: string;
3069
+ configuredOutputBaseDir?: string;
3070
+ discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[] };
3071
+ workflowAgentScope?: unknown;
3072
+ outputOverride?: string;
3073
+ options?: { missionDetached?: boolean; suppressRoutineResultIntercom?: boolean; runFanoutBudget?: RunFanoutBudgetDescriptor; parentDeadlineAt?: number };
3074
+ }): SubagentParamsLike {
3075
+ let childParams = input.childParams;
3076
+ if (input.childParams.output === undefined && input.childParams.resume === undefined && input.outputOverride !== undefined) {
3077
+ childParams = { ...input.childParams, output: input.outputOverride };
3078
+ } else if (input.childParams.output === undefined && input.childParams.resume === undefined && input.aggregateOutputPath !== undefined) {
3079
+ childParams = { ...input.childParams, output: workflowChildDefaultOutput(input.aggregateOutputPath, input.artifactsDir, input.parentWorkflowRunId, input.workflowKey) };
3080
+ } else if (input.childParams.resume === undefined) {
3081
+ const resolvedOutput = resolveWorkflowChildOutputPath({ ctxCwd: input.ctxCwd, workflowCwd: input.workflowCwd, artifactsDir: input.artifactsDir, workflowRunId: input.parentWorkflowRunId, aggregateOutputPath: input.aggregateOutputPath, configuredOutputBaseDir: input.configuredOutputBaseDir, discoverAgents: input.discoverAgents, workflowAgentScope: input.workflowAgentScope, key: input.workflowKey, params: input.childParams });
3082
+ if (resolvedOutput.path) childParams = { ...input.childParams, output: resolvedOutput.path };
3083
+ }
3084
+ return prepareWorkflowLaunchParams(input.workflowDefaults, childParams, input.parentWorkflowRunId, input.workflowKey, input.options);
3085
+ }
4114
3086
 
3087
+ function finalizeSingleWorktreeHandoff(input: {
3088
+ worktreeSetup: WorktreeSetup;
3089
+ artifactsDir: string;
3090
+ runId: string;
3091
+ cwd: string;
3092
+ agent: string;
3093
+ result: SingleResult;
3094
+ }): { suffix: string; reference?: NonNullable<Details["parallelHandoff"]> } {
3095
+ const diffsDir = path.join(input.artifactsDir, "worktree-diffs", input.runId);
3096
+ const diffs = diffWorktrees(input.worktreeSetup, [input.agent], diffsDir);
3097
+ const diffSummary = formatWorktreeDiffSummary(diffs);
3098
+ const manifestPath = parallelHandoffPath(input.artifactsDir, input.runId);
3099
+ const handoff = {
3100
+ manifestPath,
3101
+ runId: input.runId,
3102
+ mode: "single" as const,
3103
+ source: "foreground" as const,
3104
+ cwd: input.cwd,
3105
+ stepIndex: 0,
3106
+ flatStartIndex: 0,
3107
+ setup: input.worktreeSetup,
3108
+ diffs,
3109
+ results: [{
3110
+ agent: input.result.agent,
3111
+ status: resolveSubagentResultStatus(omitUndefinedProperties({
3112
+ exitCode: input.result.exitCode,
3113
+ interrupted: input.result.interrupted,
3114
+ detached: input.result.detached,
3115
+ state: input.result.stopped ? "stopped" : undefined,
3116
+ processSignal: input.result.processSignal,
3117
+ timedOut: input.result.timedOut,
3118
+ stopped: input.result.stopped,
3119
+ turnBudgetExceeded: input.result.turnBudgetExceeded,
3120
+ })),
3121
+ summary: resultSummaryForIntercom(input.result),
3122
+ ...(input.result.artifactPaths?.outputPath ? { outputPath: input.result.artifactPaths.outputPath } : {}),
3123
+ ...(input.result.structuredOutput !== undefined ? { structuredOutput: input.result.structuredOutput } : {}),
3124
+ ...(input.result.structuredOutputPath ? { structuredOutputPath: input.result.structuredOutputPath } : {}),
3125
+ ...(input.result.sessionFile ? { sessionPath: input.result.sessionFile } : {}),
3126
+ }],
3127
+ };
3128
+ try {
3129
+ writeParallelHandoffGroup(handoff);
3130
+ const cleanup = cleanupWorktrees(input.worktreeSetup, { kind: "preserve", capturedDiffs: diffs, handoffManifestPath: manifestPath });
3131
+ const reference = writeParallelHandoffGroup({ ...handoff, cleanup });
4115
3132
  return {
4116
- content: [{ type: "text", text: fullContent }],
4117
- details,
3133
+ suffix: [diffSummary, formatParallelHandoffReference(reference)].filter(Boolean).join("\n\n"),
3134
+ reference,
4118
3135
  };
4119
- } finally {
4120
- if (worktreeSetup && !worktreeCleanupHandled) cleanupWorktrees(worktreeSetup);
3136
+ } catch (error) {
3137
+ return { suffix: [diffSummary, formatParallelHandoffError(error)].filter(Boolean).join("\n\n") };
4121
3138
  }
4122
3139
  }
4123
3140
 
@@ -4167,6 +3184,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
4167
3184
  currentProvider,
4168
3185
  data.modelScope === undefined ? {} : { scope: data.modelScope },
4169
3186
  );
3187
+ const modelOverrideFromParent = inheritsParentModel(params.model as string | undefined, agentConfig.model, parentModel);
4170
3188
  let skillOverride: string[] | false | undefined = normalizeSkillInput(params.skill);
4171
3189
  let readsOverride: string[] | false | undefined = params.reads;
4172
3190
  const rawOutput = params.output !== undefined ? params.output : agentConfig.output;
@@ -4175,109 +3193,35 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
4175
3193
  const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
4176
3194
  const maxSubagentDepth = resolveChildMaxSubagentDepth(currentMaxSubagentDepth, agentConfig.maxSubagentDepth);
4177
3195
 
4178
- if (params.clarify === true && ctx.hasUI) {
4179
- const behavior = resolveStepBehavior(agentConfig, omitUndefinedProperties({ output: effectiveOutput, skills: skillOverride }));
4180
- const availableSkills = discoverAvailableSkills(effectiveCwd);
4181
-
4182
- const result = await ctx.ui.custom<ChainClarifyResult>(
4183
- (tui, theme, _kb, done) =>
4184
- new ChainClarifyComponent(
4185
- tui, theme,
4186
- [agentConfig],
4187
- [task],
4188
- task,
4189
- undefined,
4190
- [behavior],
4191
- availableModels,
4192
- currentProvider,
4193
- availableSkills,
4194
- done,
4195
- "single",
4196
- ),
4197
- { overlay: true, overlayOptions: { anchor: "center", width: 84, maxHeight: "80%" } },
4198
- );
4199
-
4200
- if (!result || !result.confirmed) {
4201
- return { content: [{ type: "text", text: "Cancelled" }], details: { mode: "single", results: [] } };
4202
- }
4203
-
4204
- task = result.templates[0]!;
4205
- const override = result.behaviorOverrides[0];
4206
- if (override?.model !== undefined) modelOverride = resolveEffectiveSubagentModel(override.model, agentConfig.model, parentModel, availableModels, currentProvider, data.modelScope === undefined ? {} : { scope: data.modelScope });
4207
- if (override?.output !== undefined) effectiveOutput = normalizeSingleOutputOverride(override.output, agentConfig.output);
4208
- if (override?.skills !== undefined) skillOverride = override.skills;
4209
- if (override?.reads !== undefined) readsOverride = override.reads;
4210
3196
 
4211
- if (result.runInBackground) {
4212
- if (!isAsyncAvailable()) {
4213
- return {
4214
- content: [{ type: "text", text: "Background mode requires upstream jiti for TypeScript execution but it could not be found. Ensure the pi-subagents package dependencies are installed." }],
4215
- isError: true,
4216
- details: { mode: "single" as const, results: [] },
4217
- };
4218
- }
4219
- const id = data.asyncRunId;
4220
- const capacity = acquireClarifiedBackgroundCapacity(data, deps, "single", id);
4221
- if (capacity.errorResult) return capacity.errorResult;
4222
- const asyncCtx = compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
4223
- pi: deps.pi,
4224
- cwd: ctx.cwd,
4225
- currentSessionId: data.parentSessionId!,
4226
- parentSessionId: data.parentPiSessionId,
4227
- currentModelProvider: parentModel?.provider,
4228
- currentModel: parentModel,
4229
- modelScope: data.modelScope,
4230
- interactive: ctx.hasUI,
4231
- permissions: deps.config.permissions,
3197
+ const sourceCwd = effectiveCwd;
3198
+ const { setup: worktreeSetup, errorResult: worktreeSetupError } = createSingleWorktreeSetup(
3199
+ params.worktree,
3200
+ sourceCwd,
3201
+ runId,
3202
+ params.agent!,
3203
+ deps.config.worktreeSetupHook,
3204
+ deps.config.worktreeSetupHookTimeoutMs,
3205
+ deps.config.worktreeBaseDir,
3206
+ );
3207
+ if (worktreeSetupError) return worktreeSetupError;
3208
+ const singleCwd = worktreeSetup?.worktrees[0]?.agentCwd ?? sourceCwd;
3209
+ let pendingHandoff: Details["parallelHandoff"];
3210
+ if (worktreeSetup) {
3211
+ try {
3212
+ pendingHandoff = writePendingParallelHandoff({
3213
+ manifestPath: parallelHandoffPath(artifactsDir, runId),
3214
+ runId,
3215
+ mode: "single",
3216
+ source: "foreground",
3217
+ cwd: sourceCwd,
3218
+ stepIndex: 0,
3219
+ flatStartIndex: 0,
3220
+ setup: worktreeSetup,
4232
3221
  });
4233
- try {
4234
- return executeAsyncSingle(id, compactOptional<Parameters<typeof executeAsyncSingle>[1]>({
4235
- agent: params.agent!,
4236
- task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(task) : task,
4237
- goal: task,
4238
- agentConfig,
4239
- recoveryAgentConfig: data.recoveryAgents.find((agent) => agent.name === params.agent),
4240
- ctx: asyncCtx,
4241
- availableModels,
4242
- cwd: effectiveCwd,
4243
- maxOutput: params.maxOutput,
4244
- artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
4245
- artifactConfig,
4246
- shareEnabled,
4247
- activeAsyncCapacity: capacity.activeAsyncCapacity,
4248
- sessionRoot,
4249
- sessionFile: sessionFileForTask(params.agent!, 0, modelOverride),
4250
- context: contextPolicy.contextForAgent(params.agent!),
4251
- skills: skillOverride === false ? [] : skillOverride,
4252
- output: effectiveOutput,
4253
- outputMode: effectiveOutputMode,
4254
- ...(readsOverride !== undefined ? { reads: readsOverride } : {}),
4255
- outputBaseDir: resolveSingleRunOutputBaseDir(deps, artifactsDir, id),
4256
- modelOverride,
4257
- thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride),
4258
- maxSubagentDepth,
4259
- waitToolEnabled: deps.waitToolEnabled,
4260
- worktreeSetupHook: deps.config.worktreeSetupHook,
4261
- worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
4262
- worktreeBaseDir: deps.config.worktreeBaseDir,
4263
- controlConfig,
4264
- intercomBridge: params.intercomBridge,
4265
- controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
4266
- childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
4267
- nestedRoute: data.nestedRoute,
4268
- agentContract: params.agentContract,
4269
- structuredOutputSchema: params.outputSchema,
4270
- acceptance: params.acceptance,
4271
- timeoutMs: data.timeoutMs,
4272
- turnBudget: data.turnBudget,
4273
- toolBudget: effectiveToolBudget.toolBudget,
4274
- usageBudget: data.usageBudget,
4275
- allowZeroToolBudget: data.allowZeroToolBudget && effectiveToolBudget.toolBudget === data.toolBudget,
4276
- runFanoutBudget: data.runFanoutBudget,
4277
- }));
4278
- } finally {
4279
- finalizeClarifiedBackgroundCapacity(data, deps, capacity.activeAsyncCapacity);
4280
- }
3222
+ } catch (error) {
3223
+ cleanupWorktrees(worktreeSetup);
3224
+ return { content: [{ type: "text", text: formatParallelHandoffError(error) }], isError: true, details: { mode: "single", results: [] } };
4281
3225
  }
4282
3226
  }
4283
3227
 
@@ -4286,9 +3230,10 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
4286
3230
  task = wrapForkTask(task);
4287
3231
  }
4288
3232
  const cleanTask = task;
4289
- const outputPath = resolveSingleOutputPath(effectiveOutput, ctx.cwd, effectiveCwd, resolveSingleRunOutputBaseDir(deps, artifactsDir, runId));
3233
+ const outputPath = resolveSingleOutputPath(effectiveOutput, ctx.cwd, singleCwd, resolveSingleRunOutputBaseDir(deps, artifactsDir, runId));
4290
3234
  const validationError = validateFileOnlyOutputMode(effectiveOutputMode, outputPath, `Single run (${params.agent})`);
4291
3235
  if (validationError) {
3236
+ if (worktreeSetup) cleanupWorktrees(worktreeSetup);
4292
3237
  return { content: [{ type: "text", text: validationError }], isError: true, details: { mode: "single", results: [] } };
4293
3238
  }
4294
3239
  const structuredRuntime = params.outputSchema
@@ -4297,7 +3242,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
4297
3242
  // Reads: caller override > agent defaultReads > none. `~`/`~/` expand to home;
4298
3243
  // absolute paths pass through; relative paths resolve against the child cwd.
4299
3244
  const reads = readsOverride !== undefined ? readsOverride : agentConfig.defaultReads ?? false;
4300
- const readPaths = Array.isArray(reads) ? resolveExistingReadPaths(reads, effectiveCwd) : [];
3245
+ const readPaths = Array.isArray(reads) ? resolveExistingReadPaths(reads, singleCwd) : [];
4301
3246
  const readsInstruction = readPaths.length > 0
4302
3247
  ? `[Read from: ${readPaths.join(", ")}]\n\n`
4303
3248
  : "";
@@ -4314,13 +3259,13 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
4314
3259
  let detachForeground: ((reason?: string) => boolean) | undefined;
4315
3260
  const foregroundControl = deps.state.foregroundControls.get(runId);
4316
3261
  if (foregroundControl) {
4317
- const thinking = resolveEffectiveThinking(modelOverride, thinkingOverrideForTask(params.agent!, 0, modelOverride));
3262
+ const thinking = resolveEffectiveThinking(modelOverride, thinkingOverrideForTask(params.agent!, 0, modelOverride, modelOverrideFromParent));
4318
3263
  beginForegroundChild(foregroundControl, omitUndefinedProperties({
4319
3264
  index: 0,
4320
3265
  agent: params.agent!,
4321
3266
  authoredTask,
4322
3267
  effectivePrompt: task,
4323
- cwd: effectiveCwd,
3268
+ cwd: singleCwd,
4324
3269
  outputPath,
4325
3270
  rerun: { params: { ...params, task: authoredTask, async: params.async ?? false } },
4326
3271
  description: foregroundControl.description,
@@ -4352,14 +3297,14 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
4352
3297
  ...workflowForegroundSteeringLaunchOptions(foregroundControl, 0),
4353
3298
  context: data.contextPolicy.contextForAgent(params.agent!),
4354
3299
  runFanoutBudget: params.runFanoutAdmitted ? data.runFanoutBudget : { ...data.runFanoutBudget, parentPath: `${data.runFanoutBudget.parentPath ? `${data.runFanoutBudget.parentPath}/` : ""}single` },
4355
- cwd: effectiveCwd,
3300
+ cwd: singleCwd,
4356
3301
  signal,
4357
3302
  interruptSignal: interruptController.signal,
4358
3303
  allowIntercomDetach: agentConfig.systemPrompt?.includes(INTERCOM_BRIDGE_MARKER) === true,
4359
3304
  intercomEvents: deps.pi.events,
4360
3305
  runId,
4361
3306
  sessionDir: sessionDirForIndex(0),
4362
- sessionFile: sessionFileForTask(params.agent!, 0, modelOverride),
3307
+ sessionFile: sessionFileForTask(params.agent!, 0, modelOverride, modelOverrideFromParent),
4363
3308
  share: shareEnabled,
4364
3309
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
4365
3310
  artifactConfig,
@@ -4376,7 +3321,8 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
4376
3321
  nestedRoute: foregroundControl?.nestedRoute,
4377
3322
  index: 0,
4378
3323
  modelOverride,
4379
- thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride),
3324
+ modelOverrideFromParent,
3325
+ thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride, modelOverrideFromParent),
4380
3326
  availableModels,
4381
3327
  preferredModelProvider: currentProvider,
4382
3328
  modelScope: data.modelScope,
@@ -4392,10 +3338,21 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
4392
3338
  onDetachedExit: (result) => {
4393
3339
  try {
4394
3340
  try {
4395
- updateRememberedForegroundChild(deps.state, { runId, mode: "single", cwd: effectiveCwd, sessionId: data.parentSessionId, index: 0, result, events: deps.pi.events, notify: params.workflowParentRunId === undefined });
3341
+ updateRememberedForegroundChild(deps.state, { runId, mode: "single", cwd: singleCwd, sessionId: data.parentSessionId, index: 0, result, events: deps.pi.events, notify: true });
4396
3342
  } catch {
4397
3343
  // Remembered foreground state is best-effort; run history and cleanup must still complete.
4398
3344
  }
3345
+ const workflowParentRunId = params.workflowParentRunId ?? foregroundControl?.parentWorkflowRunId;
3346
+ if (workflowParentRunId) {
3347
+ reconcileDetachedWorkflowChildCompletion({
3348
+ state: deps.state,
3349
+ workflowRunId: workflowParentRunId,
3350
+ childRunId: runId,
3351
+ result,
3352
+ events: deps.pi.events,
3353
+ workflowKey: params.workflowKey ?? foregroundControl?.workflowKey,
3354
+ });
3355
+ }
4399
3356
  } finally {
4400
3357
  try {
4401
3358
  if (!artifactConfig.enabled) cleanupStructuredOutputRuntime(structuredRuntime);
@@ -4419,6 +3376,9 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
4419
3376
  capabilityCeiling: data.capabilityCeiling,
4420
3377
  allowZeroToolBudget: data.allowZeroToolBudget && effectiveToolBudget.toolBudget === data.toolBudget,
4421
3378
  }));
3379
+ } catch (error) {
3380
+ if (worktreeSetup) cleanupWorktrees(worktreeSetup);
3381
+ throw error;
4422
3382
  } finally {
4423
3383
  // An attached runSync rejection still owns its child and structured runtime.
4424
3384
  // A successful detached receipt transfers both to onDetachedExit while the
@@ -4432,6 +3392,13 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
4432
3392
  recordRun(params.agent!, cleanTask, r.exitCode, r.progressSummary?.durationMs ?? 0);
4433
3393
  }
4434
3394
 
3395
+ let worktreeHandoff: ReturnType<typeof finalizeSingleWorktreeHandoff> | undefined;
3396
+ if (worktreeSetup) {
3397
+ worktreeHandoff = r.detached
3398
+ ? { suffix: pendingHandoff ? formatParallelHandoffReference(pendingHandoff) : "", reference: pendingHandoff }
3399
+ : finalizeSingleWorktreeHandoff({ worktreeSetup, artifactsDir, runId, cwd: sourceCwd, agent: params.agent!, result: r });
3400
+ }
3401
+
4435
3402
  if (r.progress) allProgress.push(r.progress);
4436
3403
  if (r.artifactPaths) allArtifactPaths.push(r.artifactPaths);
4437
3404
 
@@ -4466,8 +3433,9 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
4466
3433
  totalChildUsage: sumResultsUsage([r]),
4467
3434
  totalCost,
4468
3435
  usageBudget: usageBudgetState(data.usageBudget, totalCost),
3436
+ ...(worktreeHandoff?.reference ? { parallelHandoff: worktreeHandoff.reference } : {}),
4469
3437
  }));
4470
- rememberForegroundRun(deps.state, { runId, mode: "single", cwd: effectiveCwd, sessionId: data.parentSessionId, results: details.results });
3438
+ rememberForegroundRun(deps.state, { runId, mode: "single", cwd: singleCwd, sessionId: data.parentSessionId, results: details.results });
4471
3439
 
4472
3440
  const suppressRoutineResultIntercom = shouldSuppressRoutineResultIntercom({ suppressRoutineResultIntercom: params.suppressRoutineResultIntercom, results: [r] });
4473
3441
  if (!r.detached && !r.interrupted && !suppressRoutineResultIntercom) {
@@ -4490,6 +3458,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
4490
3458
  }
4491
3459
  }
4492
3460
 
3461
+ const worktreeSuffix = worktreeHandoff?.suffix ? `\n\n${worktreeHandoff.suffix}` : "";
4493
3462
  if (r.detached) {
4494
3463
  const statusRecovery = `subagent({ action: "status", id: "${runId}" }) to recover the result; do not resume or launch a replacement while it remains detached.`;
4495
3464
  const blockingRecovery = `subagent_wait({ id: "${runId}" }). Use ${statusRecovery}`;
@@ -4499,26 +3468,26 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
4499
3468
  ? `Detached at user request: ${params.agent}. The child continues independently. Register a completion wake-up with subagent_wait({ id: "${runId}", nonBlocking: true }), or use ${statusRecovery}`
4500
3469
  : `Detached before task completion: ${params.agent}. Wait with ${blockingRecovery}`;
4501
3470
  return {
4502
- content: [{ type: "text", text: message }],
3471
+ content: [{ type: "text", text: `${message}${worktreeSuffix}` }],
4503
3472
  details,
4504
3473
  };
4505
3474
  }
4506
3475
 
4507
3476
  if (r.interrupted) {
4508
3477
  return {
4509
- content: [{ type: "text", text: `Run paused after interrupt (${params.agent}). Waiting for explicit next action.` }],
3478
+ content: [{ type: "text", text: `Run paused after interrupt (${params.agent}). Waiting for explicit next action.${worktreeSuffix}` }],
4510
3479
  details,
4511
3480
  };
4512
3481
  }
4513
3482
 
4514
3483
  if (r.exitCode !== 0)
4515
3484
  return {
4516
- content: [{ type: "text", text: formatFailedSingleRunOutput(r, finalizedOutput.displayOutput) }],
3485
+ content: [{ type: "text", text: `${formatFailedSingleRunOutput(r, finalizedOutput.displayOutput)}${worktreeSuffix}` }],
4517
3486
  details,
4518
3487
  isError: true,
4519
3488
  };
4520
3489
  return {
4521
- content: [{ type: "text", text: finalizedOutput.displayOutput || "(no output)" }],
3490
+ content: [{ type: "text", text: `${finalizedOutput.displayOutput || "(no output)"}${worktreeSuffix}` }],
4522
3491
  details,
4523
3492
  };
4524
3493
  }
@@ -4641,13 +3610,103 @@ function workflowChildResult(key: string, result: AgentToolResult<Details>): Wor
4641
3610
  };
4642
3611
  }
4643
3612
 
3613
+ function workflowSteerReceipt(key: string, result: AgentToolResult<Details>): WorkflowSteerResult {
3614
+ const steering = result.details.steering;
3615
+ const error = result.content.map((part) => part.type === "text" ? part.text : "").filter(Boolean).join("\n") || undefined;
3616
+ if (!steering) return { key, state: "failed", ...(error ? { error } : {}) };
3617
+ const state = result.isError === true || steering.state === "failed" || steering.state === "partial"
3618
+ ? "failed"
3619
+ : steering.deliveryStatus === "delivered" ? "delivered" : "queued";
3620
+ return {
3621
+ key,
3622
+ state,
3623
+ requestId: steering.requestId,
3624
+ deliveryStatus: steering.deliveryStatus,
3625
+ targets: steering.targets.map((target) => ({ index: target.index, state: target.state, ...(target.reason ? { reason: target.reason } : {}) })),
3626
+ ...(state === "failed" && error ? { error } : {}),
3627
+ };
3628
+ }
3629
+
3630
+ export async function steerWorkflowChildByKey(input: {
3631
+ state: SubagentState;
3632
+ workflowRunId: string;
3633
+ key: string;
3634
+ message: string;
3635
+ options: WorkflowSteerOptions;
3636
+ signal?: AbortSignal;
3637
+ asyncDirRoot?: string;
3638
+ resolveRunId?: () => string | undefined;
3639
+ }): Promise<WorkflowSteerResult> {
3640
+ const asyncDirRoot = input.asyncDirRoot ?? DIRS.async;
3641
+ const ackTimeoutMs = input.options.ackTimeoutMs ?? 3_000;
3642
+ const deadline = Date.now() + ackTimeoutMs;
3643
+ while (true) {
3644
+ const control = [...input.state.foregroundControls.values()].find((candidate) => candidate.parentWorkflowRunId === input.workflowRunId
3645
+ && candidate.workflowKey === input.key
3646
+ && Boolean(candidate.workflowSteeringDir)
3647
+ && (candidate.activeChildren?.size ?? 0) > 0);
3648
+ if (control) {
3649
+ const result = await steerWorkflowForegroundTarget({
3650
+ target: { control, workflowRunId: input.workflowRunId, sourceRunId: control.runId },
3651
+ message: input.message,
3652
+ mode: input.options.mode,
3653
+ index: input.options.index,
3654
+ ackTimeoutMs: Math.max(1, deadline - Date.now()),
3655
+ signal: input.signal,
3656
+ });
3657
+ return workflowSteerReceipt(input.key, result);
3658
+ }
3659
+
3660
+ const workflowStatus = readStatus(path.join(asyncDirRoot, input.workflowRunId));
3661
+ const step = workflowStatus?.steps?.find((candidate) => candidate.workflowKey === input.key);
3662
+ const childRunId = step?.runId ?? input.resolveRunId?.();
3663
+ if (childRunId) {
3664
+ const asyncDir = path.join(asyncDirRoot, childRunId);
3665
+ const childStatus = reconcileAsyncRun(asyncDir).status;
3666
+ if (childStatus && childStatus.state !== "running" && childStatus.state !== "queued" && (input.options.mode !== "follow_up" || !canQueueRetainedAsyncFollowUp(childStatus, input.options.index))) {
3667
+ return { key: input.key, state: "missed", error: `Workflow child '${input.key}' is ${childStatus.state}.` };
3668
+ }
3669
+ if (childStatus) {
3670
+ const result = await steerAsyncRun({
3671
+ state: input.state,
3672
+ runId: childRunId,
3673
+ message: input.message,
3674
+ mode: input.options.mode,
3675
+ index: input.options.index,
3676
+ ackTimeoutMs: Math.max(1, deadline - Date.now()),
3677
+ location: { asyncDir },
3678
+ signal: input.signal,
3679
+ });
3680
+ return workflowSteerReceipt(input.key, result);
3681
+ }
3682
+ }
3683
+ if (step && step.status !== "running" && step.status !== "pending") {
3684
+ return { key: input.key, state: "missed", error: `Workflow child '${input.key}' is ${step.status}.` };
3685
+ }
3686
+ if (workflowStatus && workflowStatus.state !== "running" && workflowStatus.state !== "queued") {
3687
+ return { key: input.key, state: "missed", error: `Workflow '${input.workflowRunId}' is ${workflowStatus.state}.` };
3688
+ }
3689
+ if (input.signal?.aborted || Date.now() >= deadline) {
3690
+ return { key: input.key, state: "missed", error: `Workflow child '${input.key}' had no live steering route.` };
3691
+ }
3692
+ await new Promise<void>((resolve) => setTimeout(resolve, Math.min(10, Math.max(1, deadline - Date.now()))));
3693
+ }
3694
+ }
3695
+
4644
3696
  export function prepareWorkflowLaunchParams(
4645
3697
  workflowDefaults: SubagentParamsLike,
4646
3698
  childParams: Record<string, unknown>,
4647
3699
  parentWorkflowRunId: string,
4648
3700
  workflowKey: string,
4649
- options: { missionDetached?: boolean; suppressRoutineResultIntercom?: boolean; runFanoutBudget?: RunFanoutBudgetDescriptor } = {},
3701
+ options: { missionDetached?: boolean; suppressRoutineResultIntercom?: boolean; runFanoutBudget?: RunFanoutBudgetDescriptor; parentDeadlineAt?: number } = {},
4650
3702
  ): SubagentParamsLike {
3703
+ const parentTimeoutMs = options.parentDeadlineAt === undefined
3704
+ || childParams.timeoutMs !== undefined
3705
+ || childParams.maxRuntimeMs !== undefined
3706
+ || workflowDefaults.timeoutMs !== undefined
3707
+ || workflowDefaults.maxRuntimeMs !== undefined
3708
+ ? undefined
3709
+ : Math.max(1, options.parentDeadlineAt - Date.now());
4651
3710
  if (typeof childParams.resume === "string") {
4652
3711
  if (childParams.gate !== undefined || workflowDefaults.gate !== undefined) {
4653
3712
  throw new Error("gate is not supported with retained resume; resume uses the retained child contract.");
@@ -4656,12 +3715,15 @@ export function prepareWorkflowLaunchParams(
4656
3715
  const turnBudget = childParams.turnBudget ?? workflowDefaults.turnBudget;
4657
3716
  const toolBudget = childParams.toolBudget ?? workflowDefaults.toolBudget;
4658
3717
  const intercomBridge = childParams.intercomBridge ?? workflowDefaults.intercomBridge;
3718
+ const worktree = childParams.worktree ?? workflowDefaults.worktree;
4659
3719
  return {
4660
3720
  action: "resume",
4661
3721
  id: childParams.resume.trim(),
3722
+ ...(typeof childParams.index === "number" && Number.isInteger(childParams.index) ? { index: childParams.index } : {}),
4662
3723
  message: typeof childParams.task === "string" ? childParams.task.trim() : "",
4663
3724
  workflowParentRunId: parentWorkflowRunId,
4664
3725
  workflowKey,
3726
+ ...(worktree !== undefined ? { worktree: worktree as boolean } : {}),
4665
3727
  ...(options.runFanoutBudget ? { runFanoutBudget: { ...options.runFanoutBudget, parentPath: `${options.runFanoutBudget.parentPath ? `${options.runFanoutBudget.parentPath}/` : ""}workflow[${workflowKey}]` } } : {}),
4666
3728
  ...(options.missionDetached ? { mission: false } : {}),
4667
3729
  ...(timeoutMs !== undefined ? { timeoutMs: timeoutMs as number } : {}),
@@ -4677,12 +3739,13 @@ export function prepareWorkflowLaunchParams(
4677
3739
  ...(options.missionDetached ? { mission: false } : {}),
4678
3740
  workflowParentRunId: parentWorkflowRunId,
4679
3741
  workflowKey,
3742
+ ...(parentTimeoutMs !== undefined ? { workflowParentDeadlineAt: options.parentDeadlineAt } : {}),
4680
3743
  ...(options.runFanoutBudget ? { runFanoutBudget: { ...options.runFanoutBudget, parentPath: `${options.runFanoutBudget.parentPath ? `${options.runFanoutBudget.parentPath}/` : ""}workflow[${workflowKey}]` } } : {}),
4681
3744
  ...(options.suppressRoutineResultIntercom ? { suppressRoutineResultIntercom: true } : {}),
4682
3745
  } as SubagentParamsLike;
4683
3746
  const normalizedGate = normalizeGateParams(launchParams);
4684
3747
  if (!normalizedGate.ok) throw new Error(normalizedGate.error);
4685
- return prepareWorkflowChildParams(normalizedGate.params);
3748
+ return normalizedGate.params;
4686
3749
  }
4687
3750
 
4688
3751
  type GateParamsNormalizationResult =
@@ -4700,41 +3763,6 @@ function normalizeGateParams(params: SubagentParamsLike): GateParamsNormalizatio
4700
3763
  return { ok: true, params: { ...rest, ...(normalized.acceptance !== undefined ? { acceptance: normalized.acceptance } : {}) } };
4701
3764
  }
4702
3765
 
4703
- function prepareWorkflowChildParams(params: SubagentParamsLike): SubagentParamsLike {
4704
- if (params.worktree !== true || !params.agent) return params;
4705
- const {
4706
- agent,
4707
- task = "",
4708
- model,
4709
- skill,
4710
- output,
4711
- outputMode,
4712
- outputSchema,
4713
- acceptance,
4714
- agentContract,
4715
- toolBudget,
4716
- reads,
4717
- ...runParams
4718
- } = params;
4719
- return {
4720
- ...runParams,
4721
- worktree: true,
4722
- tasks: [{
4723
- agent,
4724
- task,
4725
- ...(model !== undefined ? { model } : {}),
4726
- ...(skill !== undefined ? { skill } : {}),
4727
- ...(output !== undefined ? { output } : {}),
4728
- ...(reads !== undefined ? { reads } : {}),
4729
- ...(outputMode !== undefined ? { outputMode } : {}),
4730
- ...(outputSchema !== undefined ? { outputSchema } : {}),
4731
- ...(acceptance !== undefined ? { acceptance } : {}),
4732
- ...(agentContract !== undefined ? { agentContract } : {}),
4733
- ...(toolBudget !== undefined ? { toolBudget } : {}),
4734
- }],
4735
- };
4736
- }
4737
-
4738
3766
  function formatWorkflowValue(value: unknown): string {
4739
3767
  if (value === undefined) return "(undefined)";
4740
3768
  if (typeof value === "string") return value;
@@ -4865,7 +3893,6 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4865
3893
  const warning = getProjectArtifactPackagingWarning(workflowCwd);
4866
3894
  if (warning) console.warn(`[pi-subagents] ${warning}`);
4867
3895
  }
4868
- const workflowPrompts = { render: (ref: string, vars?: unknown) => renderWorkflowPrompt(ref, vars, workflowCwd) };
4869
3896
  const chatProgressResult = resolveWorkflowChatProgress({ requested: requestParams.chatProgress, parentCwd, workflowCwd, background: requestParams.async !== false });
4870
3897
  if (chatProgressResult.error) return { content: [{ type: "text", text: chatProgressResult.error }], isError: true, details: { mode: "workflow", results: [] } };
4871
3898
  const chatProgress = chatProgressResult.projection!;
@@ -4942,12 +3969,17 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4942
3969
  };
4943
3970
  if (workflowRunId) {
4944
3971
  const toolCallId = _id;
3972
+ const workflowSessionRoot = requestParams.sessionDir
3973
+ ? path.resolve(deps.expandTilde(requestParams.sessionDir))
3974
+ : trustedSessionRootsForStatus(ctx, deps)[0];
4945
3975
  const asyncDir = path.join(DIRS.async, workflowRunId);
4946
3976
  const resultPath = resultFilePath(DIRS.results, workflowRunId);
4947
3977
  const statusPath = path.join(asyncDir, "status.json");
4948
3978
  const eventsPath = path.join(asyncDir, "events.jsonl");
4949
3979
  const startedAt = Date.now();
4950
3980
  const currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
3981
+ const completionOwnerId = deps.state.completionOwnerId ?? currentCompletionOwnerId();
3982
+ deps.state.completionOwnerId = completionOwnerId;
4951
3983
  try {
4952
3984
  fs.mkdirSync(asyncDir, { recursive: true });
4953
3985
  writeRunFanoutBudgetDescriptor(asyncDir, workflowFanoutBudget);
@@ -4965,51 +3997,115 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4965
3997
  runId: workflowRunId,
4966
3998
  toolCallId,
4967
3999
  sessionId: currentSessionId ?? undefined,
4000
+ completionOwnerId,
4968
4001
  mode: "workflow",
4969
4002
  state: "running",
4970
4003
  startedAt,
4971
4004
  lastUpdate: startedAt,
4972
4005
  ...(timeout !== undefined ? { deadlineAt: startedAt + timeout, timeoutMs: timeout } : {}),
4973
4006
  cwd: workflowCwd,
4007
+ ...(workflowSessionRoot ? { sessionRoot: workflowSessionRoot } : {}),
4008
+ ...(requestParams.scheduleOrigin ? { scheduleOrigin: requestParams.scheduleOrigin } : {}),
4974
4009
  pid: process.pid,
4975
4010
  steps: [],
4976
4011
  workflow: { trace: [], emits: [], console: [] },
4977
4012
  runFanoutBudget: getRunFanoutBudgetSnapshot(workflowFanoutBudget),
4978
4013
  };
4979
- const appendWorkflowEvent = (event: Record<string, unknown>) => fs.appendFileSync(eventsPath, `${JSON.stringify({ ts: Date.now(), runId: workflowRunId, ...event })}\n`, "utf-8");
4014
+ const appendWorkflowEvent = (event: Record<string, unknown>) => {
4015
+ try {
4016
+ fs.appendFileSync(eventsPath, `${JSON.stringify({ ts: Date.now(), runId: workflowRunId, ...event })}\n`, "utf-8");
4017
+ } catch (error) {
4018
+ // The event log is a journal, not workflow truth. Callers append from
4019
+ // inside run-result handling, so losing an entry to a full disk or to a
4020
+ // transient Windows lock must not fail the run being recorded.
4021
+ if (!isStorageCapacityError(error) && !isRetryableFileSystemError(error)) throw error;
4022
+ console.error(`Failed to append async workflow event '${eventsPath}':`, error);
4023
+ }
4024
+ };
4980
4025
  let indexedState: AsyncStatus["state"] | undefined;
4981
- const persist = () => {
4026
+ const indexPersistence = createCapacityResilientJsonWriter({
4027
+ keepAlive: true,
4028
+ onSuccess: (_filePath, payload) => { indexedState = (payload as { state: AsyncStatus["state"] }).state; },
4029
+ onError: (error, filePath) => console.error(`Failed to update async workflow index '${filePath}':`, error),
4030
+ });
4031
+ const queueActiveRunIndex = (): void => {
4032
+ const state = status.state;
4033
+ if (indexedState === state && indexPersistence.pendingCount() === 0) return;
4034
+ indexPersistence.write(asyncDir, { state, toolCallId: status.toolCallId }, (_filePath, payload) => {
4035
+ const indexPayload = payload as { state: AsyncStatus["state"]; toolCallId?: string };
4036
+ updateActiveRunIndex(asyncDir, indexPayload.state, indexPayload.toolCallId, { retryCapacityErrors: true });
4037
+ });
4038
+ };
4039
+ const runPersistence = createCapacityResilientJsonWriter({
4040
+ keepAlive: true,
4041
+ onSuccess: (filePath) => { if (filePath === statusPath) queueActiveRunIndex(); },
4042
+ onError: (error, filePath) => console.error(`Failed to persist async workflow state '${filePath}':`, error),
4043
+ write: (filePath, payload) => filePath === resultPath
4044
+ ? writeAsyncResultFile(filePath, payload as Record<string, unknown>)
4045
+ : writeAtomicJson(filePath, payload),
4046
+ });
4047
+ let initialPersistenceComplete = false;
4048
+ let persistClosed = false;
4049
+ let statusPersistenceDegraded = false;
4050
+ // Progress journalling runs from admit, launch, the launch and progress observers,
4051
+ // onTrace and onEmit -- all inside the promises a workflowScript awaits. Those
4052
+ // callers pass tolerateStatusWriteFailure so a transient lock on status.json cannot
4053
+ // mark a finished child failed or abort its still-running siblings. Initial and
4054
+ // terminal writes stay fail-fast on purpose: no child work is at risk by then, and
4055
+ // silently dropping a terminal write would leave status.json and the active-run
4056
+ // index pinned at "running" after the result already says complete.
4057
+ const persist = (options: { tolerateStatusWriteFailure?: boolean } = {}) => {
4058
+ if (persistClosed) return;
4059
+ const liveJob = deps.state.asyncJobs.get(workflowRunId);
4060
+ if (liveJob && (liveJob.status === "complete" || liveJob.status === "failed") && status.state !== "complete" && status.state !== "failed") return;
4982
4061
  status.lastUpdate = Date.now();
4983
- writeAtomicJson(statusPath, status);
4984
- if (indexedState !== status.state) {
4985
- updateActiveRunIndex(asyncDir, status.state, status.toolCallId);
4986
- indexedState = status.state;
4062
+ if (!initialPersistenceComplete) {
4063
+ writeAtomicJson(statusPath, status);
4064
+ initialPersistenceComplete = true;
4065
+ queueActiveRunIndex();
4066
+ } else if (options.tolerateStatusWriteFailure) {
4067
+ try {
4068
+ runPersistence.write(statusPath, status);
4069
+ statusPersistenceDegraded = false;
4070
+ } catch (error) {
4071
+ const message = `Failed to persist async workflow state ${statusPath}: ${error instanceof Error ? error.message : String(error)}`;
4072
+ console.error(message, error);
4073
+ if (!statusPersistenceDegraded) {
4074
+ statusPersistenceDegraded = true;
4075
+ try {
4076
+ appendWorkflowEvent({ type: "subagent.workflow.status_write_failed", error: message });
4077
+ } catch (eventError) {
4078
+ console.error(`Failed to record degraded status persistence for '${statusPath}':`, eventError);
4079
+ }
4080
+ }
4081
+ }
4082
+ } else {
4083
+ runPersistence.write(statusPath, status);
4987
4084
  }
4988
- const job = deps.state.asyncJobs.get(workflowRunId);
4989
- if (job) {
4990
- job.status = status.state;
4991
- job.updatedAt = status.lastUpdate;
4992
- job.activityState = status.activityState;
4993
- job.lastActivityAt = status.lastActivityAt;
4994
- job.currentTool = status.currentTool;
4995
- job.currentToolStartedAt = status.currentToolStartedAt;
4996
- job.currentPath = status.currentPath;
4997
- job.turnCount = status.turnCount;
4998
- job.toolCount = status.toolCount;
4999
- job.currentStep = status.currentStep;
4085
+ if (liveJob) {
4086
+ liveJob.status = status.state;
4087
+ liveJob.updatedAt = status.lastUpdate;
4088
+ liveJob.activityState = status.activityState;
4089
+ liveJob.lastActivityAt = status.lastActivityAt;
4090
+ liveJob.currentTool = status.currentTool;
4091
+ liveJob.currentToolStartedAt = status.currentToolStartedAt;
4092
+ liveJob.currentPath = status.currentPath;
4093
+ liveJob.turnCount = status.turnCount;
4094
+ liveJob.toolCount = status.toolCount;
4095
+ liveJob.currentStep = status.currentStep;
5000
4096
  if (status.steps) {
5001
- job.steps = status.steps.map((step, index) => ({ ...step, index }));
5002
- job.agents = status.steps.map((step) => step.agent);
4097
+ liveJob.steps = status.steps.map((step, index) => ({ ...step, index }));
4098
+ liveJob.agents = status.steps.map((step) => step.agent);
5003
4099
  } else {
5004
- delete job.steps;
5005
- delete job.agents;
4100
+ delete liveJob.steps;
4101
+ delete liveJob.agents;
5006
4102
  }
5007
- job.workflow = status.workflow;
4103
+ liveJob.workflow = status.workflow;
5008
4104
  }
5009
4105
  };
5010
4106
  const writeWorkflowResult = (payload: Record<string, unknown>): boolean => {
5011
4107
  try {
5012
- writeAsyncResultFile(resultPath, payload);
4108
+ runPersistence.write(resultPath, payload);
5013
4109
  return true;
5014
4110
  } catch (error) {
5015
4111
  const message = `Failed to write async workflow result ${resultPath}: ${error instanceof Error ? error.message : String(error)}`;
@@ -5039,20 +4135,33 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5039
4135
  status.toolCount = toolCounts.length > 0 ? toolCounts.reduce((total, count) => total + count, 0) : undefined;
5040
4136
  status.currentStep = runningSteps.length === 1 ? steps.indexOf(runningSteps[0]!) : undefined;
5041
4137
  };
5042
- const workflowJob: AsyncJobState = { asyncId: workflowRunId, asyncDir, toolCallId, cwd: workflowCwd, status: "running", sessionId: currentSessionId ?? undefined, mode: "workflow", agents: [], steps: [], startedAt, updatedAt: startedAt, ...(timeout !== undefined ? { timeoutMs: timeout, deadlineAt: startedAt + timeout } : {}), workflow: status.workflow };
4138
+ const workflowJob: AsyncJobState = { asyncId: workflowRunId, asyncDir, toolCallId, cwd: workflowCwd, ...(workflowSessionRoot ? { sessionRoot: workflowSessionRoot } : {}), status: "running", sessionId: currentSessionId ?? undefined, mode: "workflow", agents: [], steps: [], startedAt, updatedAt: startedAt, ...(requestParams.scheduleOrigin ? { scheduleOrigin: requestParams.scheduleOrigin } : {}), ...(timeout !== undefined ? { timeoutMs: timeout, deadlineAt: startedAt + timeout } : {}), workflow: status.workflow };
5043
4139
  deps.state.asyncJobs.set(workflowRunId, workflowJob);
5044
4140
  deps.state.fleetJobs ??= new Map();
5045
4141
  deps.state.fleetJobs.set(workflowRunId, workflowJob);
5046
- persist();
4142
+ try {
4143
+ persist();
4144
+ } catch (error) {
4145
+ deps.state.workflowControllers?.delete(workflowRunId);
4146
+ deps.state.asyncJobs.delete(workflowRunId);
4147
+ deps.state.fleetJobs?.delete(workflowRunId);
4148
+ workflowCapacity?.rollback();
4149
+ indexPersistence.dispose();
4150
+ return { content: [{ type: "text", text: `Failed to create async workflow storage: ${error instanceof Error ? error.message : String(error)}` }], isError: true, details: { mode: "workflow", results: [] } };
4151
+ }
5047
4152
  appendWorkflowEvent({ type: "subagent.workflow.started" });
5048
4153
  const { workflowScript, async: _workflowAsync, chatProgress: _chatProgress, ...workflowRequest } = requestParams;
5049
4154
  void Promise.resolve().then(async () => {
4155
+ const workflowDeadlineAt = timeout === undefined ? undefined : Date.now() + timeout;
5050
4156
  const workflowResults: SingleResult[] = [];
4157
+ const workflowChildRunIds = new Map<string, string>();
5051
4158
  const { action: _action, agent: _agent, task: _task, resume: _resume, tasks: _tasks, chain: _chain, concurrency: _concurrency, foregroundOnly: _foregroundOnly, clarify: _clarify, timeoutMs: _timeoutMs, maxRuntimeMs: _maxRuntimeMs, usageBudget: _usageBudget, missionId: _missionId, mission: _mission, ...workflowChildDefaults } = workflowRequest;
5052
4159
  const workflowOutput = typeof workflowChildDefaults.output === "string" || typeof workflowChildDefaults.output === "boolean" ? workflowChildDefaults.output : undefined;
5053
4160
  const configuredOutputBaseDir = resolveConfiguredSingleRunOutputBaseDir(deps);
5054
4161
  const workflowAggregateOutputPath = resolveWorkflowAggregateOutputPath(workflowOutput, ctx.cwd, workflowCwd, resolveSingleRunOutputBaseDir(deps, workflowArtifactsDir, workflowRunId), configuredOutputBaseDir);
5055
4162
  const claimedOutputPaths = new Map<string, string>();
4163
+ const childOutputOverrides = new Map<string, string>();
4164
+ const producedChildOutputPaths = new Set<string>();
5056
4165
  const workflowSteps = new Map<string, NonNullable<AsyncStatus["steps"]>[number]>();
5057
4166
  let projectedTraceLength = 0;
5058
4167
  let projectedTraceTail: NonNullable<Details["workflow"]>["trace"][number] | undefined;
@@ -5114,7 +4223,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5114
4223
  projectedTraceLength = trace.length;
5115
4224
  projectedTraceTail = trace.at(-1);
5116
4225
  projectWorkflowActivity();
5117
- persist();
4226
+ persist({ tolerateStatusWriteFailure: true });
5118
4227
  appendWorkflowEvent({ type: "subagent.workflow.trace", trace });
5119
4228
  };
5120
4229
  try {
@@ -5122,20 +4231,20 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5122
4231
  script: workflowScript,
5123
4232
  timeoutMs: timeout,
5124
4233
  signal: controller.signal,
5125
- prompts: workflowPrompts,
5126
4234
  ...(workflowState ? { state: workflowState } : {}),
5127
4235
  onTrace: updateTrace,
5128
4236
  admit: (calls) => {
5129
- const outputClaims = workflowChildOutputClaims({ ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, workflowRunId, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: deps.discoverAgents, workflowAgentScope: workflowChildDefaults.agentScope, claimedOutputPaths, entries: calls });
4237
+ const outputClaims = workflowChildOutputClaims({ ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, workflowRunId, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: deps.discoverAgents, workflowAgentScope: workflowChildDefaults.agentScope, state: deps.state, claimedOutputPaths, entries: calls });
5130
4238
  if (outputClaims.error) throw new Error(outputClaims.error);
5131
4239
  status.runFanoutBudget = claimRunFanoutBatch(workflowFanoutBudget, calls.map(({ key }) => `workflow[${key}]`));
5132
4240
  if (outputClaims.claims) applyWorkflowChildOutputClaims(claimedOutputPaths, outputClaims.claims);
5133
- persist();
4241
+ if (outputClaims.overrides) for (const [key, output] of outputClaims.overrides) childOutputOverrides.set(key, output);
4242
+ persist({ tolerateStatusWriteFailure: true });
5134
4243
  },
5135
4244
  onEmit: (emits) => {
5136
4245
  // Each emit is validated at the host boundary in runWorkflowScript before onEmit fires.
5137
4246
  status.workflow = { ...(status.workflow ?? { trace: [], console: [] }), emits };
5138
- persist();
4247
+ persist({ tolerateStatusWriteFailure: true });
5139
4248
  appendWorkflowEvent({ type: "subagent.workflow.emit", value: emits.at(-1) });
5140
4249
  },
5141
4250
  launch: async (key, childParams, workflowSignal, admission) => {
@@ -5153,7 +4262,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5153
4262
  });
5154
4263
  const result = await runMissionWorkflowChild(missionBinding, workflowRunId, key, childPhase, () => {
5155
4264
  const childRequest = bindMissionWorkflowChildAsyncLaunch(
5156
- { ...prepareWorkflowChildLaunchParams({ workflowDefaults: workflowChildDefaults, childParams, parentWorkflowRunId: workflowRunId, workflowKey: key, ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: deps.discoverAgents, workflowAgentScope: workflowChildDefaults.agentScope, options: { missionDetached: detachWorkflowChildMissions, runFanoutBudget: workflowFanoutBudget } }), runFanoutAdmitted: admission.admitted },
4265
+ { ...prepareWorkflowChildLaunchParams({ workflowDefaults: workflowChildDefaults, childParams, parentWorkflowRunId: workflowRunId, workflowKey: key, ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: deps.discoverAgents, workflowAgentScope: workflowChildDefaults.agentScope, outputOverride: childOutputOverrides.get(key), options: { missionDetached: detachWorkflowChildMissions, runFanoutBudget: workflowFanoutBudget, parentDeadlineAt: workflowDeadlineAt } }), runFanoutAdmitted: admission.admitted },
5157
4266
  missionBinding,
5158
4267
  deps.asyncByDefault,
5159
4268
  );
@@ -5164,7 +4273,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5164
4273
  step.sessionFile = launch.sessionFile;
5165
4274
  step.async = launch.async;
5166
4275
  if (launch.runId) step.runId = launch.runId;
5167
- persist();
4276
+ persist({ tolerateStatusWriteFailure: true });
5168
4277
  }
5169
4278
  recordMissionWorkflowChild(missionBinding, workflowRunId, key, { status: "running", agent: launch.agent, ...(launch.sessionFile ? { sessionPath: launch.sessionFile } : {}) });
5170
4279
  });
@@ -5187,7 +4296,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5187
4296
  step.thinking = progress.thinking;
5188
4297
  step.error = progress.error;
5189
4298
  projectWorkflowActivity();
5190
- persist();
4299
+ persist({ tolerateStatusWriteFailure: true });
5191
4300
  recordMissionWorkflowChild(missionBinding, workflowRunId, key, {
5192
4301
  status: step.status,
5193
4302
  heartbeat: { status: step.status, ...(childPhase ? { phase: childPhase } : {}) },
@@ -5195,7 +4304,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5195
4304
  }, ctx, preserveActiveSession);
5196
4305
  });
5197
4306
  workflowResults.push(...result.details.results);
4307
+ for (const childResult of result.details.results) {
4308
+ if (childResult.savedOutputPath) producedChildOutputPaths.add(childResult.savedOutputPath);
4309
+ }
5198
4310
  const child = workflowChildResult(key, result);
4311
+ if (child.runId) workflowChildRunIds.set(key, child.runId);
5199
4312
  const step = status.steps?.find((candidate) => candidate.workflowKey === key);
5200
4313
  if (step) {
5201
4314
  step.async = Boolean(result.details.asyncId || result.details.asyncDir);
@@ -5219,16 +4332,18 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5219
4332
  return child;
5220
4333
  },
5221
4334
  status: async (keyOrRunId, workflowSignal) => workflowChildResult(keyOrRunId, await execute(randomUUID(), { action: "status", id: keyOrRunId }, workflowSignal, undefined, ctx, preserveActiveSession)),
4335
+ steer: (key, message, options, workflowSignal) => steerWorkflowChildByKey({ state: deps.state, workflowRunId, key, message, options, signal: workflowSignal, resolveRunId: () => workflowChildRunIds.get(key) }),
5222
4336
  });
5223
4337
  const returnPreview = formatWorkflowValue(workflow.value).slice(0, 1_000);
5224
4338
  const emitPreview = workflow.emits.length > 0 ? ` Emitted: ${workflow.emits.map(formatWorkflowValue).join(", ").slice(0, 1_000)}` : "";
5225
4339
  const summary = `Workflow completed with ${workflow.children.length} child run(s). Return: ${returnPreview}${emitPreview} Trace: ${workflow.trace.length} event(s).`;
5226
- const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, summary);
4340
+ const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, summary, producedChildOutputPaths);
5227
4341
  const resultSummary = appendWorkflowOutputWarning(summary, outputWarning);
5228
4342
  const workflowUsage = sumResultsUsage(workflowResults);
5229
4343
  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) };
5230
- if (!writeWorkflowResult({ id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: true, state: "complete", summary: resultSummary, output: resultSummary, results: workflow.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), 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: workflowCwd, sessionId: currentSessionId, timestamp: Date.now(), durationMs: Date.now() - startedAt })) return;
4344
+ if (!writeWorkflowResult({ id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: true, state: "complete", summary: resultSummary, output: resultSummary, results: workflow.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), 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: workflowCwd, sessionId: currentSessionId, completionOwnerId, ...(requestParams.scheduleOrigin ? { scheduleOrigin: requestParams.scheduleOrigin } : {}), timestamp: Date.now(), durationMs: Date.now() - startedAt })) return;
5231
4345
  persist();
4346
+ persistClosed = true;
5232
4347
  appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, ...(status.error ? { error: status.error } : {}) });
5233
4348
  } catch (error) {
5234
4349
  const partial = error instanceof WorkflowScriptError ? error.partial : { trace: [], emits: [], console: [], children: [] };
@@ -5239,17 +4354,31 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5239
4354
  const state = stopped ? "stopped" : pauseForDetached ? "paused" : "failed";
5240
4355
  for (const step of status.steps ?? []) {
5241
4356
  if (step.workflowKey && detachedChildKeys.has(step.workflowKey)) {
4357
+ if (step.status === "completed" || step.status === "complete" || step.status === "failed") continue;
5242
4358
  step.status = "paused";
5243
4359
  step.activityState = "needs_attention";
4360
+ } else if (pauseForDetached && step.status === "running") {
4361
+ step.status = "stopped";
4362
+ step.stopped = true;
4363
+ delete step.activityState;
5244
4364
  }
5245
4365
  }
5246
4366
  status = compactOptional<AsyncStatus>({ ...status, state, stopped: stopped || undefined, activityState: pauseForDetached ? "needs_attention" : undefined, error: error instanceof Error ? error.message : String(error), endedAt: Date.now(), workflow: { trace: partial.trace, emits: partial.emits, console: partial.console } });
5247
- const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, status.error ?? (pauseForDetached ? "Workflow paused." : "Workflow failed."));
5248
- const resultSummary = appendWorkflowOutputWarning(status.error ?? (pauseForDetached ? "Workflow paused." : "Workflow failed."), outputWarning);
5249
- if (!writeWorkflowResult({ id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: false, state: status.state, summary: resultSummary, error: status.error, stopped: status.stopped, activityState: status.activityState, results: partial.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.detached ? { detached: true } : {}), ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: workflowCwd, sessionId: currentSessionId, timestamp: Date.now(), durationMs: Date.now() - startedAt })) return;
4367
+ if (pauseForDetached) {
4368
+ const promoted = promotePausedWorkflowIfSettled(status);
4369
+ if (promoted) status = promoted;
4370
+ }
4371
+ const terminalSummary = status.state === "complete"
4372
+ ? "Workflow completed after detached child finished."
4373
+ : status.error ?? (pauseForDetached ? "Workflow paused." : "Workflow failed.");
4374
+ const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, terminalSummary, producedChildOutputPaths);
4375
+ const resultSummary = appendWorkflowOutputWarning(terminalSummary, outputWarning);
4376
+ if (!writeWorkflowResult({ id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: status.state === "complete", state: status.state, summary: resultSummary, error: status.state === "complete" ? undefined : status.error, stopped: status.stopped, activityState: status.activityState, results: partial.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.detached && status.state !== "complete" ? { detached: true } : {}), ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: workflowCwd, sessionId: currentSessionId, completionOwnerId, ...(requestParams.scheduleOrigin ? { scheduleOrigin: requestParams.scheduleOrigin } : {}), timestamp: Date.now(), durationMs: Date.now() - startedAt })) return;
5250
4377
  persist();
5251
- appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, error: status.error, ...(status.activityState ? { activityState: status.activityState } : {}) });
4378
+ persistClosed = true;
4379
+ appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, ...(status.error ? { error: status.error } : {}), ...(status.activityState ? { activityState: status.activityState } : {}) });
5252
4380
  } finally {
4381
+ persistClosed = true;
5253
4382
  deps.state.workflowControllers?.delete(workflowRunId);
5254
4383
  deps.state.activeAsyncCapacity = workflowCapacity?.reconcile(new Set(deps.state.workflowControllers?.keys() ?? []))
5255
4384
  ?? deps.state.activeAsyncCapacity;
@@ -5265,8 +4394,12 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5265
4394
  const configuredOutputBaseDir = resolveConfiguredSingleRunOutputBaseDir(deps);
5266
4395
  const workflowAggregateOutputPath = resolveWorkflowAggregateOutputPath(workflowOutput, ctx.cwd, workflowCwd, resolveSingleRunOutputBaseDir(deps, workflowArtifactsDir, _id), configuredOutputBaseDir);
5267
4396
  const claimedOutputPaths = new Map<string, string>();
4397
+ const childOutputOverrides = new Map<string, string>();
4398
+ const producedChildOutputPaths = new Set<string>();
5268
4399
  const workflowResults: SingleResult[] = [];
4400
+ const workflowChildRunIds = new Map<string, string>();
5269
4401
  let liveWorkflow: NonNullable<Details["workflow"]> = { trace: [], emits: [], console: [] };
4402
+ const workflowDeadlineAt = timeout === undefined ? undefined : Date.now() + timeout;
5270
4403
  const sendWorkflowProgress = () => {
5271
4404
  const update = workflowChatProgressUpdate(_id, chatProgress, liveWorkflow);
5272
4405
  if (update) onUpdate?.(update);
@@ -5276,17 +4409,17 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5276
4409
  script: requestParams.workflowScript,
5277
4410
  timeoutMs: timeout,
5278
4411
  signal,
5279
- prompts: workflowPrompts,
5280
4412
  ...(workflowState ? { state: workflowState } : {}),
5281
4413
  onTrace: (trace) => {
5282
4414
  liveWorkflow = { ...liveWorkflow, trace };
5283
4415
  sendWorkflowProgress();
5284
4416
  },
5285
4417
  admit: (calls) => {
5286
- const outputClaims = workflowChildOutputClaims({ ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, workflowRunId: _id, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: deps.discoverAgents, workflowAgentScope: workflowChildDefaults.agentScope, claimedOutputPaths, entries: calls });
4418
+ const outputClaims = workflowChildOutputClaims({ ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, workflowRunId: _id, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: deps.discoverAgents, workflowAgentScope: workflowChildDefaults.agentScope, state: deps.state, claimedOutputPaths, entries: calls });
5287
4419
  if (outputClaims.error) throw new Error(outputClaims.error);
5288
4420
  claimRunFanoutBatch(workflowFanoutBudget, calls.map(({ key }) => `workflow[${key}]`));
5289
4421
  if (outputClaims.claims) applyWorkflowChildOutputClaims(claimedOutputPaths, outputClaims.claims);
4422
+ if (outputClaims.overrides) for (const [key, output] of outputClaims.overrides) childOutputOverrides.set(key, output);
5290
4423
  },
5291
4424
  onEmit: (emits) => {
5292
4425
  liveWorkflow = { ...liveWorkflow, emits };
@@ -5307,7 +4440,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5307
4440
  });
5308
4441
  const result = await runMissionWorkflowChild(missionBinding, _id, key, childPhase, () => {
5309
4442
  const childRequest = bindMissionWorkflowChildAsyncLaunch(
5310
- { ...prepareWorkflowChildLaunchParams({ workflowDefaults: workflowChildDefaults, childParams, parentWorkflowRunId: _id, workflowKey: key, ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: deps.discoverAgents, workflowAgentScope: workflowChildDefaults.agentScope, options: { missionDetached: detachWorkflowChildMissions, suppressRoutineResultIntercom: chatProgress.mode === "live-card", runFanoutBudget: workflowFanoutBudget } }), runFanoutAdmitted: admission.admitted },
4443
+ { ...prepareWorkflowChildLaunchParams({ workflowDefaults: workflowChildDefaults, childParams, parentWorkflowRunId: _id, workflowKey: key, ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: deps.discoverAgents, workflowAgentScope: workflowChildDefaults.agentScope, outputOverride: childOutputOverrides.get(key), options: { missionDetached: detachWorkflowChildMissions, suppressRoutineResultIntercom: chatProgress.mode === "live-card", runFanoutBudget: workflowFanoutBudget, parentDeadlineAt: workflowDeadlineAt } }), runFanoutAdmitted: admission.admitted },
5311
4444
  missionBinding,
5312
4445
  deps.asyncByDefault,
5313
4446
  );
@@ -5327,8 +4460,12 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5327
4460
  }, ctx, preserveActiveSession);
5328
4461
  });
5329
4462
  workflowResults.push(...result.details.results);
4463
+ for (const childResult of result.details.results) {
4464
+ if (childResult.savedOutputPath) producedChildOutputPaths.add(childResult.savedOutputPath);
4465
+ }
5330
4466
  if (result.details.asyncDir && missionBinding) writeMissionAsyncBinding(result.details.asyncDir, missionBinding);
5331
4467
  const child = workflowChildResult(key, result);
4468
+ if (child.runId) workflowChildRunIds.set(key, child.runId);
5332
4469
  const childStatus = missionWorkflowChildStatus(result);
5333
4470
  recordMissionWorkflowChild(missionBinding, _id, key, {
5334
4471
  status: childStatus,
@@ -5342,6 +4479,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5342
4479
  return child;
5343
4480
  },
5344
4481
  status: async (keyOrRunId, workflowSignal) => workflowChildResult(keyOrRunId, await execute(randomUUID(), { action: "status", id: keyOrRunId }, workflowSignal, undefined, ctx, preserveActiveSession)),
4482
+ steer: (key, message, options, workflowSignal) => steerWorkflowChildByKey({ state: deps.state, workflowRunId: _id, key, message, options, signal: workflowSignal, resolveRunId: () => workflowChildRunIds.get(key) }),
5345
4483
  });
5346
4484
  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}` : ""}`);
5347
4485
  const sections = ["Workflow completed.", `Return:\n${formatWorkflowValue(workflow.value)}`];
@@ -5349,7 +4487,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5349
4487
  if (workflow.console.length > 0) sections.push(`Console:\n${workflow.console.map((entry) => `[${entry.level}] ${entry.text}`).join("\n")}`);
5350
4488
  if (traceLines.length > 0) sections.push(`Call trace:\n${traceLines.join("\n")}`);
5351
4489
  const workflowText = sections.join("\n\n");
5352
- const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, workflowText);
4490
+ const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, workflowText, producedChildOutputPaths);
5353
4491
  const displayText = appendWorkflowOutputWarning(workflowText, outputWarning);
5354
4492
  return attachWorkflowMission(withRunFanoutBudget({
5355
4493
  content: [{ type: "text", text: displayText }],
@@ -5364,7 +4502,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5364
4502
  if (partial.console.length > 0) sections.push(`Console:\n${partial.console.map((entry) => `[${entry.level}] ${entry.text}`).join("\n")}`);
5365
4503
  if (traceLines.length > 0) sections.push(`Call trace:\n${traceLines.join("\n")}`);
5366
4504
  const workflowText = sections.join("\n\n");
5367
- const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, workflowText);
4505
+ const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, workflowText, producedChildOutputPaths);
5368
4506
  const displayText = appendWorkflowOutputWarning(workflowText, outputWarning);
5369
4507
  return attachWorkflowMission(withRunFanoutBudget({
5370
4508
  content: [{ type: "text", text: displayText }],
@@ -5373,7 +4511,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5373
4511
  }, workflowFanoutBudget));
5374
4512
  }
5375
4513
  }
5376
- const directParams = prepareWorkflowChildParams(requestParams);
4514
+ const directParams = requestParams;
5377
4515
  const requestCwd = resolveRequestedCwd(ctx.cwd, directParams.cwd);
5378
4516
  const paramsWithResolvedCwd = directParams.cwd === undefined ? directParams : { ...directParams, cwd: requestCwd };
5379
4517
  const action = paramsWithResolvedCwd.action;
@@ -5690,61 +4828,6 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5690
4828
  }
5691
4829
  return withBudget(inspectSubagentStatus(paramsWithResolvedCwd, omitUndefinedProperties({ state: deps.state, nested: nestedScope, sessionRoots })));
5692
4830
  }
5693
-
5694
- if (action === "approve-checkpoint" || action === "reject-checkpoint") {
5695
- deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
5696
- const targetRunId = paramsWithResolvedCwd.runId ?? paramsWithResolvedCwd.id;
5697
- if (!targetRunId && !paramsWithResolvedCwd.dir) {
5698
- return { content: [{ type: "text", text: `action='${action}' requires id or dir.` }], isError: true, details: { mode: "management", results: [] } };
5699
- }
5700
- try {
5701
- const resolved = targetRunId ? resolveSubagentRunId(targetRunId, omitUndefinedProperties({ state: deps.state, nested: nestedResolutionScopeForExecutor(deps) })) : undefined;
5702
- const decision = action === "approve-checkpoint" ? "approved" : "rejected";
5703
- if (resolved?.kind === "foreground") {
5704
- const run = deps.state.foregroundRuns?.get(resolved.id);
5705
- if (!run?.checkpoint || run.checkpoint.status !== "pending") {
5706
- return { content: [{ type: "text", text: `Run '${resolved.id}' is not paused at an approval checkpoint.` }], isError: true, details: { mode: "management", results: [] } };
5707
- }
5708
- if (deps.state.currentSessionId && run.sessionId !== deps.state.currentSessionId) {
5709
- return { content: [{ type: "text", text: `Run '${resolved.id}' was not found in the active session.` }], isError: true, details: { mode: "management", results: [] } };
5710
- }
5711
- const decidedAt = Date.now();
5712
- const checkpoint: NonNullable<Details["checkpoint"]> = decision === "approved"
5713
- ? { ...run.checkpoint, status: "approved", approvedAt: decidedAt }
5714
- : { ...run.checkpoint, status: "rejected", rejectedAt: decidedAt };
5715
- run.checkpoint = checkpoint;
5716
- run.updatedAt = decidedAt;
5717
- return {
5718
- content: [{ type: "text", text: `Checkpoint '${checkpoint.name}' ${decision} for foreground run ${resolved.id}.` }],
5719
- details: { mode: "management", results: [], checkpoint },
5720
- };
5721
- }
5722
- const location = paramsWithResolvedCwd.dir
5723
- ? resolveAsyncRunLocation(paramsWithResolvedCwd, DIRS.async, DIRS.results)
5724
- : resolved?.kind === "async"
5725
- ? resolved.location
5726
- : undefined;
5727
- if (!location?.asyncDir) {
5728
- return { content: [{ type: "text", text: `action='${action}' targets paused foreground or async checkpoints. No matching run found.` }], isError: true, details: { mode: "management", results: [] } };
5729
- }
5730
- const status = readStatus(location.asyncDir);
5731
- const runId = status?.runId ?? location.resolvedId ?? targetRunId ?? path.basename(location.asyncDir);
5732
- if (!status?.checkpoint || status.state !== "paused") {
5733
- return { content: [{ type: "text", text: `Run '${runId}' is not paused at an approval checkpoint.` }], isError: true, details: { mode: "management", results: [] } };
5734
- }
5735
- if (deps.state.currentSessionId && status.sessionId !== deps.state.currentSessionId) {
5736
- return { content: [{ type: "text", text: `Run '${runId}' was not found in the active session.` }], isError: true, details: { mode: "management", results: [] } };
5737
- }
5738
- deliverCheckpointDecisionRequest({ asyncDir: location.asyncDir, decision, source: "subagent-action", ...(paramsWithResolvedCwd.message ? { reason: paramsWithResolvedCwd.message } : {}) });
5739
- return {
5740
- content: [{ type: "text", text: `Checkpoint '${status.checkpoint.name}' ${decision} for run ${runId}.` }],
5741
- details: { mode: "management", results: [], checkpoint: { ...status.checkpoint, status: decision } },
5742
- };
5743
- } catch (error) {
5744
- const text = error instanceof Error ? error.message : String(error);
5745
- return { content: [{ type: "text", text }], isError: true, details: { mode: "management", results: [] } };
5746
- }
5747
- }
5748
4831
  if (action === "resume") {
5749
4832
  return resumeAsyncRun(omitUndefinedProperties({ params: paramsWithResolvedCwd, requestCwd, ctx, deps, parentModel: requestParentModel, signal }));
5750
4833
  }
@@ -6033,14 +5116,23 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6033
5116
  const parentSessionFile = ctx.sessionManager.getSessionFile() ?? null;
6034
5117
  const discovered = deps.discoverAgents(effectiveCwd, scope);
6035
5118
  const discoveredAgents = discovered.agents;
6036
- const canonicalParams = canonicalizeExecutionParams(effectiveParams, discoveredAgents);
5119
+ const canonicalParams = canonicalizeExecutionParams(effectiveParams, discoveredAgents, discovered.agentDiagnostics);
6037
5120
  if (canonicalParams.error) return buildRequestedModeError(effectiveParams, canonicalParams.error);
6038
5121
  effectiveParams = canonicalParams.params!;
6039
5122
  const modelScope = discovered.modelScope;
6040
5123
  effectiveParams = applySingleAgentLaunchDefaults(effectiveParams, discoveredAgents);
6041
5124
  const turnBudget = resolveTurnBudgetConfig(effectiveParams.turnBudget ?? deps.config.turnBudget);
6042
5125
  if (turnBudget.error) return buildRequestedModeError(effectiveParams, turnBudget.error);
6043
- const contextPolicy = resolveAgentDefaultContextPolicy(effectiveParams, discoveredAgents);
5126
+ // An agent-level defaultContext is a preference, unlike an explicit request.
5127
+ // Prefer fork only when the parent session is persisted and has a current leaf;
5128
+ // otherwise use fresh immediately instead of launching a guaranteed-to-fail fork.
5129
+ // Explicit context:"fork" remains strict.
5130
+ const contextPolicy = resolveAgentDefaultContextPolicy(
5131
+ effectiveParams,
5132
+ discoveredAgents,
5133
+ deps.config.defaultSubagentContext,
5134
+ canPreferFork(ctx.sessionManager),
5135
+ );
6044
5136
  effectiveParams = contextPolicy.params;
6045
5137
  const sessionName = resolveIntercomSessionTarget(deps.pi.getSessionName(), ctx.sessionManager.getSessionId());
6046
5138
  const intercomBridge = resolveIntercomBridge({
@@ -6052,7 +5144,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6052
5144
  const agents = intercomBridge.active
6053
5145
  ? discoveredAgents.map((agent) => applyIntercomBridgeToAgent(agent, intercomBridge))
6054
5146
  : discoveredAgents;
6055
- const runId = randomUUID().slice(0, 8);
5147
+ const runId = randomUUID();
6056
5148
  const inheritedNestedRoute = resolveInheritedNestedRouteFromEnv();
6057
5149
  const nestedParentAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
6058
5150
  const shareEnabled = effectiveParams.share === true;
@@ -6086,30 +5178,33 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6086
5178
 
6087
5179
  let forkSessionFileForIndex: (idx?: number) => string | undefined = () => undefined;
6088
5180
  let forkThinkingOverrideForIndex: (idx?: number) => AgentConfig["thinking"] | undefined = () => undefined;
6089
- let prepareForkThinking = (_agentName: string, _index: number, _modelOverride?: string): void => {};
5181
+ let prepareForkThinking = (_agentName: string, _index: number, _modelOverride?: string, _modelOverrideFromParent?: boolean): void => {};
6090
5182
  const forkThinkingRequirements = new Map<number, boolean>();
6091
5183
  const forkThinkingDowngrades = new Map<number, string>();
6092
5184
  try {
6093
5185
  const forkAvailableModels = contextPolicy.usesFork ? ctx.modelRegistry.getAvailable().map(toModelInfo) : [];
6094
5186
  const parentModel = requestParentModel;
6095
- prepareForkThinking = (agentName, index, modelOverride) => {
5187
+ prepareForkThinking = (agentName, index, modelOverride, modelOverrideFromParent) => {
6096
5188
  const agentConfig = agents.find((agent) => agent.name === agentName);
6097
- if (agentConfig?.runner?.type === "external-cli") {
5189
+ if (agentConfig?.runner?.type === "external-cli" || agentConfig?.runner?.type === "external-job") {
6098
5190
  forkThinkingRequirements.set(index, true);
6099
5191
  return;
6100
5192
  }
6101
- const primaryModel = resolveEffectiveSubagentModel(
6102
- modelOverride,
6103
- agentConfig?.model,
6104
- parentModel,
6105
- forkAvailableModels,
6106
- parentModel?.provider,
6107
- );
5193
+ const primaryModel = modelOverrideFromParent
5194
+ ? modelOverride
5195
+ : resolveEffectiveSubagentModel(
5196
+ modelOverride,
5197
+ agentConfig?.model,
5198
+ parentModel,
5199
+ forkAvailableModels,
5200
+ parentModel?.provider,
5201
+ );
6108
5202
  const candidates = buildModelCandidates(
6109
5203
  primaryModel,
6110
5204
  agentConfig?.fallbackModels,
6111
5205
  forkAvailableModels,
6112
5206
  parentModel?.provider,
5207
+ { primaryModelFromParent: modelOverrideFromParent ?? inheritsParentModel(modelOverride, agentConfig?.model, parentModel) },
6113
5208
  );
6114
5209
  forkThinkingRequirements.set(
6115
5210
  index,
@@ -6135,9 +5230,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6135
5230
  : (effectiveParams.chain ?? []).flatMap((step) => getStepAgents(step as ChainStep));
6136
5231
  const externalAgent = selectedAgentNames
6137
5232
  .map((name) => agents.find((agent) => agent.name === name))
6138
- .find((agent) => agent?.runner?.type === "external-cli");
5233
+ .find((agent) => agent?.runner?.type === "external-cli" || agent?.runner?.type === "external-job");
6139
5234
  if (externalAgent && (!effectiveAsync || effectiveParams.foregroundOnly === true)) {
6140
- 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.`);
5235
+ return buildRequestedModeError(effectiveParams, `Agent '${externalAgent.name}' uses runner.type='${externalAgent.runner?.type}', which currently supports async/background execution only. Omit async or pass async:true; clarify and foregroundOnly are unsupported.`);
6141
5236
  }
6142
5237
  const foregroundTimeout = resolveSingleAgentLaunchTimeout(
6143
5238
  effectiveParams,
@@ -6221,20 +5316,20 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6221
5316
  }
6222
5317
  const sessionDirForIndex = (idx?: number) =>
6223
5318
  path.join(sessionRoot, `run-${idx ?? 0}`);
6224
- const forkSessionFileForTask: ForkSessionFileForTask = (agentName, idx = 0, modelOverride) => {
5319
+ const forkSessionFileForTask: ForkSessionFileForTask = (agentName, idx = 0, modelOverride, modelOverrideFromParent) => {
6225
5320
  if (!shouldForkAgent(contextPolicy, agentName)) return undefined;
6226
- prepareForkThinking(agentName, idx, modelOverride);
5321
+ prepareForkThinking(agentName, idx, modelOverride, modelOverrideFromParent);
6227
5322
  return forkSessionFileForIndex(idx);
6228
5323
  };
6229
- const forkThinkingOverrideForTask: ForkThinkingOverrideForTask = (agentName, idx = 0, modelOverride) => {
5324
+ const forkThinkingOverrideForTask: ForkThinkingOverrideForTask = (agentName, idx = 0, modelOverride, modelOverrideFromParent) => {
6230
5325
  if (!shouldForkAgent(contextPolicy, agentName)) return delegatedThinkingOverride;
6231
- prepareForkThinking(agentName, idx, modelOverride);
5326
+ prepareForkThinking(agentName, idx, modelOverride, modelOverrideFromParent);
6232
5327
  const override = forkThinkingOverrideForIndex(idx);
6233
5328
  if (override === "off") forkThinkingDowngrades.set(idx, agentName);
6234
5329
  return override ?? delegatedThinkingOverride;
6235
5330
  };
6236
- const childSessionFileForTask: ForkSessionFileForTask = (agentName, idx, modelOverride) =>
6237
- forkSessionFileForTask(agentName, idx, modelOverride) ?? path.join(sessionDirForIndex(idx), "session.jsonl");
5331
+ const childSessionFileForTask: ForkSessionFileForTask = (agentName, idx, modelOverride, modelOverrideFromParent) =>
5332
+ forkSessionFileForTask(agentName, idx, modelOverride, modelOverrideFromParent) ?? path.join(sessionDirForIndex(idx), "session.jsonl");
6238
5333
  const childSessionFileForIndex = (idx?: number) =>
6239
5334
  path.join(sessionDirForIndex(idx), "session.jsonl");
6240
5335
  try {
@@ -6416,6 +5511,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6416
5511
  };
6417
5512
  deps.state.foregroundControls.set(runId, foregroundControl);
6418
5513
  deps.state.lastForegroundControlId = runId;
5514
+ deps.activateSupervisorTransport?.();
5515
+ deps.refreshResultDelivery?.();
6419
5516
  }
6420
5517
 
6421
5518
  const writeNestedForegroundEvent = (type: "subagent.nested.started" | "subagent.nested.completed", result?: AgentToolResult<Details>): void => {
@@ -6532,16 +5629,6 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6532
5629
  nestedForegroundStarted = true;
6533
5630
  }
6534
5631
  const runFanoutAnnotateContent = !delegatedExecution;
6535
- if (hasChain && effectiveParams.chain) {
6536
- const result = await runChainPath(execData, deps);
6537
- writeNestedForegroundEvent("subagent.nested.completed", result);
6538
- return attachMission(withRunFanoutBudget(withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary), runFanoutBudget, { annotateContent: runFanoutAnnotateContent }));
6539
- }
6540
- if (hasTasks && effectiveParams.tasks) {
6541
- const result = await runParallelPath(execData, deps);
6542
- writeNestedForegroundEvent("subagent.nested.completed", result);
6543
- return attachMission(withRunFanoutBudget(withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary), runFanoutBudget, { annotateContent: runFanoutAnnotateContent }));
6544
- }
6545
5632
  if (hasSingle) {
6546
5633
  const result = await runSinglePath(execData, deps);
6547
5634
  writeNestedForegroundEvent("subagent.nested.completed", result);
@@ -6598,7 +5685,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6598
5685
  onUpdate: ((r: AgentToolResult<Details>) => void) | undefined,
6599
5686
  ctx: ExtensionContext,
6600
5687
  ): Promise<AgentToolResult<Details>> => {
6601
- const normalized = normalizePublicSubagentExecution(params);
5688
+ const normalized = normalizePublicSubagentExecution(params, { asyncByDefault: deps.asyncByDefault });
6602
5689
  if (!normalized.ok) {
6603
5690
  return Promise.resolve({ content: [{ type: "text", text: normalized.error }], isError: true, details: { mode: normalized.mode, results: [] } });
6604
5691
  }