pi-subagents 0.49.0 → 0.51.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 (117) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/agents/gpt-pro.md +17 -0
  3. package/agents/oracle.md +7 -5
  4. package/agents/researcher.md +1 -1
  5. package/agents/reviewer.md +2 -2
  6. package/agents/scout.md +1 -1
  7. package/agents/worker.md +1 -1
  8. package/async-retention-discovery-worker.mjs +180 -0
  9. package/docs/agents.md +37 -2
  10. package/docs/configuration.md +76 -14
  11. package/docs/extension-api.md +78 -1
  12. package/docs/missions.md +1 -1
  13. package/docs/observability.md +20 -4
  14. package/docs/tool-reference.md +55 -39
  15. package/docs/workflows.md +171 -5
  16. package/package.json +4 -2
  17. package/skills/pi-subagents/SKILL.md +5 -4
  18. package/skills/pi-subagents/references/constraints-and-recipes.md +9 -6
  19. package/skills/pi-subagents/references/execution-controls.md +22 -18
  20. package/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  21. package/skills/pi-subagents/references/prompting-and-roles.md +33 -17
  22. package/src/agents/agent-management.ts +100 -345
  23. package/src/agents/agent-serializer.ts +2 -0
  24. package/src/agents/agents.ts +135 -25
  25. package/src/api/external-job-provider.ts +185 -0
  26. package/src/api/external-runs.ts +174 -84
  27. package/src/api/preflight.ts +42 -11
  28. package/src/api/shared-types.ts +2 -0
  29. package/src/extension/config.ts +36 -3
  30. package/src/extension/doctor.ts +3 -6
  31. package/src/extension/fanout-child.ts +2 -2
  32. package/src/extension/index.ts +210 -90
  33. package/src/extension/public-execution.ts +31 -2
  34. package/src/extension/rpc.ts +5 -1
  35. package/src/extension/schemas.ts +14 -36
  36. package/src/extension/tool-description.ts +37 -24
  37. package/src/inspectors/herdr/actions.ts +5 -9
  38. package/src/inspectors/herdr/inspector-runner.ts +2 -1
  39. package/src/inspectors/herdr/project-panes.ts +4 -8
  40. package/src/inspectors/herdr/shell-command.ts +16 -0
  41. package/src/intercom/intercom-bridge.ts +2 -3
  42. package/src/intercom/native-supervisor-channel.ts +49 -51
  43. package/src/missions/goal-driver.ts +3 -1
  44. package/src/missions/lifecycle.ts +6 -1
  45. package/src/missions/store.ts +4 -9
  46. package/src/profiles/profiles.ts +3 -1
  47. package/src/runs/background/active-run-index.ts +94 -1
  48. package/src/runs/background/async-execution.ts +98 -38
  49. package/src/runs/background/async-job-tracker.ts +21 -4
  50. package/src/runs/background/async-resume.ts +30 -17
  51. package/src/runs/background/async-retention.ts +888 -0
  52. package/src/runs/background/async-status-snapshot.ts +277 -0
  53. package/src/runs/background/async-status.ts +47 -56
  54. package/src/runs/background/chain-append.ts +3 -33
  55. package/src/runs/background/chain-root-attachment.ts +2 -2
  56. package/src/runs/background/completion-replay.ts +11 -1
  57. package/src/runs/background/control-channel.ts +14 -68
  58. package/src/runs/background/fleet-view.ts +3 -1
  59. package/src/runs/background/index-segment.ts +59 -0
  60. package/src/runs/background/notify.ts +3 -1
  61. package/src/runs/background/result-files.ts +505 -0
  62. package/src/runs/background/result-watcher.ts +250 -51
  63. package/src/runs/background/retained-children.ts +79 -20
  64. package/src/runs/background/run-id-query.ts +7 -0
  65. package/src/runs/background/run-id-resolver.ts +37 -29
  66. package/src/runs/background/run-status.ts +34 -20
  67. package/src/runs/background/scheduled-runs.ts +71 -27
  68. package/src/runs/background/stale-run-reconciler.ts +33 -16
  69. package/src/runs/background/steering.ts +11 -1
  70. package/src/runs/background/subagent-runner.ts +535 -161
  71. package/src/runs/background/subagent-wait.ts +9 -7
  72. package/src/runs/background/terminal-run-index.ts +129 -0
  73. package/src/runs/background/wait-completions.ts +22 -2
  74. package/src/runs/background/wait-subscriptions.ts +80 -1
  75. package/src/runs/foreground/async-dismiss-action.ts +2 -1
  76. package/src/runs/foreground/async-steering-action.ts +21 -14
  77. package/src/runs/foreground/execution.ts +221 -15
  78. package/src/runs/foreground/subagent-executor.ts +529 -1519
  79. package/src/runs/foreground/workflow-foreground-steering.ts +6 -5
  80. package/src/runs/shared/chain-outputs.ts +1 -3
  81. package/src/runs/shared/completion-guard.ts +96 -6
  82. package/src/runs/shared/external-cli-runner.ts +4 -0
  83. package/src/runs/shared/external-job-bridge.ts +450 -0
  84. package/src/runs/shared/external-job-runner.ts +286 -0
  85. package/src/runs/shared/mcp-direct-tool-allowlist.ts +14 -0
  86. package/src/runs/shared/model-fallback.ts +34 -3
  87. package/src/runs/shared/nested-events.ts +66 -62
  88. package/src/runs/shared/orca-progress-tabs.ts +437 -0
  89. package/src/runs/shared/parallel-handoff.ts +46 -4
  90. package/src/runs/shared/parallel-utils.ts +6 -15
  91. package/src/runs/shared/permissions.ts +5 -1
  92. package/src/runs/shared/pi-args.ts +8 -1
  93. package/src/runs/shared/subagent-control.ts +41 -4
  94. package/src/runs/shared/subagent-prompt-runtime.ts +13 -15
  95. package/src/runs/shared/subagent-startup-retry.ts +12 -0
  96. package/src/runs/shared/tool-timeout.ts +93 -0
  97. package/src/runs/shared/workflow-graph.ts +1 -23
  98. package/src/runs/shared/worktree.ts +12 -1
  99. package/src/shared/atomic-json.ts +22 -2
  100. package/src/shared/capacity-resilient-json.ts +102 -0
  101. package/src/shared/completion-owner.ts +14 -0
  102. package/src/shared/file-system-retry.ts +49 -1
  103. package/src/shared/fork-context.ts +42 -0
  104. package/src/shared/prompt-resources.ts +0 -40
  105. package/src/shared/settings.ts +3 -27
  106. package/src/shared/types.ts +88 -27
  107. package/src/shared/utils.ts +8 -0
  108. package/src/shared/watch-strategy.ts +10 -0
  109. package/src/slash/slash-commands.ts +45 -28
  110. package/src/slash/slash-live-state.ts +3 -0
  111. package/src/tui/fleet-status.ts +160 -45
  112. package/src/tui/fleet.ts +186 -28
  113. package/src/tui/render.ts +41 -10
  114. package/src/workflows/chat-progress.ts +7 -5
  115. package/src/workflows/scripted-workflow.ts +424 -125
  116. package/src/runs/foreground/chain-clarify.ts +0 -1354
  117. package/src/runs/foreground/chain-execution.ts +0 -1565
@@ -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 { 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,
@@ -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";
@@ -87,9 +89,9 @@ import {
87
89
  stripDetailsOutputsForIntercomReceipt,
88
90
  } from "../../intercom/result-intercom.ts";
89
91
  import { applySteeringRecoveryAgentConfig, buildRevivedAsyncTask, resolveAsyncResumeTarget, resolveAsyncRunLocation } from "../background/async-resume.ts";
90
- import { deliverCheckpointDecisionRequest, deliverInterruptRequest, readRevivalBriefs, requestAsyncSteer, type SteerDeliveryMode } from "../background/control-channel.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,
@@ -101,6 +103,7 @@ import { stopAsyncRun } from "./async-stop-action.ts";
101
103
  import { dismissRecoveredWorkflow } from "./async-dismiss-action.ts";
102
104
  import { reconcileAsyncRun } from "../background/stale-run-reconciler.ts";
103
105
  import { resolveAsyncRootResultPath, waitForImportedAsyncRoot } from "../background/chain-root-attachment.ts";
106
+ import { resultFilePath, writeAsyncResultFile } from "../background/result-files.ts";
104
107
  import { attachRootChildrenToSteps, createNestedRoute, findNestedControlResult, resolveInheritedNestedRouteFromEnv, resolveNestedAsyncDir, resolveNestedParentAddressFromEnv, snapshotNestedEventFiles, updateForegroundNestedProjection, writeNestedControlRequest, writeNestedEvent, type NestedRunResolutionScope } from "../shared/nested-events.ts";
105
108
  import { resolveSubagentRunId, type ResolvedSubagentRunId } from "../background/run-id-resolver.ts";
106
109
  import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
@@ -108,14 +111,13 @@ import { inspectSubagentStatus } from "../background/run-status.ts";
108
111
  import { applyForceTopLevelAsyncOverride } from "../background/top-level-async.ts";
109
112
  import { handleMissionAction, MISSION_ACTIONS } from "../../missions/actions.ts";
110
113
  import { attachMissionToLaunchResult, prepareMissionLaunch, writeMissionAsyncBinding, type MissionLaunchBinding } from "../../missions/lifecycle.ts";
111
- import { updateMission } from "../../missions/store.ts";
114
+ import { MissionNotFoundError, updateMission } from "../../missions/store.ts";
112
115
  import type { MissionWorkflowChildUpdate } from "../../missions/types.ts";
113
116
  import { createMissionWorkflowState } from "../../missions/workflow-state.ts";
114
117
  import { resolveAuthorityDecision } from "../../policy/authority.ts";
115
118
  import { handleHerdrInspectorAction, HERDR_INSPECTOR_ACTIONS } from "../../inspectors/herdr/actions.ts";
116
119
  import { handleHerdrProjectPaneAction, HERDR_PROJECT_PANE_ACTIONS } from "../../inspectors/herdr/project-panes.ts";
117
- import { previewSimpleWorkflowRun, runWorkflowScript, WorkflowScriptError, type WorkflowScriptChildResult } from "../../workflows/scripted-workflow.ts";
118
- import { renderWorkflowPrompt } from "../../shared/prompt-resources.ts";
120
+ import { previewSimpleWorkflowRun, runWorkflowScript, WorkflowScriptError, type WorkflowScriptChildResult, type WorkflowSteerOptions, type WorkflowSteerResult } from "../../workflows/scripted-workflow.ts";
119
121
  import { resolveWorkflowChatProgress, type WorkflowChatProgressProjection } from "../../workflows/chat-progress.ts";
120
122
  import {
121
123
  cleanupWorktrees,
@@ -172,7 +174,7 @@ import {
172
174
  } from "../../shared/types.ts";
173
175
 
174
176
  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"]);
175
- 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"]);
177
+ const DESTRUCTIVE_MANAGEMENT_ACTIONS = new Set(["delete", "eject", "disable", "reset", "mission.close", "worktree.discard", "refine.rollback", "inspector.close", "project.close", "stop", "interrupt", "schedule.delete"]);
176
178
 
177
179
  function editDistance(left: string, right: string): number {
178
180
  const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
@@ -285,11 +287,13 @@ export interface SubagentParamsLike {
285
287
  mode?: SteerDeliveryMode;
286
288
  workflowScript?: string;
287
289
  chatProgress?: "auto" | "off" | "live-card";
290
+ isolation?: "none" | "worktree";
288
291
  step?: ChainStep;
289
292
  /** Internal workflow ownership metadata; not part of the public schema. */
290
293
  workflowParentRunId?: string;
291
294
  workflowKey?: string;
292
295
  workflowChildAsyncId?: string;
296
+ workflowParentDeadlineAt?: number;
293
297
  suppressRoutineResultIntercom?: boolean;
294
298
  /** Internal inherited cumulative run-tree budget. */
295
299
  runFanoutBudget?: RunFanoutBudgetDescriptor;
@@ -307,6 +311,8 @@ export interface SubagentParamsLike {
307
311
  foregroundOnly?: boolean;
308
312
  timeoutMs?: number;
309
313
  maxRuntimeMs?: number;
314
+ /** Optional hard per-tool-call timeout (ms). Known-fast tools also have a default. */
315
+ toolTimeoutMs?: number;
310
316
  turnBudget?: TurnBudgetConfig;
311
317
  /** Internal-only strict turn-boundary enforcement for versioned foreground delegation. */
312
318
  enforceHardTurnLimit?: boolean;
@@ -336,8 +342,6 @@ export interface SubagentParamsLike {
336
342
  acceptance?: AcceptanceInput;
337
343
  gate?: string;
338
344
  agentContract?: AgentContract;
339
- schedule?: string;
340
- scheduleName?: string;
341
345
  at?: string;
342
346
  every?: string;
343
347
  on?: string | number;
@@ -375,8 +379,10 @@ interface ExecutorDeps {
375
379
  tempArtifactsDir: string;
376
380
  getSubagentSessionRoot: (parentSessionFile: string | null) => string;
377
381
  expandTilde: (p: string) => string;
378
- discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[]; modelScope?: ModelScopeConfig };
382
+ discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[]; agentDiagnostics?: AgentDiscoveryDiagnostic[]; modelScope?: ModelScopeConfig };
379
383
  allowMutatingManagementActions?: boolean;
384
+ activateSupervisorTransport?: () => void;
385
+ refreshResultDelivery?: () => void;
380
386
  kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
381
387
  }
382
388
 
@@ -409,6 +415,8 @@ interface ExecutionContextData {
409
415
  nestedRoute?: NestedRouteInfo;
410
416
  timeoutMs?: number;
411
417
  deadlineAt?: number;
418
+ /** Raw global config.toolTimeoutMs, for per-step resolution in async runners. */
419
+ configToolTimeoutMs?: number;
412
420
  turnBudget?: ResolvedTurnBudget;
413
421
  toolBudget?: ResolvedToolBudget;
414
422
  usageBudget?: UsageBudgetConfig;
@@ -457,6 +465,7 @@ function promptAuditRedoParams(value: unknown, rewrittenTask: string): SubagentP
457
465
  delete params.workflowParentRunId;
458
466
  delete params.workflowKey;
459
467
  delete params.workflowChildAsyncId;
468
+ delete params.workflowParentDeadlineAt;
460
469
  delete params.suppressRoutineResultIntercom;
461
470
  if (params.worktree === true && Array.isArray(params.tasks)) delete params.cwd;
462
471
  return params;
@@ -548,7 +557,7 @@ function countRequestedSubagentSpawns(params: SubagentParamsLike, config: Extens
548
557
  function staticRunFanoutPaths(params: SubagentParamsLike): string[] {
549
558
  if (params.tasks) return params.tasks.map((_, index) => `tasks[${index}]`);
550
559
  if (params.chain) return params.chain.flatMap((step, stepIndex) => {
551
- if (isDynamicParallelStep(step) || "checkpoint" in step) return [];
560
+ if (isDynamicParallelStep(step)) return [];
552
561
  if (isParallelStep(step)) return step.parallel.map((_, itemIndex) => `chain[${stepIndex}].parallel[${itemIndex}]`);
553
562
  return [`chain[${stepIndex}]`];
554
563
  });
@@ -622,7 +631,7 @@ function foregroundChildActivityFromProgress(progress: SingleResult["progress"]
622
631
  };
623
632
  }
624
633
 
625
- function rememberForegroundRun(state: SubagentState, input: { runId: string; mode: "single" | "parallel" | "chain"; cwd: string; sessionId: string | null; results: SingleResult[]; checkpoint?: Details["checkpoint"] }): void {
634
+ function rememberForegroundRun(state: SubagentState, input: { runId: string; mode: "single" | "parallel" | "chain"; cwd: string; sessionId: string | null; results: SingleResult[] }): void {
626
635
  state.foregroundRuns ??= new Map();
627
636
  const previous = state.foregroundRuns.get(input.runId);
628
637
  const updatedAt = Date.now();
@@ -632,7 +641,6 @@ function rememberForegroundRun(state: SubagentState, input: { runId: string; mod
632
641
  cwd: input.cwd,
633
642
  ...(input.sessionId ? { sessionId: input.sessionId } : {}),
634
643
  updatedAt,
635
- ...(input.checkpoint ? { checkpoint: input.checkpoint } : {}),
636
644
  children: input.results.map((result, index) => {
637
645
  const child = {
638
646
  agent: result.agent,
@@ -990,9 +998,9 @@ function interruptAsyncRun(
990
998
  };
991
999
  }
992
1000
  const activeSteps = status.steps?.filter((step) => step.status === "running") ?? [];
993
- if (activeSteps.length > 0 && activeSteps.every((step) => step.runner?.type === "external-cli")) {
1001
+ if (activeSteps.length > 0 && activeSteps.every((step) => step.runner?.type === "external-cli" || step.runner?.type === "external-job")) {
994
1002
  return {
995
- content: [{ type: "text", text: `Interrupt is unsupported for one-shot external CLI async run ${target.asyncId}; use stop instead.` }],
1003
+ content: [{ type: "text", text: `Interrupt is unsupported for external async run ${target.asyncId}; use stop instead.` }],
996
1004
  isError: true,
997
1005
  details: { mode: "management", results: [] },
998
1006
  };
@@ -1209,7 +1217,7 @@ function appendStepToAsyncChain(input: {
1209
1217
  const startIndex = (status.chainStepCount ?? status.steps?.length ?? 0) + pendingAppendRequests.reduce((total, request) => total + request.steps.length, 0);
1210
1218
  const appendPaths = chain.flatMap((step, localIndex) => {
1211
1219
  const absoluteIndex = startIndex + localIndex;
1212
- if (isDynamicParallelStep(step) || "checkpoint" in step) return [];
1220
+ if (isDynamicParallelStep(step)) return [];
1213
1221
  if (isParallelStep(step)) return step.parallel.map((_, itemIndex) => `chain[${absoluteIndex}].parallel[${itemIndex}]`);
1214
1222
  return [`chain[${absoluteIndex}]`];
1215
1223
  });
@@ -1367,12 +1375,12 @@ async function directNestedAsyncSteer(input: { target: ResolvedSubagentRunId & {
1367
1375
  const targets = targetIndexes.map((index) => ({ index, state: steps[index]?.status === "pending" ? "scheduled" as const : "pending" as const }));
1368
1376
  if (targets.every((target) => target.state === "scheduled")) {
1369
1377
  const scheduled = { requestId, state: "scheduled" as const, deliveryStatus: "queued" as const, sourceRunId: run.id, targets };
1370
- return { content: [{ type: "text", text: `Steering scheduled for nested async run ${run.id} (request ${requestId}).` }], details: { mode: "management", results: [], steering: scheduled } };
1378
+ return { content: [{ type: "text", text: steeringReceipt(input.message, `Steering scheduled for nested async run ${run.id} (request ${requestId}).`) }], details: { mode: "management", results: [], steering: scheduled } };
1371
1379
  }
1372
1380
  const waited = await waitForSteeringAction(omitUndefinedProperties({ asyncDir, sourceRunId: run.id, requestId, timeoutMs: 3_000, signal: input.signal }));
1373
1381
  const result = waited ?? { requestId, state: "pending" as const, deliveryStatus: "queued" as const, sourceRunId: run.id, targets };
1374
1382
  const stateText = result.state === "failed" ? "failed" : result.state === "partial" ? "partial" : result.deliveryStatus === "queued" ? "queued" : result.state === "delivered" ? "delivered" : "pending";
1375
- 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 } };
1383
+ 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 } };
1376
1384
  }
1377
1385
 
1378
1386
  async function interruptNestedRun(target: ResolvedSubagentRunId & { kind: "nested" }): Promise<AgentToolResult<Details>> {
@@ -1404,10 +1412,10 @@ async function steerNestedRun(input: { target: ResolvedSubagentRunId & { kind: "
1404
1412
 
1405
1413
  function externalRunnerControlError(asyncDir: string, action: "steer" | "resume"): AgentToolResult<Details> | undefined {
1406
1414
  const status = readStatus(asyncDir);
1407
- if (!status?.steps?.length || !status.steps.every((step) => step.runner?.type === "external-cli")) return undefined;
1415
+ if (!status?.steps?.length || !status.steps.every((step) => step.runner?.type === "external-cli" || step.runner?.type === "external-job")) return undefined;
1408
1416
  const message = action === "steer"
1409
- ? "One-shot external CLI runners do not accept live steer messages."
1410
- : "One-shot external CLI runners do not persist sessions and cannot be resumed.";
1417
+ ? "External runners do not accept live steer messages."
1418
+ : "External runners do not persist Pi sessions and cannot be resumed.";
1411
1419
  return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
1412
1420
  }
1413
1421
 
@@ -1560,7 +1568,7 @@ async function resumeAsyncRun(input: {
1560
1568
  details: { mode: "chain", results: [] },
1561
1569
  };
1562
1570
  }
1563
- const runId = randomUUID().slice(0, 8);
1571
+ const runId = randomUUID();
1564
1572
  const topLevelResume = depth === 0 && !resolveInheritedNestedRouteFromEnv() && !input.params.workflowParentRunId;
1565
1573
  let activeAsyncCapacity: ActiveAsyncCapacityHandle | undefined;
1566
1574
  try {
@@ -1655,7 +1663,7 @@ async function resumeAsyncRun(input: {
1655
1663
  if (target.source === "async" && !recoveryDescriptor) {
1656
1664
  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: [] } };
1657
1665
  }
1658
- const runId = randomUUID().slice(0, 8);
1666
+ const runId = randomUUID();
1659
1667
  const topLevelResume = depth === 0 && !resolveInheritedNestedRouteFromEnv() && !input.params.workflowParentRunId;
1660
1668
  let activeAsyncCapacity: ActiveAsyncCapacityHandle | undefined;
1661
1669
  try {
@@ -1724,6 +1732,7 @@ async function resumeAsyncRun(input: {
1724
1732
  worktreeSetupHook: input.deps.config.worktreeSetupHook,
1725
1733
  worktreeSetupHookTimeoutMs: input.deps.config.worktreeSetupHookTimeoutMs,
1726
1734
  worktreeBaseDir: input.deps.config.worktreeBaseDir,
1735
+ worktree: input.params.worktree === true,
1727
1736
  controlConfig: recoveryDescriptor?.controlConfig ?? resolveControlConfig(input.deps.config.control, input.params.control),
1728
1737
  intercomBridge: input.params.intercomBridge ?? recoveryDescriptor?.intercomBridge,
1729
1738
  controlIntercomTarget: intercomBridge.active ? intercomBridge.orchestratorTarget : undefined,
@@ -1767,7 +1776,7 @@ async function resumeAsyncRun(input: {
1767
1776
  const sourceStatus = readStatus(sourceAsyncDir);
1768
1777
  if (sourceStatus?.steering) {
1769
1778
  for (const brief of queuedBriefs) updateSteeringTarget(sourceStatus.steering, brief.request.id, target.index, "delivered", Date.now());
1770
- writeAtomicJson(path.join(sourceAsyncDir, "status.json"), sourceStatus);
1779
+ createCapacityResilientJsonWriter({ keepAlive: true }).write(path.join(sourceAsyncDir, "status.json"), sourceStatus);
1771
1780
  }
1772
1781
  }
1773
1782
 
@@ -1958,16 +1967,19 @@ async function maybeBuildForegroundIntercomReceipt(input: {
1958
1967
  };
1959
1968
  }
1960
1969
 
1961
- function canonicalizeAgentName(name: string, agents: AgentConfig[]): { name?: string; error?: string } {
1970
+ function canonicalizeAgentName(name: string, agents: AgentConfig[], diagnostics?: AgentDiscoveryDiagnostic[]): { name?: string; error?: string } {
1962
1971
  const resolved = resolveAgentName(name, agents);
1972
+ const candidates = resolved.error ? agents.filter((agent) => resolveAgentName(name, [agent]).agent) : resolved.agent;
1973
+ const diagnostic = findBlockingAgentDiagnostic(name, candidates, diagnostics);
1974
+ if (diagnostic) return { error: `Agent '${name}' has invalid configuration: ${diagnostic.error}` };
1963
1975
  if (resolved.error) return { error: resolved.error };
1964
1976
  if (!resolved.agent) return { error: `Unknown agent: ${name}` };
1965
1977
  return { name: resolved.agent.name };
1966
1978
  }
1967
1979
 
1968
- function canonicalizeExecutionParams(params: SubagentParamsLike, agents: AgentConfig[]): { params?: SubagentParamsLike; error?: string } {
1980
+ function canonicalizeExecutionParams(params: SubagentParamsLike, agents: AgentConfig[], diagnostics?: AgentDiscoveryDiagnostic[]): { params?: SubagentParamsLike; error?: string } {
1969
1981
  const resolve = (name: string, location?: string): { name?: string; error?: string } => {
1970
- const result = canonicalizeAgentName(name, agents);
1982
+ const result = canonicalizeAgentName(name, agents, diagnostics);
1971
1983
  return result.error && location ? { error: `${result.error} (${location})` } : result;
1972
1984
  };
1973
1985
  if (params.agent) {
@@ -2171,13 +2183,21 @@ interface AgentDefaultContextPolicy {
2171
2183
  usesFork: boolean;
2172
2184
  }
2173
2185
 
2174
- function resolveAgentDefaultContextPolicy(params: SubagentParamsLike, agents: AgentConfig[]): AgentDefaultContextPolicy {
2175
- if (params.context !== undefined) {
2176
- return resolveExplicitContextPolicy(params);
2177
- }
2186
+ function resolveAgentDefaultContextPolicy(
2187
+ params: SubagentParamsLike,
2188
+ agents: AgentConfig[],
2189
+ defaultSubagentContext: ExtensionConfig["defaultSubagentContext"],
2190
+ canUseDefaultFork = false,
2191
+ ): AgentDefaultContextPolicy {
2192
+ if (params.context !== undefined) return resolveExplicitContextPolicy(params);
2178
2193
  const byName = new Map(agents.map((agent) => [agent.name, agent]));
2179
2194
  const contextForAgent = (agentName: string): ContextMode =>
2180
- byName.get(agentName)?.defaultContext === "fork" ? "fork" : "fresh";
2195
+ resolveSubagentLaunchContext({
2196
+ explicitContext: params.context,
2197
+ agentDefaultContext: byName.get(agentName)?.defaultContext,
2198
+ defaultSubagentContext,
2199
+ canUseImplicitFork: canUseDefaultFork,
2200
+ });
2181
2201
  const requestedAgentNames = collectRequestedAgentNames(params);
2182
2202
  const contextSummary = summarizeContextModes(requestedAgentNames.map((name) => contextForAgent(name)));
2183
2203
  const usesFork = contextSummary === "fork" || contextSummary === "mixed";
@@ -2190,7 +2210,10 @@ function resolveAgentDefaultContextPolicy(params: SubagentParamsLike, agents: Ag
2190
2210
  }
2191
2211
 
2192
2212
  function resolveExplicitContextPolicy(params: SubagentParamsLike): AgentDefaultContextPolicy {
2193
- const context = params.context === "fork" ? "fork" : "fresh";
2213
+ const context = resolveSubagentLaunchContext({
2214
+ explicitContext: params.context,
2215
+ canUseImplicitFork: false,
2216
+ });
2194
2217
  return {
2195
2218
  params,
2196
2219
  contextForAgent: () => context,
@@ -2230,12 +2253,16 @@ function applySingleAgentLaunchDefaults(params: SubagentParamsLike, agents: Agen
2230
2253
  if ((params.chain?.length ?? 0) > 0 || (params.tasks?.length ?? 0) > 0 || !params.agent) return params;
2231
2254
  const agent = agents.find((candidate) => candidate.name === params.agent);
2232
2255
  if (!agent) return params;
2256
+ const parentTimeoutMs = params.timeoutMs === undefined && params.maxRuntimeMs === undefined && agent.defaultTimeoutMs === undefined && params.workflowParentDeadlineAt !== undefined
2257
+ ? Math.max(1, params.workflowParentDeadlineAt - Date.now())
2258
+ : undefined;
2233
2259
  return {
2234
2260
  ...params,
2235
2261
  ...(params.async === undefined && agent.defaultAsync !== undefined ? { async: agent.defaultAsync } : {}),
2236
2262
  ...(params.timeoutMs === undefined && params.maxRuntimeMs === undefined && agent.defaultTimeoutMs !== undefined
2237
2263
  ? { timeoutMs: agent.defaultTimeoutMs }
2238
2264
  : {}),
2265
+ ...(parentTimeoutMs !== undefined ? { timeoutMs: parentTimeoutMs } : {}),
2239
2266
  ...(params.turnBudget === undefined && agent.defaultTurnBudget !== undefined
2240
2267
  ? { turnBudget: agent.defaultTurnBudget }
2241
2268
  : {}),
@@ -2506,16 +2533,19 @@ function resolveStaticLaunchSummary(input: {
2506
2533
  thinkingOverrideForTask: ForkThinkingOverrideForTask;
2507
2534
  }): StaticLaunchSummary {
2508
2535
  const agentConfig = input.agents.find((agent) => agent.name === input.agent);
2509
- const model = resolveEffectiveSubagentModel(
2510
- input.explicitModel,
2511
- agentConfig?.model,
2512
- input.parentModel,
2513
- input.availableModels,
2514
- input.currentProvider,
2515
- input.modelScope === undefined ? {} : { scope: input.modelScope },
2516
- );
2517
- const thinkingOverride = input.thinkingOverrideForTask(input.agent, input.index, model);
2518
- const thinking = resolveEffectiveThinking(model, thinkingOverride ?? agentConfig?.thinking);
2536
+ const externalRunner = agentConfig?.runner?.type === "external-cli" || agentConfig?.runner?.type === "external-job";
2537
+ const model = externalRunner
2538
+ ? undefined
2539
+ : resolveEffectiveSubagentModel(
2540
+ input.explicitModel,
2541
+ agentConfig?.model,
2542
+ input.parentModel,
2543
+ input.availableModels,
2544
+ input.currentProvider,
2545
+ input.modelScope === undefined ? {} : { scope: input.modelScope },
2546
+ );
2547
+ const thinkingOverride = externalRunner ? undefined : input.thinkingOverrideForTask(input.agent, input.index, model);
2548
+ const thinking = externalRunner ? undefined : resolveEffectiveThinking(model, thinkingOverride ?? agentConfig?.thinking);
2519
2549
  return {
2520
2550
  agent: input.agent,
2521
2551
  ...(model ? { model } : {}),
@@ -2696,27 +2726,6 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2696
2726
  const hasSingle = !hasChain && !hasTasks && Boolean(params.agent);
2697
2727
  if (!effectiveAsync) return null;
2698
2728
 
2699
- if (hasChain && params.chain) {
2700
- const chainWorktreeTaskCwdError = buildChainWorktreeTaskCwdError(params.chain as ChainStep[], effectiveCwd);
2701
- if (chainWorktreeTaskCwdError) {
2702
- return {
2703
- content: [{ type: "text", text: chainWorktreeTaskCwdError }],
2704
- isError: true,
2705
- details: { mode: "chain" as const, results: [] },
2706
- };
2707
- }
2708
- }
2709
-
2710
- if (hasTasks && params.tasks) {
2711
- const maxParallelTasks = resolveTopLevelParallelMaxTasks(deps.config.parallel?.maxTasks);
2712
- if (params.tasks.length > maxParallelTasks) {
2713
- return buildParallelModeError(`Max ${maxParallelTasks} tasks`);
2714
- }
2715
- if (params.worktree) {
2716
- const worktreeTaskCwdError = buildParallelWorktreeTaskCwdError(params.tasks, effectiveCwd);
2717
- if (worktreeTaskCwdError) return buildParallelModeError(worktreeTaskCwdError);
2718
- }
2719
- }
2720
2729
 
2721
2730
  if (!isAsyncAvailable()) {
2722
2731
  return {
@@ -2744,114 +2753,6 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2744
2753
  const controlIntercomTarget = intercomBridge.active ? intercomBridge.orchestratorTarget : undefined;
2745
2754
  const childIntercomTarget = intercomBridge.active ? (agent: string, index: number) => resolveSubagentIntercomTarget(id, agent, index) : undefined;
2746
2755
 
2747
- if (hasTasks && params.tasks) {
2748
- const skillOverrides = params.tasks.map((task) => normalizeSkillInput(task.skill));
2749
- const parallelTasks = params.tasks.map((task, index) => compactOptional<ParallelTaskItem>({
2750
- agent: task.agent,
2751
- task: shouldForkAgent(contextPolicy, task.agent) ? wrapForkTask(task.task) : task.task,
2752
- cwd: task.cwd,
2753
- ...(task.model !== undefined ? { model: task.model } : {}),
2754
- ...(skillOverrides[index] !== undefined ? { skill: skillOverrides[index] } : {}),
2755
- ...(task.output !== undefined && task.output !== true ? { output: task.output } : {}),
2756
- ...(task.outputMode !== undefined ? { outputMode: task.outputMode } : {}),
2757
- ...(task.reads !== undefined && task.reads !== true ? { reads: task.reads } : {}),
2758
- ...(task.progress !== undefined ? { progress: task.progress } : {}),
2759
- ...(task.toolBudget !== undefined ? { toolBudget: task.toolBudget } : {}),
2760
- ...(task.outputSchema !== undefined ? { outputSchema: task.outputSchema } : {}),
2761
- ...(task.agentContract !== undefined ? { agentContract: task.agentContract } : {}),
2762
- ...(task.acceptance !== undefined ? { acceptance: task.acceptance } : {}),
2763
- }));
2764
- return executeAsyncChain(id, compactOptional<Parameters<typeof executeAsyncChain>[1]>({
2765
- chain: [compactOptional<ParallelStep>({
2766
- parallel: parallelTasks,
2767
- concurrency: resolveTopLevelParallelConcurrency(params.concurrency, deps.config.parallel?.concurrency),
2768
- worktree: params.worktree,
2769
- })],
2770
- resultMode: "parallel",
2771
- goal: params.tasks[0]?.task ?? "",
2772
- agents,
2773
- ctx: asyncCtx,
2774
- availableModels,
2775
- cwd: effectiveCwd,
2776
- maxOutput: params.maxOutput,
2777
- artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
2778
- artifactConfig,
2779
- shareEnabled,
2780
- sessionRoot,
2781
- chainSkills: [],
2782
- activeAsyncCapacity: data.activeAsyncCapacity,
2783
- sessionFilesByFlatIndex: params.tasks.map((task, index) => sessionFileForTask(task.agent, index, task.model)),
2784
- thinkingOverridesByFlatIndex: params.tasks.map((task, index) => thinkingOverrideForTask(task.agent, index, task.model)),
2785
- contextForAgent: contextPolicy.contextForAgent,
2786
- maxSubagentDepth: currentMaxSubagentDepth,
2787
- waitToolEnabled: deps.waitToolEnabled,
2788
- worktreeSetupHook: deps.config.worktreeSetupHook,
2789
- worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
2790
- worktreeBaseDir: deps.config.worktreeBaseDir,
2791
- controlConfig,
2792
- agentContract: params.agentContract,
2793
- controlIntercomTarget,
2794
- childIntercomTarget,
2795
- nestedRoute,
2796
- timeoutMs: data.timeoutMs,
2797
- turnBudget: data.turnBudget,
2798
- toolBudget: data.toolBudget,
2799
- usageBudget: data.usageBudget,
2800
- configToolBudget: data.configToolBudget,
2801
- capabilityCeiling: data.capabilityCeiling,
2802
- runFanoutBudget: data.runFanoutBudget,
2803
- globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2804
- parentWorkflowRunId: params.workflowParentRunId,
2805
- workflowKey: params.workflowKey,
2806
- }));
2807
- }
2808
-
2809
- if (hasChain && params.chain) {
2810
- const normalized = normalizeSkillInput(params.skill);
2811
- const chainSkills = normalized === false ? [] : (normalized ?? []);
2812
- const rawChain = params.chain as ChainStep[];
2813
- const chain = wrapChainTasksForFork(rawChain, contextPolicy);
2814
- return executeAsyncChain(id, compactOptional<Parameters<typeof executeAsyncChain>[1]>({
2815
- chain,
2816
- task: params.task,
2817
- goal: resolveAsyncEventGoal(params.task, rawChain),
2818
- agents,
2819
- ctx: asyncCtx,
2820
- availableModels,
2821
- cwd: effectiveCwd,
2822
- maxOutput: params.maxOutput,
2823
- artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
2824
- artifactConfig,
2825
- shareEnabled,
2826
- sessionRoot,
2827
- chainSkills,
2828
- activeAsyncCapacity: data.activeAsyncCapacity,
2829
- sessionFilesByFlatIndex: collectChainSessionFiles(chain, sessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems),
2830
- thinkingOverridesByFlatIndex: collectChainThinkingOverrides(chain, thinkingOverrideForTask, deps.config.chain?.dynamicFanout?.maxItems),
2831
- contextForAgent: contextPolicy.contextForAgent,
2832
- dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
2833
- maxSubagentDepth: currentMaxSubagentDepth,
2834
- waitToolEnabled: deps.waitToolEnabled,
2835
- worktreeSetupHook: deps.config.worktreeSetupHook,
2836
- worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
2837
- worktreeBaseDir: deps.config.worktreeBaseDir,
2838
- controlConfig,
2839
- agentContract: params.agentContract,
2840
- controlIntercomTarget,
2841
- childIntercomTarget,
2842
- nestedRoute,
2843
- timeoutMs: data.timeoutMs,
2844
- turnBudget: data.turnBudget,
2845
- toolBudget: data.toolBudget,
2846
- usageBudget: data.usageBudget,
2847
- configToolBudget: data.configToolBudget,
2848
- capabilityCeiling: data.capabilityCeiling,
2849
- runFanoutBudget: data.runFanoutBudget,
2850
- globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
2851
- parentWorkflowRunId: params.workflowParentRunId,
2852
- workflowKey: params.workflowKey,
2853
- }));
2854
- }
2855
2756
 
2856
2757
  if (hasSingle) {
2857
2758
  const a = agents.find((x) => x.name === params.agent);
@@ -2868,11 +2769,11 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2868
2769
  const normalizedSkills = normalizeSkillInput(params.skill);
2869
2770
  const skills = normalizedSkills === false ? [] : normalizedSkills;
2870
2771
  const maxSubagentDepth = resolveChildMaxSubagentDepth(currentMaxSubagentDepth, a.maxSubagentDepth);
2871
- const externalRunnerWithoutExplicitModel = a.runner?.type === "external-cli"
2772
+ const externalRunnerWithoutExplicitModel = (a.runner?.type === "external-cli" || a.runner?.type === "external-job")
2872
2773
  && params.model === undefined
2873
2774
  && (a.model === undefined || (a.modelSource?.type === "subagents.defaultModel" && a.model === a.modelSource.model));
2874
- const modelOverride = externalRunnerWithoutExplicitModel
2875
- ? undefined
2775
+ const modelOverride = a.runner?.type === "external-cli" || a.runner?.type === "external-job"
2776
+ ? params.model ?? (externalRunnerWithoutExplicitModel ? undefined : a.model)
2876
2777
  : resolveEffectiveSubagentModel(params.model as string | undefined, a.model, parentModel, availableModels, currentProvider, data.modelScope === undefined ? {} : { scope: data.modelScope });
2877
2778
  return executeAsyncSingle(id, compactOptional<Parameters<typeof executeAsyncSingle>[1]>({
2878
2779
  agent: params.agent!,
@@ -2900,6 +2801,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2900
2801
  thinkingOverride: externalRunnerWithoutExplicitModel ? undefined : thinkingOverrideForTask(params.agent!, 0, modelOverride),
2901
2802
  maxSubagentDepth,
2902
2803
  waitToolEnabled: deps.waitToolEnabled,
2804
+ ...(params.worktree === true ? { worktree: true } : {}),
2903
2805
  worktreeSetupHook: deps.config.worktreeSetupHook,
2904
2806
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
2905
2807
  worktreeBaseDir: deps.config.worktreeBaseDir,
@@ -2916,6 +2818,8 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2916
2818
  toolBudget: data.toolBudget,
2917
2819
  usageBudget: data.usageBudget,
2918
2820
  configToolBudget: data.configToolBudget,
2821
+ toolTimeoutMs: data.params?.toolTimeoutMs,
2822
+ configToolTimeoutMs: data.configToolTimeoutMs,
2919
2823
  capabilityCeiling: data.capabilityCeiling,
2920
2824
  runFanoutBudget: data.runFanoutBudget,
2921
2825
  parentWorkflowRunId: params.workflowParentRunId,
@@ -2926,300 +2830,11 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2926
2830
  return null;
2927
2831
  }
2928
2832
 
2929
- async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Promise<AgentToolResult<Details>> {
2930
- const {
2931
- params,
2932
- effectiveCwd,
2933
- agents,
2934
- ctx,
2935
- signal,
2936
- runId,
2937
- shareEnabled,
2938
- sessionDirForIndex,
2939
- sessionFileForIndex,
2940
- sessionFileForTask,
2941
- thinkingOverrideForTask,
2942
- artifactsDir,
2943
- artifactConfig,
2944
- onUpdate,
2945
- sessionRoot,
2946
- controlConfig,
2947
- contextPolicy,
2948
- } = data;
2949
- const onControlEvent = createForegroundControlNotifier(data, deps);
2950
- const childIntercomTarget = data.intercomBridge.active ? resolveSubagentIntercomTarget : undefined;
2951
- const foregroundControl = deps.state.foregroundControls.get(runId);
2952
- const normalized = normalizeSkillInput(params.skill);
2953
- const chainSkills = normalized === false ? [] : (normalized ?? []);
2954
- const chain = wrapChainTasksForFork(params.chain as ChainStep[], contextPolicy);
2955
- const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
2956
- const chainCtx = normalizeParentModel(ctx.model) || !data.parentModel ? ctx : ({ ...ctx, model: data.parentModel } as ExtensionContext);
2957
- const chainResult = await executeChain(compactOptional<Parameters<typeof executeChain>[0]>({
2958
- chain,
2959
- task: params.task,
2960
- agents,
2961
- ctx: chainCtx,
2962
- modelScope: data.modelScope,
2963
- intercomEvents: deps.pi.events,
2964
- signal,
2965
- runId,
2966
- cwd: effectiveCwd,
2967
- shareEnabled,
2968
- sessionDirForIndex,
2969
- sessionFileForIndex,
2970
- sessionFileForTask,
2971
- thinkingOverrideForTask,
2972
- contextForAgent: contextPolicy.contextForAgent,
2973
- artifactsDir,
2974
- artifactConfig,
2975
- includeProgress: params.includeProgress,
2976
- clarify: params.clarify,
2977
- onUpdate,
2978
- onControlEvent,
2979
- controlConfig,
2980
- agentContract: params.agentContract,
2981
- childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(runId, agent, index) : undefined,
2982
- orchestratorIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
2983
- foregroundControl,
2984
- nestedRoute: foregroundControl?.nestedRoute,
2985
- chainSkills,
2986
- chainDir: params.chainDir ?? getChainRunsDir(effectiveCwd, artifactConfig.dir),
2987
- dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
2988
- maxSubagentDepth: currentMaxSubagentDepth,
2989
- worktreeSetupHook: deps.config.worktreeSetupHook,
2990
- worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
2991
- worktreeBaseDir: deps.config.worktreeBaseDir,
2992
- timeoutMs: data.timeoutMs,
2993
- deadlineAt: data.deadlineAt,
2994
- turnBudget: data.turnBudget,
2995
- onDetachedExit: (index, result) => {
2996
- try {
2997
- updateRememberedForegroundChild(deps.state, { runId, mode: "chain", cwd: effectiveCwd, sessionId: data.parentSessionId, index, result, events: deps.pi.events });
2998
- } finally {
2999
- removeForegroundControlIfIdle(deps.state, runId);
3000
- }
3001
- },
3002
- onForegroundChildSettled: () => {
3003
- removeForegroundControlIfIdle(deps.state, runId);
3004
- },
3005
- toolBudget: data.toolBudget,
3006
- usageBudget: data.usageBudget,
3007
- configToolBudget: data.configToolBudget,
3008
- permissions: deps.config.permissions,
3009
- globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
3010
- capabilityCeiling: data.capabilityCeiling,
3011
- runFanoutBudget: data.runFanoutBudget,
3012
- }));
3013
-
3014
- if (chainResult.requestedAsync) {
3015
- if (!isAsyncAvailable()) {
3016
- return {
3017
- 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." }],
3018
- isError: true,
3019
- details: { mode: "chain" as const, results: [] },
3020
- };
3021
- }
3022
- const id = data.asyncRunId;
3023
- const capacity = acquireClarifiedBackgroundCapacity(data, deps, "chain", id);
3024
- if (capacity.errorResult) return capacity.errorResult;
3025
- const parentModel = data.parentModel;
3026
- const asyncCtx = compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
3027
- pi: deps.pi,
3028
- cwd: ctx.cwd,
3029
- currentSessionId: data.parentSessionId!,
3030
- parentSessionId: data.parentPiSessionId,
3031
- currentModelProvider: parentModel?.provider,
3032
- currentModel: parentModel,
3033
- modelScope: data.modelScope,
3034
- interactive: ctx.hasUI,
3035
- permissions: deps.config.permissions,
3036
- });
3037
- const rawAsyncChain = chainResult.requestedAsync.chain;
3038
- const asyncChain = wrapChainTasksForFork(rawAsyncChain, contextPolicy);
3039
- const firstAgent = firstChainAgent(rawAsyncChain);
3040
- try {
3041
- return executeAsyncChain(id, compactOptional<Parameters<typeof executeAsyncChain>[1]>({
3042
- chain: asyncChain,
3043
- task: params.task,
3044
- goal: resolveAsyncEventGoal(params.task, rawAsyncChain, firstAgent ? shouldForkAgent(contextPolicy, firstAgent) : false),
3045
- agents,
3046
- ctx: asyncCtx,
3047
- availableModels: ctx.modelRegistry.getAvailable().map(toModelInfo),
3048
- cwd: effectiveCwd,
3049
- maxOutput: params.maxOutput,
3050
- artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
3051
- artifactConfig,
3052
- shareEnabled,
3053
- sessionRoot,
3054
- chainSkills: chainResult.requestedAsync.chainSkills,
3055
- activeAsyncCapacity: capacity.activeAsyncCapacity,
3056
- sessionFilesByFlatIndex: collectChainSessionFiles(asyncChain, sessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems),
3057
- thinkingOverridesByFlatIndex: collectChainThinkingOverrides(asyncChain, thinkingOverrideForTask, deps.config.chain?.dynamicFanout?.maxItems),
3058
- contextForAgent: contextPolicy.contextForAgent,
3059
- dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
3060
- maxSubagentDepth: currentMaxSubagentDepth,
3061
- waitToolEnabled: deps.waitToolEnabled,
3062
- worktreeSetupHook: deps.config.worktreeSetupHook,
3063
- worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
3064
- worktreeBaseDir: deps.config.worktreeBaseDir,
3065
- controlConfig,
3066
- agentContract: params.agentContract,
3067
- controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
3068
- childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
3069
- nestedRoute: data.nestedRoute,
3070
- timeoutMs: data.timeoutMs,
3071
- turnBudget: data.turnBudget,
3072
- toolBudget: data.toolBudget,
3073
- usageBudget: data.usageBudget,
3074
- configToolBudget: data.configToolBudget,
3075
- capabilityCeiling: data.capabilityCeiling,
3076
- runFanoutBudget: data.runFanoutBudget,
3077
- globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
3078
- }));
3079
- } finally {
3080
- finalizeClarifiedBackgroundCapacity(data, deps, capacity.activeAsyncCapacity);
3081
- }
3082
- }
3083
-
3084
- const rawChainDetails = chainResult.details ? compactOptional<Details>({ ...chainResult.details, runId, timeoutMs: data.timeoutMs }) : undefined;
3085
- if (foregroundControl && rawChainDetails) {
3086
- updateForegroundNestedProjection(foregroundControl);
3087
- attachRootChildrenToSteps(runId, rawChainDetails.results, foregroundControl.nestedChildren);
3088
- rawChainDetails.totalCost = sumResultsCost(rawChainDetails.results);
3089
- const usageBudget = usageBudgetState(data.usageBudget, rawChainDetails.totalCost);
3090
- if (usageBudget === undefined) delete rawChainDetails.usageBudget;
3091
- else rawChainDetails.usageBudget = usageBudget;
3092
- }
3093
- const chainDetails = rawChainDetails ? compactForegroundDetails(rawChainDetails) : undefined;
3094
- if (chainDetails) rememberForegroundRun(deps.state, { runId, mode: "chain", cwd: effectiveCwd, sessionId: data.parentSessionId, results: chainDetails.results, checkpoint: chainDetails.checkpoint });
3095
- const intercomReceipt = chainDetails && !chainDetails.results.some((result) => result.interrupted || result.detached)
3096
- ? await maybeBuildForegroundIntercomReceipt({
3097
- pi: deps.pi,
3098
- intercomBridge: data.intercomBridge,
3099
- runId,
3100
- mode: "chain",
3101
- details: chainDetails,
3102
- ...(foregroundControl?.nestedChildren?.length ? { nestedChildren: foregroundControl.nestedChildren } : {}),
3103
- })
3104
- : null;
3105
- if (intercomReceipt) {
3106
- return {
3107
- ...chainResult,
3108
- content: [{ type: "text", text: intercomReceipt.text }],
3109
- details: intercomReceipt.details,
3110
- };
3111
- }
3112
-
3113
- return chainDetails ? { ...chainResult, details: chainDetails } : chainResult;
3114
- }
3115
-
3116
- interface ForegroundParallelRunInput {
3117
- tasks: TaskParam[];
3118
- taskTexts: string[];
3119
- taskDescriptions: string[];
3120
- agents: AgentConfig[];
3121
- ctx: ExtensionContext;
3122
- state: SubagentState;
3123
- intercomEvents: IntercomEventBus;
3124
- parentSessionId: string | null;
3125
- capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
3126
- runFanoutBudget: RunFanoutBudgetDescriptor;
3127
- signal: AbortSignal;
3128
- runId: string;
3129
- sessionDirForIndex: (idx?: number) => string | undefined;
3130
- sessionFileForIndex: (idx?: number) => string | undefined;
3131
- sessionFileForTask: ForkSessionFileForTask;
3132
- thinkingOverrideForTask: ForkThinkingOverrideForTask;
3133
- shareEnabled: boolean;
3134
- artifactConfig: ArtifactConfig;
3135
- artifactsDir: string;
3136
- outputBaseDir: string;
3137
- maxOutput?: MaxOutputConfig;
3138
- paramsCwd: string;
3139
- progressDir: string;
3140
- maxSubagentDepths: number[];
3141
- waitToolEnabled?: boolean;
3142
- availableModels: ModelInfo[];
3143
- modelScope?: ModelScopeConfig;
3144
- parentModel?: ParentModel;
3145
- modelOverrides: (string | undefined)[];
3146
- behaviors: Array<ReturnType<typeof resolveStepBehavior>>;
3147
- firstProgressIndex: number;
3148
- controlConfig: ResolvedControlConfig;
3149
- contextPolicy: AgentDefaultContextPolicy;
3150
- onControlEvent?: (event: ControlEvent) => void;
3151
- childIntercomTarget?: (agent: string, index: number) => string | undefined;
3152
- orchestratorIntercomTarget?: string;
3153
- foregroundControl?: SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never;
3154
- concurrencyLimit: number;
3155
- globalSemaphore?: Semaphore;
3156
- liveResults: (SingleResult | undefined)[];
3157
- liveProgress: (AgentProgress | undefined)[];
3158
- onUpdate?: (r: AgentToolResult<Details>) => void;
3159
- worktreeSetup?: WorktreeSetup;
3160
- timeoutMs?: number;
3161
- deadlineAt?: number;
3162
- turnBudget?: ResolvedTurnBudget;
3163
- usageBudget?: UsageBudgetConfig;
3164
- toolBudgets: (ResolvedToolBudget | undefined)[];
3165
- agentContract?: AgentContract;
3166
- permissions?: ExtensionConfig["permissions"];
3167
- }
3168
- function buildParallelModeError(message: string): AgentToolResult<Details> {
3169
- return {
3170
- content: [{ type: "text", text: message }],
3171
- isError: true,
3172
- details: { mode: "parallel" as const, results: [] },
3173
- };
3174
- }
3175
-
3176
- function acquireClarifiedBackgroundCapacity(
3177
- data: ExecutionContextData,
3178
- deps: ExecutorDeps,
3179
- mode: "single" | "parallel" | "chain",
3180
- asyncRunId: string,
3181
- ): { activeAsyncCapacity?: ActiveAsyncCapacityHandle; errorResult?: AgentToolResult<Details> } {
3182
- if (!data.topLevelAsyncCapacityEligible || !data.parentSessionId) return {};
3183
- const liveWorkflowRunIds = new Set(deps.state.workflowControllers?.keys() ?? []);
3184
- const activeLimit = resolveMaxActiveAsyncRunsPerSession(deps.config.maxActiveAsyncRunsPerSession);
3185
- try {
3186
- const activeAsyncCapacity = acquireActiveAsyncCapacity({
3187
- sessionId: data.parentSessionId,
3188
- limit: activeLimit,
3189
- runId: asyncRunId,
3190
- kind: "runner",
3191
- asyncDir: path.join(DIRS.async, asyncRunId),
3192
- }, { liveWorkflowRunIds });
3193
- deps.state.activeAsyncCapacity = getActiveAsyncCapacitySnapshot(data.parentSessionId, activeLimit, { liveWorkflowRunIds });
3194
- return { activeAsyncCapacity };
3195
- } catch (error) {
3196
- if (error instanceof ActiveAsyncCapacityError) {
3197
- deps.state.activeAsyncCapacity = error.snapshot;
3198
- return { errorResult: { content: [{ type: "text", text: error.message }], isError: true, details: { mode, results: [], activeAsyncCapacity: error.snapshot } } };
3199
- }
3200
- throw error;
3201
- }
3202
- }
3203
-
3204
- function finalizeClarifiedBackgroundCapacity(
3205
- data: ExecutionContextData,
3206
- deps: ExecutorDeps,
3207
- activeAsyncCapacity: ActiveAsyncCapacityHandle | undefined,
3208
- ): void {
3209
- if (!activeAsyncCapacity || !data.parentSessionId) return;
3210
- if (!activeAsyncCapacity.owner.runnerStartedAt) activeAsyncCapacity.rollback();
3211
- deps.state.activeAsyncCapacity = getActiveAsyncCapacitySnapshot(
3212
- data.parentSessionId,
3213
- resolveMaxActiveAsyncRunsPerSession(deps.config.maxActiveAsyncRunsPerSession),
3214
- { liveWorkflowRunIds: new Set(deps.state.workflowControllers?.keys() ?? []) },
3215
- );
3216
- }
3217
-
3218
- function createParallelWorktreeSetup(
2833
+ function createSingleWorktreeSetup(
3219
2834
  enabled: boolean | undefined,
3220
2835
  cwd: string,
3221
2836
  runId: string,
3222
- tasks: TaskParam[],
2837
+ agent: string,
3223
2838
  setupHook: ExtensionConfig["worktreeSetupHook"],
3224
2839
  setupHookTimeoutMs: ExtensionConfig["worktreeSetupHookTimeoutMs"],
3225
2840
  baseDir: ExtensionConfig["worktreeBaseDir"],
@@ -3227,8 +2842,8 @@ function createParallelWorktreeSetup(
3227
2842
  if (!enabled) return {};
3228
2843
  try {
3229
2844
  return {
3230
- setup: createWorktrees(cwd, runId, tasks.length, omitUndefinedProperties({
3231
- agents: tasks.map((task) => task.agent),
2845
+ setup: createWorktrees(cwd, runId, 1, omitUndefinedProperties({
2846
+ agents: [agent],
3232
2847
  setupHook: setupHook
3233
2848
  ? { hookPath: setupHook, ...(setupHookTimeoutMs === undefined ? {} : { timeoutMs: setupHookTimeoutMs }) }
3234
2849
  : undefined,
@@ -3237,27 +2852,25 @@ function createParallelWorktreeSetup(
3237
2852
  };
3238
2853
  } catch (error) {
3239
2854
  const message = error instanceof Error ? error.message : String(error);
3240
- return { errorResult: buildParallelModeError(message) };
2855
+ return { errorResult: { content: [{ type: "text", text: message }], isError: true, details: { mode: "single", results: [] } } };
3241
2856
  }
3242
2857
  }
3243
2858
 
3244
- function buildParallelWorktreeTaskCwdError(
3245
- tasks: ReadonlyArray<{ agent: string; cwd?: string }>,
3246
- sharedCwd: string,
3247
- ): string | undefined {
3248
- const conflict = findWorktreeTaskCwdConflict(tasks, sharedCwd);
3249
- if (!conflict) return undefined;
3250
- return formatWorktreeTaskCwdConflict(conflict, sharedCwd);
3251
- }
3252
-
3253
2859
  function resolveConfiguredSingleRunOutputBaseDir(deps: ExecutorDeps): string | undefined {
3254
2860
  return deps.config.singleRunOutputBaseDir
3255
2861
  ? path.resolve(deps.expandTilde(deps.config.singleRunOutputBaseDir))
3256
2862
  : undefined;
3257
2863
  }
3258
2864
 
2865
+ export function sanitizeRunPathSegment(value: string, maxBytes = 120): string {
2866
+ const sanitized = value.trim().replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^_+|_+$/g, "");
2867
+ if (!sanitized) return "unknown";
2868
+ if (Buffer.byteLength(sanitized, "utf-8") <= maxBytes) return sanitized;
2869
+ return sanitized.slice(0, maxBytes).replace(/_+$/, "") || "unknown";
2870
+ }
2871
+
3259
2872
  function resolveSingleRunOutputBaseDir(deps: ExecutorDeps, artifactsDir: string, runId: string): string {
3260
- return resolveConfiguredSingleRunOutputBaseDir(deps) ?? path.join(artifactsDir, "outputs", runId);
2873
+ return resolveConfiguredSingleRunOutputBaseDir(deps) ?? path.join(artifactsDir, "outputs", sanitizeRunPathSegment(runId));
3261
2874
  }
3262
2875
 
3263
2876
  function resolveWorkflowAggregateOutputPath(
@@ -3276,814 +2889,157 @@ function workflowChildDefaultOutput(aggregateOutputPath: string | undefined, art
3276
2889
  const parsed = path.parse(aggregateOutputPath);
3277
2890
  return path.join(parsed.dir, `${parsed.name}.${workflowKey}${parsed.ext || ".md"}`);
3278
2891
  }
3279
- return path.join(artifactsDir, "outputs", workflowRunId, `${workflowKey}.md`);
2892
+ return path.join(artifactsDir, "outputs", sanitizeRunPathSegment(workflowRunId), `${workflowKey}.md`);
3280
2893
  }
3281
2894
 
3282
- function writeWorkflowAggregateOutput(outputPath: string | undefined, text: string): string | undefined {
2895
+ function writeWorkflowAggregateOutput(outputPath: string | undefined, text: string, producedChildOutputPaths: ReadonlySet<string>): string | undefined {
3283
2896
  if (!outputPath) return undefined;
3284
2897
  try {
2898
+ if (producedChildOutputPaths.has(outputPath)) return undefined;
3285
2899
  fs.mkdirSync(path.dirname(outputPath), { recursive: true });
3286
2900
  fs.writeFileSync(outputPath, text, "utf-8");
3287
2901
  return undefined;
3288
- } catch (error) {
3289
- return `Output file error: ${outputPath}\n${error instanceof Error ? error.message : String(error)}`;
3290
- }
3291
- }
3292
-
3293
- function appendWorkflowOutputWarning(text: string, warning: string | undefined): string {
3294
- return warning ? `${text}\n\n${warning}` : text;
3295
- }
3296
-
3297
- function resolveWorkflowChildOutputPath(input: {
3298
- ctxCwd: string;
3299
- workflowCwd: string;
3300
- artifactsDir: string;
3301
- workflowRunId: string;
3302
- aggregateOutputPath?: string;
3303
- configuredOutputBaseDir?: string;
3304
- discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[] };
3305
- workflowAgentScope?: unknown;
3306
- key: string;
3307
- params: Record<string, unknown>;
3308
- }): string | undefined {
3309
- const rawOutput = input.params.output;
3310
- const hasExplicitOutput = typeof rawOutput === "string" || typeof rawOutput === "boolean";
3311
- const childCwd = typeof input.params.cwd === "string" ? resolveChildCwd(input.workflowCwd, input.params.cwd) : input.workflowCwd;
3312
- const agentScope = resolveExecutionAgentScope(input.params.agentScope ?? input.workflowAgentScope);
3313
- const agents = input.discoverAgents(childCwd, agentScope).agents;
3314
- const agent = typeof input.params.agent === "string" ? resolveAgentName(input.params.agent, agents).agent : undefined;
3315
- const agentOutput = typeof agent?.output === "string" ? agent.output : undefined;
3316
- const output = rawOutput === true || rawOutput === "true"
3317
- ? agentOutput
3318
- : hasExplicitOutput
3319
- ? rawOutput
3320
- : workflowChildDefaultOutput(input.aggregateOutputPath, input.artifactsDir, input.workflowRunId, input.key);
3321
- return resolveSingleOutputPath(output, input.ctxCwd, childCwd, input.configuredOutputBaseDir);
3322
- }
3323
-
3324
- function workflowChildOutputClaims(input: {
3325
- ctxCwd: string;
3326
- workflowCwd: string;
3327
- artifactsDir: string;
3328
- workflowRunId: string;
3329
- aggregateOutputPath?: string;
3330
- configuredOutputBaseDir?: string;
3331
- discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[] };
3332
- workflowAgentScope?: unknown;
3333
- claimedOutputPaths: Map<string, string>;
3334
- entries: Array<{ key: string; params: Record<string, unknown> }>;
3335
- }): { error?: string; claims?: Map<string, string> } {
3336
- const claims = new Map(input.claimedOutputPaths);
3337
- for (const { key, params } of input.entries) {
3338
- const resolved = resolveWorkflowChildOutputPath({ ...input, key, params });
3339
- if (!resolved) continue;
3340
- 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.` };
3341
- const previous = claims.get(resolved);
3342
- if (previous) return { error: `Workflow children '${previous}' and '${key}' resolve output to the same path: ${resolved}. Use distinct child output paths.` };
3343
- claims.set(resolved, key);
3344
- }
3345
- return { claims };
3346
- }
3347
-
3348
- function applyWorkflowChildOutputClaims(target: Map<string, string>, claims: Map<string, string>): void {
3349
- target.clear();
3350
- for (const [resolved, key] of claims) target.set(resolved, key);
3351
- }
3352
-
3353
- function prepareWorkflowChildLaunchParams(input: {
3354
- workflowDefaults: SubagentParamsLike;
3355
- childParams: Record<string, unknown>;
3356
- parentWorkflowRunId: string;
3357
- workflowKey: string;
3358
- ctxCwd: string;
3359
- workflowCwd: string;
3360
- artifactsDir: string;
3361
- aggregateOutputPath?: string;
3362
- configuredOutputBaseDir?: string;
3363
- discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[] };
3364
- workflowAgentScope?: unknown;
3365
- options?: { missionDetached?: boolean; suppressRoutineResultIntercom?: boolean; runFanoutBudget?: RunFanoutBudgetDescriptor };
3366
- }): SubagentParamsLike {
3367
- let childParams = input.childParams;
3368
- if (input.childParams.output === undefined && input.childParams.resume === undefined) {
3369
- childParams = { ...input.childParams, output: workflowChildDefaultOutput(input.aggregateOutputPath, input.artifactsDir, input.parentWorkflowRunId, input.workflowKey) };
3370
- } else if (input.childParams.resume === undefined) {
3371
- 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 });
3372
- if (resolvedOutput) childParams = { ...input.childParams, output: resolvedOutput };
3373
- }
3374
- return prepareWorkflowLaunchParams(input.workflowDefaults, childParams, input.parentWorkflowRunId, input.workflowKey, input.options);
3375
- }
3376
-
3377
- function buildChainWorktreeTaskCwdError(chain: ChainStep[], sharedCwd: string): string | undefined {
3378
- for (let stepIndex = 0; stepIndex < chain.length; stepIndex++) {
3379
- const step = chain[stepIndex]!;
3380
- if (!isParallelStep(step) || !step.worktree) continue;
3381
- const stepCwd = resolveChildCwd(sharedCwd, step.cwd);
3382
- const conflict = findWorktreeTaskCwdConflict(step.parallel, stepCwd);
3383
- if (!conflict) continue;
3384
- const detail = formatWorktreeTaskCwdConflict(conflict, stepCwd);
3385
- return `parallel chain step ${stepIndex + 1}: ${detail}`;
3386
- }
3387
- return undefined;
3388
- }
3389
-
3390
- function resolveParallelTaskCwd(
3391
- task: TaskParam,
3392
- paramsCwd: string,
3393
- worktreeSetup: WorktreeSetup | undefined,
3394
- index: number,
3395
- ): string {
3396
- if (worktreeSetup) return worktreeSetup.worktrees[index]!.agentCwd;
3397
- return resolveChildCwd(paramsCwd, task.cwd);
3398
- }
3399
-
3400
- function finalizeParallelWorktreeHandoff(input: {
3401
- worktreeSetup: WorktreeSetup;
3402
- artifactsDir: string;
3403
- runId: string;
3404
- cwd: string;
3405
- tasks: TaskParam[];
3406
- results: SingleResult[];
3407
- }): { suffix: string; reference?: NonNullable<Details["parallelHandoff"]> } {
3408
- const diffsDir = path.join(input.artifactsDir, "worktree-diffs", input.runId);
3409
- const diffs = diffWorktrees(input.worktreeSetup, input.tasks.map((task) => task.agent), diffsDir);
3410
- const diffSummary = formatWorktreeDiffSummary(diffs);
3411
- const manifestPath = parallelHandoffPath(input.artifactsDir, input.runId);
3412
- const handoff = {
3413
- manifestPath,
3414
- runId: input.runId,
3415
- mode: "parallel" as const,
3416
- source: "foreground" as const,
3417
- cwd: input.cwd,
3418
- stepIndex: 0,
3419
- flatStartIndex: 0,
3420
- setup: input.worktreeSetup,
3421
- diffs,
3422
- results: input.results.map((result) => ({
3423
- agent: result.agent,
3424
- status: resolveSubagentResultStatus(omitUndefinedProperties({
3425
- exitCode: result.exitCode,
3426
- interrupted: result.interrupted,
3427
- detached: result.detached,
3428
- state: result.stopped ? "stopped" : undefined,
3429
- processSignal: result.processSignal,
3430
- timedOut: result.timedOut,
3431
- stopped: result.stopped,
3432
- turnBudgetExceeded: result.turnBudgetExceeded,
3433
- })),
3434
- summary: resultSummaryForIntercom(result),
3435
- ...(result.artifactPaths?.outputPath ? { outputPath: result.artifactPaths.outputPath } : {}),
3436
- ...(result.structuredOutput !== undefined ? { structuredOutput: result.structuredOutput } : {}),
3437
- ...(result.structuredOutputPath ? { structuredOutputPath: result.structuredOutputPath } : {}),
3438
- ...(result.sessionFile ? { sessionPath: result.sessionFile } : {}),
3439
- })),
3440
- };
3441
- try {
3442
- writeParallelHandoffGroup(handoff);
3443
- const cleanup = cleanupWorktrees(input.worktreeSetup, { kind: "preserve", capturedDiffs: diffs, handoffManifestPath: manifestPath });
3444
- const reference = writeParallelHandoffGroup({ ...handoff, cleanup });
3445
- return {
3446
- suffix: [diffSummary, formatParallelHandoffReference(reference)].filter(Boolean).join("\n\n"),
3447
- reference,
3448
- };
3449
- } catch (error) {
3450
- return { suffix: [diffSummary, formatParallelHandoffError(error)].filter(Boolean).join("\n\n") };
3451
- }
3452
- }
3453
-
3454
- function findDuplicateParallelOutputPath(input: {
3455
- tasks: TaskParam[];
3456
- behaviors: ResolvedStepBehavior[];
3457
- paramsCwd: string;
3458
- ctxCwd: string;
3459
- outputBaseDir: string;
3460
- worktreeSetup?: WorktreeSetup;
3461
- }): string | undefined {
3462
- const seen = new Map<string, { index: number; agent: string }>();
3463
- for (let index = 0; index < input.tasks.length; index++) {
3464
- const behavior = input.behaviors[index];
3465
- if (!behavior?.output) continue;
3466
- const task = input.tasks[index]!;
3467
- const taskCwd = resolveParallelTaskCwd(task, input.paramsCwd, input.worktreeSetup, index);
3468
- const outputPath = resolveSingleOutputPath(behavior.output, input.ctxCwd, taskCwd, input.outputBaseDir);
3469
- if (!outputPath) continue;
3470
- const previous = seen.get(outputPath);
3471
- if (previous) {
3472
- return `Parallel tasks ${previous.index + 1} (${previous.agent}) and ${index + 1} (${task.agent}) resolve output to the same path: ${outputPath}. Use distinct output paths.`;
3473
- }
3474
- seen.set(outputPath, { index, agent: task.agent });
3475
- }
3476
- return undefined;
3477
- }
3478
-
3479
- async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Promise<SingleResult[]> {
3480
- // Pre-warm fork session files sequentially before concurrent dispatch to avoid
3481
- // races where multiple workers simultaneously try to branch the same parent session.
3482
- for (let i = 0; i < input.tasks.length; i++) {
3483
- input.sessionFileForTask(input.tasks[i]!.agent, i, input.modelOverrides[i]);
3484
- }
3485
- const completedResults: SingleResult[] = [];
3486
- if (input.foregroundControl) retainForegroundSchedulingOwner(input.foregroundControl);
3487
- return mapConcurrent(input.tasks, input.concurrencyLimit, async (task, index) => {
3488
- const budgetState = usageBudgetState(input.usageBudget, sumResultsCost(completedResults));
3489
- if (budgetState?.exhausted) {
3490
- return {
3491
- index,
3492
- agent: task.agent,
3493
- task: input.taskTexts[index] ?? "(skipped)",
3494
- exitCode: 1,
3495
- messages: [],
3496
- usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
3497
- error: usageBudgetExceededMessage(budgetState),
3498
- skipped: true,
3499
- } as SingleResult;
3500
- }
3501
- const behavior = input.behaviors[index];
3502
- const effectiveSkills = behavior?.skills;
3503
- const taskCwd = resolveParallelTaskCwd(task, input.paramsCwd, input.worktreeSetup, index);
3504
- const readInstructions = behavior
3505
- ? buildChainInstructions({ ...behavior, output: false, progress: false }, taskCwd, false)
3506
- : { prefix: "", suffix: "" };
3507
- const progressInstructions = behavior
3508
- ? buildChainInstructions({ ...behavior, output: false, reads: false }, input.progressDir, index === input.firstProgressIndex)
3509
- : { prefix: "", suffix: "" };
3510
- const outputPath = resolveSingleOutputPath(behavior?.output, input.ctx.cwd, taskCwd, input.outputBaseDir);
3511
- const agentConfig = input.agents.find((agent) => agent.name === task.agent);
3512
- const taskText = injectSingleOutputInstruction(
3513
- `${readInstructions.prefix}${input.taskTexts[index]!}${progressInstructions.suffix}`,
3514
- outputPath,
3515
- agentConfig,
3516
- );
3517
- const interruptController = new AbortController();
3518
- if (input.foregroundControl) {
3519
- const model = input.modelOverrides[index];
3520
- const thinking = resolveEffectiveThinking(model, input.thinkingOverrideForTask(task.agent, index, model));
3521
- beginForegroundChild(input.foregroundControl, {
3522
- index,
3523
- agent: task.agent,
3524
- authoredTask: input.taskDescriptions[index] ?? "",
3525
- effectivePrompt: taskText,
3526
- cwd: taskCwd,
3527
- ...(outputPath ? { outputPath } : {}),
3528
- rerun: { params: compactOptional<Record<string, unknown>>({
3529
- agent: task.agent,
3530
- task: input.taskDescriptions[index] ?? "",
3531
- async: false,
3532
- cwd: input.worktreeSetup ? undefined : taskCwd,
3533
- worktree: input.worktreeSetup ? true : false,
3534
- context: input.contextPolicy.contextForAgent(task.agent),
3535
- share: input.shareEnabled || undefined,
3536
- maxOutput: input.maxOutput,
3537
- timeoutMs: input.timeoutMs,
3538
- turnBudget: input.turnBudget,
3539
- ...(model ? { model } : {}),
3540
- ...(effectiveSkills !== undefined ? { skill: effectiveSkills } : {}),
3541
- ...(outputPath ? { output: outputPath } : {}),
3542
- ...(behavior?.outputMode !== undefined ? { outputMode: behavior.outputMode } : {}),
3543
- ...(behavior?.reads !== undefined ? { reads: behavior.reads } : {}),
3544
- ...(behavior?.progress !== undefined ? { progress: behavior.progress } : {}),
3545
- ...(task.toolBudget !== undefined ? { toolBudget: task.toolBudget } : {}),
3546
- ...(task.outputSchema !== undefined ? { outputSchema: task.outputSchema } : {}),
3547
- ...(task.acceptance !== undefined ? { acceptance: task.acceptance } : {}),
3548
- ...(task.agentContract !== undefined || input.agentContract !== undefined ? { agentContract: task.agentContract ?? input.agentContract } : {}),
3549
- }) },
3550
- description: `${task.agent} child`,
3551
- ...(model ? { model } : {}),
3552
- ...(thinking ? { thinking } : {}),
3553
- interrupt: () => {
3554
- if (interruptController.signal.aborted) return false;
3555
- interruptController.abort();
3556
- return true;
3557
- },
3558
- });
3559
- }
3560
- const structuredRuntime = task.outputSchema
3561
- ? createStructuredOutputRuntime(task.outputSchema, path.join(input.artifactsDir, "structured-output", input.runId))
3562
- : undefined;
3563
- let detachedReceipt = false;
3564
- const result = await runSync(input.ctx.cwd, input.agents, task.agent, taskText, compactOptional<Parameters<typeof runSync>[4]>({
3565
- permissions: input.permissions,
3566
- parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
3567
- llmIntentArbiter: createTaskMutationArbiter(input.ctx),
3568
- ...workflowForegroundSteeringLaunchOptions(input.foregroundControl, index),
3569
- context: input.contextPolicy.contextForAgent(task.agent),
3570
- runFanoutBudget: { ...input.runFanoutBudget, parentPath: `${input.runFanoutBudget.parentPath ? `${input.runFanoutBudget.parentPath}/` : ""}tasks[${index}]` },
3571
- cwd: taskCwd,
3572
- signal: input.signal,
3573
- interruptSignal: interruptController.signal,
3574
- allowIntercomDetach: agentConfig?.systemPrompt?.includes(INTERCOM_BRIDGE_MARKER) === true,
3575
- intercomEvents: input.intercomEvents,
3576
- runId: input.runId,
3577
- index,
3578
- sessionDir: input.sessionDirForIndex(index),
3579
- sessionFile: input.sessionFileForTask(task.agent, index, input.modelOverrides[index]),
3580
- share: input.shareEnabled,
3581
- artifactsDir: input.artifactConfig.enabled ? input.artifactsDir : undefined,
3582
- artifactConfig: input.artifactConfig,
3583
- maxOutput: input.maxOutput,
3584
- outputPath,
3585
- outputMode: behavior?.outputMode,
3586
- maxSubagentDepth: input.maxSubagentDepths[index],
3587
- waitToolEnabled: input.waitToolEnabled,
3588
- capabilityCeiling: input.capabilityCeiling,
3589
- controlConfig: input.controlConfig,
3590
- onControlEvent: input.onControlEvent,
3591
- onDetachedExit: (result) => {
3592
- try {
3593
- updateRememberedForegroundChild(input.state, { runId: input.runId, mode: "parallel", cwd: taskCwd, sessionId: input.parentSessionId, index, result, events: input.intercomEvents });
3594
- } finally {
3595
- try {
3596
- if (input.foregroundControl) finishForegroundChild(input.foregroundControl, index);
3597
- } finally {
3598
- removeForegroundControlIfIdle(input.state, input.runId);
3599
- }
3600
- }
3601
- },
3602
- intercomSessionName: input.childIntercomTarget?.(task.agent, index),
3603
- orchestratorIntercomTarget: input.orchestratorIntercomTarget,
3604
- nestedRoute: input.foregroundControl?.nestedRoute,
3605
- modelOverride: input.modelOverrides[index],
3606
- thinkingOverride: input.thinkingOverrideForTask(task.agent, index, input.modelOverrides[index]),
3607
- availableModels: input.availableModels,
3608
- preferredModelProvider: input.parentModel?.provider,
3609
- modelScope: input.modelScope,
3610
- skills: effectiveSkills === false ? [] : effectiveSkills,
3611
- structuredOutput: structuredRuntime,
3612
- agentContract: task.agentContract ?? input.agentContract,
3613
- acceptance: task.acceptance,
3614
- acceptanceContext: { mode: "parallel" },
3615
- onEffectivePrompt: input.foregroundControl ? (prompt) => updateLiveEffectivePrompt(input.foregroundControl!, index, prompt) : undefined,
3616
- timeoutMs: input.timeoutMs,
3617
- deadlineAt: input.deadlineAt,
3618
- turnBudget: input.turnBudget,
3619
- toolBudget: input.toolBudgets[index],
3620
- onUpdate: input.onUpdate
3621
- ? (progressUpdate) => {
3622
- const stepResults = progressUpdate.details?.results || [];
3623
- const stepProgress = progressUpdate.details?.progress || [];
3624
- if (input.foregroundControl && stepProgress.length > 0) {
3625
- updateForegroundChild(input.foregroundControl, index, stepProgress[0]);
3626
- }
3627
- if (stepResults.length > 0) input.liveResults[index] = stepResults[0];
3628
- if (stepProgress.length > 0) input.liveProgress[index] = stepProgress[0];
3629
- const mergedResults = input.liveResults.filter((result): result is SingleResult => result !== undefined);
3630
- const mergedProgress = input.liveProgress.filter((progress): progress is AgentProgress => progress !== undefined);
3631
- input.onUpdate?.({
3632
- content: progressUpdate.content,
3633
- details: compactOptional<Details>({
3634
- mode: "parallel",
3635
- results: mergedResults,
3636
- progress: mergedProgress,
3637
- controlEvents: progressUpdate.details?.controlEvents,
3638
- totalSteps: input.tasks.length,
3639
- }),
3640
- });
3641
- }
3642
- : undefined,
3643
- })).then((result) => {
3644
- detachedReceipt = result.detached === true;
3645
- return result;
3646
- }).finally(() => {
3647
- // mapConcurrent rejects before siblings settle, so every attached child
3648
- // attempts idle removal after releasing its own control. Detached receipts
3649
- // transfer both responsibilities to the authoritative exit callback.
3650
- if (!detachedReceipt) {
3651
- if (input.foregroundControl) finishForegroundChild(input.foregroundControl, index);
3652
- removeForegroundControlIfIdle(input.state, input.runId);
3653
- }
3654
- });
3655
- completedResults.push(result);
3656
- return result;
3657
- }, input.globalSemaphore, () => {
3658
- if (input.foregroundControl) settleForegroundSchedulingOwner(input.foregroundControl);
3659
- removeForegroundControlIfIdle(input.state, input.runId);
3660
- });
3661
- }
3662
-
3663
- async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps): Promise<AgentToolResult<Details>> {
3664
- const {
3665
- params,
3666
- effectiveCwd,
3667
- agents,
3668
- ctx,
3669
- signal,
3670
- runId,
3671
- sessionDirForIndex,
3672
- sessionFileForIndex,
3673
- sessionFileForTask,
3674
- thinkingOverrideForTask,
3675
- shareEnabled,
3676
- artifactConfig,
3677
- artifactsDir,
3678
- backgroundRequestedWhileClarifying,
3679
- onUpdate,
3680
- sessionRoot,
3681
- controlConfig,
3682
- contextPolicy,
3683
- } = data;
3684
- const onControlEvent = createForegroundControlNotifier(data, deps);
3685
- const childIntercomTarget = data.intercomBridge.active ? resolveSubagentIntercomTarget : undefined;
3686
- const allProgress: AgentProgress[] = [];
3687
- const allArtifactPaths: ArtifactPaths[] = [];
3688
- const tasks = params.tasks!;
3689
- const maxParallelTasks = resolveTopLevelParallelMaxTasks(deps.config.parallel?.maxTasks);
3690
- const parallelConcurrency = resolveTopLevelParallelConcurrency(params.concurrency, deps.config.parallel?.concurrency);
3691
-
3692
- if (tasks.length > maxParallelTasks)
3693
- return {
3694
- content: [{ type: "text", text: `Max ${maxParallelTasks} tasks` }],
3695
- isError: true,
3696
- details: { mode: "parallel" as const, results: [] },
3697
- };
3698
-
3699
- const agentConfigs: AgentConfig[] = [];
3700
- for (const t of tasks) {
3701
- const config = agents.find((a) => a.name === t.agent);
3702
- if (!config) {
3703
- return {
3704
- content: [{ type: "text", text: `Unknown agent: ${t.agent}` }],
3705
- isError: true,
3706
- details: { mode: "parallel" as const, results: [] },
3707
- };
3708
- }
3709
- agentConfigs.push(config);
3710
- }
3711
-
3712
- const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
3713
- const maxSubagentDepths = agentConfigs.map((config) =>
3714
- resolveChildMaxSubagentDepth(currentMaxSubagentDepth, config.maxSubagentDepth),
3715
- );
3716
- const toolBudgets: (ResolvedToolBudget | undefined)[] = [];
3717
- for (let index = 0; index < tasks.length; index++) {
3718
- const resolved = resolveEffectiveToolBudget(omitUndefinedProperties({ stepBudget: tasks[index]?.toolBudget, runBudget: data.toolBudget, agentBudget: agentConfigs[index]?.toolBudget, configBudget: data.configToolBudget }));
3719
- if (resolved.error) return buildParallelModeError(resolved.error);
3720
- toolBudgets.push(resolved.toolBudget);
3721
- }
3722
-
3723
- if (params.worktree) {
3724
- const worktreeTaskCwdError = buildParallelWorktreeTaskCwdError(tasks, effectiveCwd);
3725
- if (worktreeTaskCwdError) return buildParallelModeError(worktreeTaskCwdError);
3726
- }
3727
-
3728
- const parentModel = data.parentModel;
3729
- const currentProvider = parentModel?.provider;
3730
- const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo);
3731
- let taskTexts = tasks.map((t) => t.task);
3732
- const skillOverrides: (string[] | false | undefined)[] = tasks.map((t) =>
3733
- normalizeSkillInput(t.skill),
3734
- );
3735
- const behaviorOverrides: StepOverrides[] = tasks.map((task, index) => ({
3736
- ...(task.output !== undefined && task.output !== true ? { output: task.output } : {}),
3737
- ...(task.outputMode !== undefined ? { outputMode: task.outputMode } : {}),
3738
- ...(task.reads !== undefined && task.reads !== true ? { reads: task.reads } : {}),
3739
- ...(task.progress !== undefined ? { progress: task.progress } : {}),
3740
- ...(skillOverrides[index] !== undefined ? { skills: skillOverrides[index] } : {}),
3741
- ...(task.model !== undefined ? { model: task.model } : {}),
3742
- }));
3743
- const modelOverrides: (string | undefined)[] = tasks.map((_, i) =>
3744
- resolveEffectiveSubagentModel(behaviorOverrides[i]?.model, agentConfigs[i]?.model, parentModel, availableModels, currentProvider, data.modelScope === undefined ? {} : { scope: data.modelScope }),
3745
- );
3746
-
3747
- if (params.clarify === true && ctx.hasUI) {
3748
- const behaviors = agentConfigs.map((c, i) =>
3749
- resolveStepBehavior(c, behaviorOverrides[i]!),
3750
- );
3751
- const availableSkills = discoverAvailableSkills(effectiveCwd);
3752
-
3753
- const result = await ctx.ui.custom<ChainClarifyResult>(
3754
- (tui, theme, _kb, done) =>
3755
- new ChainClarifyComponent(
3756
- tui, theme,
3757
- agentConfigs,
3758
- taskTexts,
3759
- "",
3760
- undefined,
3761
- behaviors,
3762
- availableModels,
3763
- currentProvider,
3764
- availableSkills,
3765
- done,
3766
- "parallel",
3767
- ),
3768
- { overlay: true, overlayOptions: { anchor: "center", width: 84, maxHeight: "80%" } },
3769
- );
3770
-
3771
- if (!result || !result.confirmed) {
3772
- return { content: [{ type: "text", text: "Cancelled" }], details: { mode: "parallel", results: [] } };
3773
- }
3774
-
3775
- taskTexts = result.templates;
3776
- for (let i = 0; i < result.behaviorOverrides.length; i++) {
3777
- const override = result.behaviorOverrides[i];
3778
- if (override?.model !== undefined) {
3779
- modelOverrides[i] = resolveEffectiveSubagentModel(override.model, agentConfigs[i]?.model, parentModel, availableModels, currentProvider, data.modelScope === undefined ? {} : { scope: data.modelScope });
3780
- behaviorOverrides[i]!.model = override.model;
3781
- }
3782
- if (override?.output !== undefined) behaviorOverrides[i]!.output = override.output;
3783
- if (override?.reads !== undefined) behaviorOverrides[i]!.reads = override.reads;
3784
- if (override?.progress !== undefined) behaviorOverrides[i]!.progress = override.progress;
3785
- if (override?.skills !== undefined) {
3786
- skillOverrides[i] = override.skills;
3787
- behaviorOverrides[i]!.skills = override.skills;
3788
- }
3789
- }
3790
-
3791
- if (result.runInBackground) {
3792
- if (!isAsyncAvailable()) {
3793
- return {
3794
- 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." }],
3795
- isError: true,
3796
- details: { mode: "parallel" as const, results: [] },
3797
- };
3798
- }
3799
- const id = data.asyncRunId;
3800
- const capacity = acquireClarifiedBackgroundCapacity(data, deps, "parallel", id);
3801
- if (capacity.errorResult) return capacity.errorResult;
3802
- const asyncCtx = compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
3803
- pi: deps.pi,
3804
- cwd: ctx.cwd,
3805
- currentSessionId: data.parentSessionId!,
3806
- parentSessionId: data.parentPiSessionId,
3807
- currentModelProvider: parentModel?.provider,
3808
- currentModel: parentModel,
3809
- modelScope: data.modelScope,
3810
- interactive: ctx.hasUI,
3811
- permissions: deps.config.permissions,
3812
- });
3813
- const parallelTasks = tasks.map((t, i) => {
3814
- const taskText = shouldForkAgent(contextPolicy, t.agent) ? wrapForkTask(taskTexts[i]!) : taskTexts[i]!;
3815
- const progress = taskDisallowsFileUpdates(taskText) ? false : behaviorOverrides[i]?.progress;
3816
- return compactOptional<ParallelTaskItem>({
3817
- agent: t.agent,
3818
- task: taskText,
3819
- cwd: t.cwd,
3820
- ...(behaviorOverrides[i]?.model !== undefined ? { model: behaviorOverrides[i]!.model } : {}),
3821
- ...(skillOverrides[i] !== undefined ? { skill: skillOverrides[i] } : {}),
3822
- ...(behaviorOverrides[i]?.output !== undefined ? { output: behaviorOverrides[i]!.output } : {}),
3823
- ...(behaviorOverrides[i]?.outputMode !== undefined ? { outputMode: behaviorOverrides[i]!.outputMode } : {}),
3824
- ...(behaviorOverrides[i]?.reads !== undefined ? { reads: behaviorOverrides[i]!.reads } : {}),
3825
- ...(progress !== undefined ? { progress } : {}),
3826
- ...(t.toolBudget !== undefined ? { toolBudget: t.toolBudget } : {}),
3827
- ...(t.outputSchema !== undefined ? { outputSchema: t.outputSchema } : {}),
3828
- ...(t.acceptance !== undefined ? { acceptance: t.acceptance } : {}),
3829
- ...(t.agentContract !== undefined ? { agentContract: t.agentContract } : {}),
3830
- });
3831
- });
3832
- try {
3833
- return executeAsyncChain(id, compactOptional<Parameters<typeof executeAsyncChain>[1]>({
3834
- chain: [compactOptional<ParallelStep>({ parallel: parallelTasks, concurrency: parallelConcurrency, worktree: params.worktree })],
3835
- resultMode: "parallel",
3836
- goal: taskTexts[0] ?? "",
3837
- agents,
3838
- ctx: asyncCtx,
3839
- availableModels,
3840
- cwd: effectiveCwd,
3841
- maxOutput: params.maxOutput,
3842
- artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
3843
- artifactConfig,
3844
- shareEnabled,
3845
- sessionRoot,
3846
- chainSkills: [],
3847
- activeAsyncCapacity: capacity.activeAsyncCapacity,
3848
- sessionFilesByFlatIndex: tasks.map((task, index) => sessionFileForTask(task.agent, index, modelOverrides[index])),
3849
- thinkingOverridesByFlatIndex: tasks.map((task, index) => thinkingOverrideForTask(task.agent, index, modelOverrides[index])),
3850
- contextForAgent: contextPolicy.contextForAgent,
3851
- maxSubagentDepth: currentMaxSubagentDepth,
3852
- waitToolEnabled: deps.waitToolEnabled,
3853
- worktreeSetupHook: deps.config.worktreeSetupHook,
3854
- worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
3855
- worktreeBaseDir: deps.config.worktreeBaseDir,
3856
- controlConfig,
3857
- agentContract: params.agentContract,
3858
- controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
3859
- childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
3860
- nestedRoute: data.nestedRoute,
3861
- timeoutMs: data.timeoutMs,
3862
- turnBudget: data.turnBudget,
3863
- usageBudget: data.usageBudget,
3864
- toolBudget: data.toolBudget,
3865
- configToolBudget: data.configToolBudget,
3866
- globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
3867
- runFanoutBudget: data.runFanoutBudget,
3868
- }));
3869
- } finally {
3870
- finalizeClarifiedBackgroundCapacity(data, deps, capacity.activeAsyncCapacity);
3871
- }
3872
- }
3873
- }
3874
-
3875
- const behaviors = tasks.map((task, index) => {
3876
- let behavior = suppressProgressForReadOnlyTask(resolveStepBehavior(agentConfigs[index]!, behaviorOverrides[index]!), taskTexts[index]);
3877
- if (behaviorOverrides[index]?.output === undefined && typeof behavior.output === "string" && !path.isAbsolute(behavior.output)) {
3878
- behavior = { ...behavior, output: path.join("parallel-0", `${index}-${task.agent}`, behavior.output) };
3879
- }
3880
- return behavior;
3881
- });
3882
- const firstProgressIndex = behaviors.findIndex((behavior) => behavior.progress);
3883
- const liveResults: (SingleResult | undefined)[] = new Array(tasks.length).fill(undefined);
3884
- const liveProgress: (AgentProgress | undefined)[] = new Array(tasks.length).fill(undefined);
3885
- const foregroundControl = deps.state.foregroundControls.get(runId);
3886
- const { setup: worktreeSetup, errorResult } = createParallelWorktreeSetup(
3887
- params.worktree,
3888
- effectiveCwd,
3889
- runId,
3890
- tasks,
3891
- deps.config.worktreeSetupHook,
3892
- deps.config.worktreeSetupHookTimeoutMs,
3893
- deps.config.worktreeBaseDir,
3894
- );
3895
- if (errorResult) return errorResult;
3896
-
3897
- let worktreeCleanupHandled = false;
3898
- let pendingHandoff: Details["parallelHandoff"];
3899
- try {
3900
- if (worktreeSetup) {
3901
- pendingHandoff = writePendingParallelHandoff({
3902
- manifestPath: parallelHandoffPath(artifactsDir, runId),
3903
- runId,
3904
- mode: "parallel",
3905
- source: "foreground",
3906
- cwd: effectiveCwd,
3907
- stepIndex: 0,
3908
- flatStartIndex: 0,
3909
- setup: worktreeSetup,
3910
- });
3911
- }
3912
- const outputBaseDir = path.join(artifactsDir, "outputs", runId);
3913
- const duplicateOutputError = findDuplicateParallelOutputPath(omitUndefinedProperties({
3914
- tasks,
3915
- behaviors,
3916
- paramsCwd: effectiveCwd,
3917
- ctxCwd: ctx.cwd,
3918
- outputBaseDir,
3919
- worktreeSetup,
3920
- }));
3921
- if (duplicateOutputError) return buildParallelModeError(duplicateOutputError);
3922
- for (let index = 0; index < tasks.length; index++) {
3923
- const taskCwd = resolveParallelTaskCwd(tasks[index]!, effectiveCwd, worktreeSetup, index);
3924
- const outputPath = resolveSingleOutputPath(behaviors[index]?.output, ctx.cwd, taskCwd, outputBaseDir);
3925
- const validationError = validateFileOnlyOutputMode(behaviors[index]?.outputMode, outputPath, `Parallel task ${index + 1} (${tasks[index]!.agent})`);
3926
- if (validationError) return buildParallelModeError(validationError);
3927
- }
3928
-
3929
- const parallelProgressPrecreated = firstProgressIndex !== -1;
3930
- const parallelProgressDir = path.join(artifactsDir, "progress", runId);
3931
- if (parallelProgressPrecreated) writeInitialProgressFile(parallelProgressDir);
3932
-
3933
- const taskDescriptions = taskTexts.map((taskText) => taskText.trim());
3934
- for (let i = 0; i < taskTexts.length; i++) {
3935
- if (shouldForkAgent(contextPolicy, tasks[i]!.agent)) taskTexts[i] = wrapForkTask(taskTexts[i]!);
3936
- }
3937
-
3938
- const deadlineAt = data.deadlineAt ?? (data.timeoutMs !== undefined ? Date.now() + data.timeoutMs : undefined);
3939
- const results = await runForegroundParallelTasks(compactOptional<ForegroundParallelRunInput>({
3940
- tasks,
3941
- permissions: deps.config.permissions,
3942
- taskTexts,
3943
- taskDescriptions,
3944
- agents,
3945
- ctx,
3946
- state: deps.state,
3947
- intercomEvents: deps.pi.events,
3948
- parentSessionId: data.parentSessionId,
3949
- capabilityCeiling: data.capabilityCeiling,
3950
- runFanoutBudget: data.runFanoutBudget,
3951
- signal,
3952
- runId,
3953
- sessionDirForIndex,
3954
- sessionFileForIndex,
3955
- sessionFileForTask,
3956
- thinkingOverrideForTask,
3957
- shareEnabled,
3958
- artifactConfig,
3959
- artifactsDir,
3960
- outputBaseDir,
3961
- maxOutput: params.maxOutput,
3962
- paramsCwd: effectiveCwd,
3963
- progressDir: parallelProgressDir,
3964
- availableModels,
3965
- modelScope: data.modelScope,
3966
- parentModel,
3967
- modelOverrides,
3968
- behaviors,
3969
- firstProgressIndex: parallelProgressPrecreated ? -1 : firstProgressIndex,
3970
- controlConfig,
3971
- contextPolicy,
3972
- onControlEvent,
3973
- childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(runId, agent, index) : undefined,
3974
- orchestratorIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
3975
- foregroundControl,
3976
- concurrencyLimit: parallelConcurrency,
3977
- globalSemaphore: new Semaphore(deps.config.globalConcurrencyLimit ?? DEFAULT_GLOBAL_CONCURRENCY_LIMIT),
3978
- maxSubagentDepths,
3979
- waitToolEnabled: deps.waitToolEnabled,
3980
- liveResults,
3981
- liveProgress,
3982
- onUpdate,
3983
- worktreeSetup,
3984
- timeoutMs: data.timeoutMs,
3985
- deadlineAt,
3986
- turnBudget: data.turnBudget,
3987
- usageBudget: data.usageBudget,
3988
- toolBudgets,
3989
- agentContract: params.agentContract,
3990
- }));
3991
- for (let i = 0; i < results.length; i++) {
3992
- const run = results[i]!;
3993
- recordRun(run.agent, taskTexts[i]!, run.exitCode, run.progressSummary?.durationMs ?? 0);
3994
- }
2902
+ } catch (error) {
2903
+ return `Output file error: ${outputPath}\n${error instanceof Error ? error.message : String(error)}`;
2904
+ }
2905
+ }
3995
2906
 
3996
- for (const result of results) {
3997
- if (result.progress) allProgress.push(result.progress);
3998
- if (result.artifactPaths) allArtifactPaths.push(result.artifactPaths);
3999
- }
2907
+ function appendWorkflowOutputWarning(text: string, warning: string | undefined): string {
2908
+ return warning ? `${text}\n\n${warning}` : text;
2909
+ }
4000
2910
 
4001
- if (foregroundControl) {
4002
- updateForegroundNestedProjection(foregroundControl);
4003
- attachRootChildrenToSteps(runId, results, foregroundControl.nestedChildren);
4004
- }
4005
- const detached = results.find((result) => result.detached);
4006
- let handoff: ReturnType<typeof finalizeParallelWorktreeHandoff> | undefined;
4007
- if (worktreeSetup) {
4008
- worktreeCleanupHandled = true;
4009
- handoff = detached
4010
- ? { suffix: pendingHandoff ? formatParallelHandoffReference(pendingHandoff) : "", reference: pendingHandoff }
4011
- : finalizeParallelWorktreeHandoff({ worktreeSetup, artifactsDir, runId, cwd: effectiveCwd, tasks, results });
4012
- }
4013
- const interrupted = results.find((result) => result.interrupted);
4014
- const totalCost = sumResultsCost(results);
4015
- const details = compactForegroundDetails(compactOptional<Details>({
4016
- mode: "parallel",
4017
- runId,
4018
- timeoutMs: data.timeoutMs,
4019
- results,
4020
- progress: params.includeProgress ? allProgress : undefined,
4021
- artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
4022
- totalChildUsage: sumResultsUsage(results),
4023
- totalCost,
4024
- usageBudget: usageBudgetState(data.usageBudget, totalCost),
4025
- ...(handoff?.reference ? { parallelHandoff: handoff.reference } : {}),
4026
- }));
4027
- rememberForegroundRun(deps.state, { runId, mode: "parallel", cwd: effectiveCwd, sessionId: data.parentSessionId, results: details.results });
4028
- if (interrupted) {
4029
- return {
4030
- content: [{ type: "text", text: `Parallel run paused after interrupt (${interrupted.agent}). Waiting for explicit next action.` }],
4031
- details,
4032
- };
4033
- }
4034
- if (detached) {
4035
- const handoffSuffix = handoff?.suffix ? `\n\n${handoff.suffix}` : "";
4036
- return {
4037
- 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}` }],
4038
- details,
4039
- };
4040
- }
2911
+ function resolveWorkflowChildOutputPath(input: {
2912
+ ctxCwd: string;
2913
+ workflowCwd: string;
2914
+ artifactsDir: string;
2915
+ workflowRunId: string;
2916
+ aggregateOutputPath?: string;
2917
+ configuredOutputBaseDir?: string;
2918
+ discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[] };
2919
+ workflowAgentScope?: unknown;
2920
+ key: string;
2921
+ params: Record<string, unknown>;
2922
+ }): string | undefined {
2923
+ const rawOutput = input.params.output;
2924
+ const hasExplicitOutput = typeof rawOutput === "string" || typeof rawOutput === "boolean";
2925
+ const childCwd = typeof input.params.cwd === "string" ? resolveChildCwd(input.workflowCwd, input.params.cwd) : input.workflowCwd;
2926
+ const agentScope = resolveExecutionAgentScope(input.params.agentScope ?? input.workflowAgentScope);
2927
+ const agents = input.discoverAgents(childCwd, agentScope).agents;
2928
+ const agent = typeof input.params.agent === "string" ? resolveAgentName(input.params.agent, agents).agent : undefined;
2929
+ const agentOutput = typeof agent?.output === "string" ? agent.output : undefined;
2930
+ const output = rawOutput === true || rawOutput === "true"
2931
+ ? agentOutput
2932
+ : hasExplicitOutput
2933
+ ? rawOutput
2934
+ : input.aggregateOutputPath
2935
+ ? workflowChildDefaultOutput(input.aggregateOutputPath, input.artifactsDir, input.workflowRunId, input.key)
2936
+ : agentOutput;
2937
+ return resolveSingleOutputPath(output, input.ctxCwd, childCwd, input.configuredOutputBaseDir);
2938
+ }
4041
2939
 
4042
- const suppressRoutineResultIntercom = shouldSuppressRoutineResultIntercom({ suppressRoutineResultIntercom: params.suppressRoutineResultIntercom, results });
4043
- if (foregroundControl) updateForegroundNestedProjection(foregroundControl);
4044
- if (!suppressRoutineResultIntercom) {
4045
- const intercomReceipt = await maybeBuildForegroundIntercomReceipt({
4046
- pi: deps.pi,
4047
- intercomBridge: data.intercomBridge,
4048
- runId,
4049
- mode: "parallel",
4050
- details,
4051
- ...(params.workflowParentRunId !== undefined ? { preserveDetailsOutputs: true } : {}),
4052
- ...(foregroundControl?.nestedChildren?.length ? { nestedChildren: foregroundControl.nestedChildren } : {}),
4053
- });
4054
- if (intercomReceipt) {
4055
- return {
4056
- content: [{ type: "text", text: intercomReceipt.text }],
4057
- details: intercomReceipt.details,
4058
- };
4059
- }
4060
- }
2940
+ function workflowChildOutputClaims(input: {
2941
+ ctxCwd: string;
2942
+ workflowCwd: string;
2943
+ artifactsDir: string;
2944
+ workflowRunId: string;
2945
+ aggregateOutputPath?: string;
2946
+ configuredOutputBaseDir?: string;
2947
+ discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[] };
2948
+ workflowAgentScope?: unknown;
2949
+ claimedOutputPaths: Map<string, string>;
2950
+ entries: Array<{ key: string; params: Record<string, unknown> }>;
2951
+ }): { error?: string; claims?: Map<string, string> } {
2952
+ const claims = new Map(input.claimedOutputPaths);
2953
+ for (const { key, params } of input.entries) {
2954
+ const resolved = resolveWorkflowChildOutputPath({ ...input, key, params });
2955
+ if (!resolved) continue;
2956
+ const previous = claims.get(resolved);
2957
+ if (previous) return { error: `Workflow children '${previous}' and '${key}' resolve output to the same path: ${resolved}. Use distinct child output paths.` };
2958
+ claims.set(resolved, key);
2959
+ }
2960
+ return { claims };
2961
+ }
4061
2962
 
4062
- const worktreeSuffix = handoff?.suffix ?? "";
4063
- const ok = results.filter((result) => result.exitCode === 0).length;
4064
- const downgradeNote = backgroundRequestedWhileClarifying ? " (background requested, but clarify kept this run foreground)" : "";
4065
- const aggregatedOutput = aggregateParallelOutputs(
4066
- results.map((result) => ({
4067
- agent: result.agent,
4068
- output: result.truncation?.text || getSingleResultOutput(result),
4069
- exitCode: result.exitCode,
4070
- ...(result.error === undefined ? {} : { error: result.error }),
4071
- ...(result.timedOut === undefined ? {} : { timedOut: result.timedOut }),
4072
- })),
4073
- (i, agent) => `=== Task ${i + 1}: ${agent} ===`,
4074
- );
2963
+ function applyWorkflowChildOutputClaims(target: Map<string, string>, claims: Map<string, string>): void {
2964
+ target.clear();
2965
+ for (const [resolved, key] of claims) target.set(resolved, key);
2966
+ }
4075
2967
 
4076
- const summary = `${ok}/${results.length} succeeded${downgradeNote}`;
4077
- const fullContent = worktreeSuffix
4078
- ? `${summary}\n\n${aggregatedOutput}\n\n${worktreeSuffix}`
4079
- : `${summary}\n\n${aggregatedOutput}`;
2968
+ function prepareWorkflowChildLaunchParams(input: {
2969
+ workflowDefaults: SubagentParamsLike;
2970
+ childParams: Record<string, unknown>;
2971
+ parentWorkflowRunId: string;
2972
+ workflowKey: string;
2973
+ ctxCwd: string;
2974
+ workflowCwd: string;
2975
+ artifactsDir: string;
2976
+ aggregateOutputPath?: string;
2977
+ configuredOutputBaseDir?: string;
2978
+ discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[] };
2979
+ workflowAgentScope?: unknown;
2980
+ options?: { missionDetached?: boolean; suppressRoutineResultIntercom?: boolean; runFanoutBudget?: RunFanoutBudgetDescriptor; parentDeadlineAt?: number };
2981
+ }): SubagentParamsLike {
2982
+ let childParams = input.childParams;
2983
+ if (input.childParams.output === undefined && input.childParams.resume === undefined && input.aggregateOutputPath !== undefined) {
2984
+ childParams = { ...input.childParams, output: workflowChildDefaultOutput(input.aggregateOutputPath, input.artifactsDir, input.parentWorkflowRunId, input.workflowKey) };
2985
+ } else if (input.childParams.resume === undefined) {
2986
+ 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 });
2987
+ if (resolvedOutput) childParams = { ...input.childParams, output: resolvedOutput };
2988
+ }
2989
+ return prepareWorkflowLaunchParams(input.workflowDefaults, childParams, input.parentWorkflowRunId, input.workflowKey, input.options);
2990
+ }
4080
2991
 
2992
+ function finalizeSingleWorktreeHandoff(input: {
2993
+ worktreeSetup: WorktreeSetup;
2994
+ artifactsDir: string;
2995
+ runId: string;
2996
+ cwd: string;
2997
+ agent: string;
2998
+ result: SingleResult;
2999
+ }): { suffix: string; reference?: NonNullable<Details["parallelHandoff"]> } {
3000
+ const diffsDir = path.join(input.artifactsDir, "worktree-diffs", input.runId);
3001
+ const diffs = diffWorktrees(input.worktreeSetup, [input.agent], diffsDir);
3002
+ const diffSummary = formatWorktreeDiffSummary(diffs);
3003
+ const manifestPath = parallelHandoffPath(input.artifactsDir, input.runId);
3004
+ const handoff = {
3005
+ manifestPath,
3006
+ runId: input.runId,
3007
+ mode: "single" as const,
3008
+ source: "foreground" as const,
3009
+ cwd: input.cwd,
3010
+ stepIndex: 0,
3011
+ flatStartIndex: 0,
3012
+ setup: input.worktreeSetup,
3013
+ diffs,
3014
+ results: [{
3015
+ agent: input.result.agent,
3016
+ status: resolveSubagentResultStatus(omitUndefinedProperties({
3017
+ exitCode: input.result.exitCode,
3018
+ interrupted: input.result.interrupted,
3019
+ detached: input.result.detached,
3020
+ state: input.result.stopped ? "stopped" : undefined,
3021
+ processSignal: input.result.processSignal,
3022
+ timedOut: input.result.timedOut,
3023
+ stopped: input.result.stopped,
3024
+ turnBudgetExceeded: input.result.turnBudgetExceeded,
3025
+ })),
3026
+ summary: resultSummaryForIntercom(input.result),
3027
+ ...(input.result.artifactPaths?.outputPath ? { outputPath: input.result.artifactPaths.outputPath } : {}),
3028
+ ...(input.result.structuredOutput !== undefined ? { structuredOutput: input.result.structuredOutput } : {}),
3029
+ ...(input.result.structuredOutputPath ? { structuredOutputPath: input.result.structuredOutputPath } : {}),
3030
+ ...(input.result.sessionFile ? { sessionPath: input.result.sessionFile } : {}),
3031
+ }],
3032
+ };
3033
+ try {
3034
+ writeParallelHandoffGroup(handoff);
3035
+ const cleanup = cleanupWorktrees(input.worktreeSetup, { kind: "preserve", capturedDiffs: diffs, handoffManifestPath: manifestPath });
3036
+ const reference = writeParallelHandoffGroup({ ...handoff, cleanup });
4081
3037
  return {
4082
- content: [{ type: "text", text: fullContent }],
4083
- details,
3038
+ suffix: [diffSummary, formatParallelHandoffReference(reference)].filter(Boolean).join("\n\n"),
3039
+ reference,
4084
3040
  };
4085
- } finally {
4086
- if (worktreeSetup && !worktreeCleanupHandled) cleanupWorktrees(worktreeSetup);
3041
+ } catch (error) {
3042
+ return { suffix: [diffSummary, formatParallelHandoffError(error)].filter(Boolean).join("\n\n") };
4087
3043
  }
4088
3044
  }
4089
3045
 
@@ -4141,109 +3097,35 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
4141
3097
  const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
4142
3098
  const maxSubagentDepth = resolveChildMaxSubagentDepth(currentMaxSubagentDepth, agentConfig.maxSubagentDepth);
4143
3099
 
4144
- if (params.clarify === true && ctx.hasUI) {
4145
- const behavior = resolveStepBehavior(agentConfig, omitUndefinedProperties({ output: effectiveOutput, skills: skillOverride }));
4146
- const availableSkills = discoverAvailableSkills(effectiveCwd);
4147
-
4148
- const result = await ctx.ui.custom<ChainClarifyResult>(
4149
- (tui, theme, _kb, done) =>
4150
- new ChainClarifyComponent(
4151
- tui, theme,
4152
- [agentConfig],
4153
- [task],
4154
- task,
4155
- undefined,
4156
- [behavior],
4157
- availableModels,
4158
- currentProvider,
4159
- availableSkills,
4160
- done,
4161
- "single",
4162
- ),
4163
- { overlay: true, overlayOptions: { anchor: "center", width: 84, maxHeight: "80%" } },
4164
- );
4165
-
4166
- if (!result || !result.confirmed) {
4167
- return { content: [{ type: "text", text: "Cancelled" }], details: { mode: "single", results: [] } };
4168
- }
4169
-
4170
- task = result.templates[0]!;
4171
- const override = result.behaviorOverrides[0];
4172
- if (override?.model !== undefined) modelOverride = resolveEffectiveSubagentModel(override.model, agentConfig.model, parentModel, availableModels, currentProvider, data.modelScope === undefined ? {} : { scope: data.modelScope });
4173
- if (override?.output !== undefined) effectiveOutput = normalizeSingleOutputOverride(override.output, agentConfig.output);
4174
- if (override?.skills !== undefined) skillOverride = override.skills;
4175
- if (override?.reads !== undefined) readsOverride = override.reads;
4176
3100
 
4177
- if (result.runInBackground) {
4178
- if (!isAsyncAvailable()) {
4179
- return {
4180
- 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." }],
4181
- isError: true,
4182
- details: { mode: "single" as const, results: [] },
4183
- };
4184
- }
4185
- const id = data.asyncRunId;
4186
- const capacity = acquireClarifiedBackgroundCapacity(data, deps, "single", id);
4187
- if (capacity.errorResult) return capacity.errorResult;
4188
- const asyncCtx = compactOptional<Parameters<typeof executeAsyncSingle>[1]["ctx"]>({
4189
- pi: deps.pi,
4190
- cwd: ctx.cwd,
4191
- currentSessionId: data.parentSessionId!,
4192
- parentSessionId: data.parentPiSessionId,
4193
- currentModelProvider: parentModel?.provider,
4194
- currentModel: parentModel,
4195
- modelScope: data.modelScope,
4196
- interactive: ctx.hasUI,
4197
- permissions: deps.config.permissions,
3101
+ const sourceCwd = effectiveCwd;
3102
+ const { setup: worktreeSetup, errorResult: worktreeSetupError } = createSingleWorktreeSetup(
3103
+ params.worktree,
3104
+ sourceCwd,
3105
+ runId,
3106
+ params.agent!,
3107
+ deps.config.worktreeSetupHook,
3108
+ deps.config.worktreeSetupHookTimeoutMs,
3109
+ deps.config.worktreeBaseDir,
3110
+ );
3111
+ if (worktreeSetupError) return worktreeSetupError;
3112
+ const singleCwd = worktreeSetup?.worktrees[0]?.agentCwd ?? sourceCwd;
3113
+ let pendingHandoff: Details["parallelHandoff"];
3114
+ if (worktreeSetup) {
3115
+ try {
3116
+ pendingHandoff = writePendingParallelHandoff({
3117
+ manifestPath: parallelHandoffPath(artifactsDir, runId),
3118
+ runId,
3119
+ mode: "single",
3120
+ source: "foreground",
3121
+ cwd: sourceCwd,
3122
+ stepIndex: 0,
3123
+ flatStartIndex: 0,
3124
+ setup: worktreeSetup,
4198
3125
  });
4199
- try {
4200
- return executeAsyncSingle(id, compactOptional<Parameters<typeof executeAsyncSingle>[1]>({
4201
- agent: params.agent!,
4202
- task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(task) : task,
4203
- goal: task,
4204
- agentConfig,
4205
- recoveryAgentConfig: data.recoveryAgents.find((agent) => agent.name === params.agent),
4206
- ctx: asyncCtx,
4207
- availableModels,
4208
- cwd: effectiveCwd,
4209
- maxOutput: params.maxOutput,
4210
- artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
4211
- artifactConfig,
4212
- shareEnabled,
4213
- activeAsyncCapacity: capacity.activeAsyncCapacity,
4214
- sessionRoot,
4215
- sessionFile: sessionFileForTask(params.agent!, 0, modelOverride),
4216
- context: contextPolicy.contextForAgent(params.agent!),
4217
- skills: skillOverride === false ? [] : skillOverride,
4218
- output: effectiveOutput,
4219
- outputMode: effectiveOutputMode,
4220
- ...(readsOverride !== undefined ? { reads: readsOverride } : {}),
4221
- outputBaseDir: resolveSingleRunOutputBaseDir(deps, artifactsDir, id),
4222
- modelOverride,
4223
- thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride),
4224
- maxSubagentDepth,
4225
- waitToolEnabled: deps.waitToolEnabled,
4226
- worktreeSetupHook: deps.config.worktreeSetupHook,
4227
- worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
4228
- worktreeBaseDir: deps.config.worktreeBaseDir,
4229
- controlConfig,
4230
- intercomBridge: params.intercomBridge,
4231
- controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
4232
- childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined,
4233
- nestedRoute: data.nestedRoute,
4234
- agentContract: params.agentContract,
4235
- structuredOutputSchema: params.outputSchema,
4236
- acceptance: params.acceptance,
4237
- timeoutMs: data.timeoutMs,
4238
- turnBudget: data.turnBudget,
4239
- toolBudget: effectiveToolBudget.toolBudget,
4240
- usageBudget: data.usageBudget,
4241
- allowZeroToolBudget: data.allowZeroToolBudget && effectiveToolBudget.toolBudget === data.toolBudget,
4242
- runFanoutBudget: data.runFanoutBudget,
4243
- }));
4244
- } finally {
4245
- finalizeClarifiedBackgroundCapacity(data, deps, capacity.activeAsyncCapacity);
4246
- }
3126
+ } catch (error) {
3127
+ cleanupWorktrees(worktreeSetup);
3128
+ return { content: [{ type: "text", text: formatParallelHandoffError(error) }], isError: true, details: { mode: "single", results: [] } };
4247
3129
  }
4248
3130
  }
4249
3131
 
@@ -4252,9 +3134,10 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
4252
3134
  task = wrapForkTask(task);
4253
3135
  }
4254
3136
  const cleanTask = task;
4255
- const outputPath = resolveSingleOutputPath(effectiveOutput, ctx.cwd, effectiveCwd, resolveSingleRunOutputBaseDir(deps, artifactsDir, runId));
3137
+ const outputPath = resolveSingleOutputPath(effectiveOutput, ctx.cwd, singleCwd, resolveSingleRunOutputBaseDir(deps, artifactsDir, runId));
4256
3138
  const validationError = validateFileOnlyOutputMode(effectiveOutputMode, outputPath, `Single run (${params.agent})`);
4257
3139
  if (validationError) {
3140
+ if (worktreeSetup) cleanupWorktrees(worktreeSetup);
4258
3141
  return { content: [{ type: "text", text: validationError }], isError: true, details: { mode: "single", results: [] } };
4259
3142
  }
4260
3143
  const structuredRuntime = params.outputSchema
@@ -4263,7 +3146,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
4263
3146
  // Reads: caller override > agent defaultReads > none. `~`/`~/` expand to home;
4264
3147
  // absolute paths pass through; relative paths resolve against the child cwd.
4265
3148
  const reads = readsOverride !== undefined ? readsOverride : agentConfig.defaultReads ?? false;
4266
- const readPaths = Array.isArray(reads) ? resolveExistingReadPaths(reads, effectiveCwd) : [];
3149
+ const readPaths = Array.isArray(reads) ? resolveExistingReadPaths(reads, singleCwd) : [];
4267
3150
  const readsInstruction = readPaths.length > 0
4268
3151
  ? `[Read from: ${readPaths.join(", ")}]\n\n`
4269
3152
  : "";
@@ -4286,7 +3169,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
4286
3169
  agent: params.agent!,
4287
3170
  authoredTask,
4288
3171
  effectivePrompt: task,
4289
- cwd: effectiveCwd,
3172
+ cwd: singleCwd,
4290
3173
  outputPath,
4291
3174
  rerun: { params: { ...params, task: authoredTask, async: params.async ?? false } },
4292
3175
  description: foregroundControl.description,
@@ -4318,7 +3201,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
4318
3201
  ...workflowForegroundSteeringLaunchOptions(foregroundControl, 0),
4319
3202
  context: data.contextPolicy.contextForAgent(params.agent!),
4320
3203
  runFanoutBudget: params.runFanoutAdmitted ? data.runFanoutBudget : { ...data.runFanoutBudget, parentPath: `${data.runFanoutBudget.parentPath ? `${data.runFanoutBudget.parentPath}/` : ""}single` },
4321
- cwd: effectiveCwd,
3204
+ cwd: singleCwd,
4322
3205
  signal,
4323
3206
  interruptSignal: interruptController.signal,
4324
3207
  allowIntercomDetach: agentConfig.systemPrompt?.includes(INTERCOM_BRIDGE_MARKER) === true,
@@ -4358,7 +3241,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
4358
3241
  onDetachedExit: (result) => {
4359
3242
  try {
4360
3243
  try {
4361
- updateRememberedForegroundChild(deps.state, { runId, mode: "single", cwd: effectiveCwd, sessionId: data.parentSessionId, index: 0, result, events: deps.pi.events, notify: params.workflowParentRunId === undefined });
3244
+ updateRememberedForegroundChild(deps.state, { runId, mode: "single", cwd: singleCwd, sessionId: data.parentSessionId, index: 0, result, events: deps.pi.events, notify: params.workflowParentRunId === undefined });
4362
3245
  } catch {
4363
3246
  // Remembered foreground state is best-effort; run history and cleanup must still complete.
4364
3247
  }
@@ -4377,12 +3260,17 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
4377
3260
  },
4378
3261
  timeoutMs: data.timeoutMs,
4379
3262
  deadlineAt,
3263
+ toolTimeoutMs: params.toolTimeoutMs,
3264
+ configToolTimeoutMs: data.configToolTimeoutMs,
4380
3265
  turnBudget: data.turnBudget,
4381
3266
  enforceHardTurnLimit: params.enforceHardTurnLimit,
4382
3267
  toolBudget: effectiveToolBudget.toolBudget,
4383
3268
  capabilityCeiling: data.capabilityCeiling,
4384
3269
  allowZeroToolBudget: data.allowZeroToolBudget && effectiveToolBudget.toolBudget === data.toolBudget,
4385
3270
  }));
3271
+ } catch (error) {
3272
+ if (worktreeSetup) cleanupWorktrees(worktreeSetup);
3273
+ throw error;
4386
3274
  } finally {
4387
3275
  // An attached runSync rejection still owns its child and structured runtime.
4388
3276
  // A successful detached receipt transfers both to onDetachedExit while the
@@ -4396,6 +3284,13 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
4396
3284
  recordRun(params.agent!, cleanTask, r.exitCode, r.progressSummary?.durationMs ?? 0);
4397
3285
  }
4398
3286
 
3287
+ let worktreeHandoff: ReturnType<typeof finalizeSingleWorktreeHandoff> | undefined;
3288
+ if (worktreeSetup) {
3289
+ worktreeHandoff = r.detached
3290
+ ? { suffix: pendingHandoff ? formatParallelHandoffReference(pendingHandoff) : "", reference: pendingHandoff }
3291
+ : finalizeSingleWorktreeHandoff({ worktreeSetup, artifactsDir, runId, cwd: sourceCwd, agent: params.agent!, result: r });
3292
+ }
3293
+
4399
3294
  if (r.progress) allProgress.push(r.progress);
4400
3295
  if (r.artifactPaths) allArtifactPaths.push(r.artifactPaths);
4401
3296
 
@@ -4430,8 +3325,9 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
4430
3325
  totalChildUsage: sumResultsUsage([r]),
4431
3326
  totalCost,
4432
3327
  usageBudget: usageBudgetState(data.usageBudget, totalCost),
3328
+ ...(worktreeHandoff?.reference ? { parallelHandoff: worktreeHandoff.reference } : {}),
4433
3329
  }));
4434
- rememberForegroundRun(deps.state, { runId, mode: "single", cwd: effectiveCwd, sessionId: data.parentSessionId, results: details.results });
3330
+ rememberForegroundRun(deps.state, { runId, mode: "single", cwd: singleCwd, sessionId: data.parentSessionId, results: details.results });
4435
3331
 
4436
3332
  const suppressRoutineResultIntercom = shouldSuppressRoutineResultIntercom({ suppressRoutineResultIntercom: params.suppressRoutineResultIntercom, results: [r] });
4437
3333
  if (!r.detached && !r.interrupted && !suppressRoutineResultIntercom) {
@@ -4454,28 +3350,36 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
4454
3350
  }
4455
3351
  }
4456
3352
 
3353
+ const worktreeSuffix = worktreeHandoff?.suffix ? `\n\n${worktreeHandoff.suffix}` : "";
4457
3354
  if (r.detached) {
3355
+ const statusRecovery = `subagent({ action: "status", id: "${runId}" }) to recover the result; do not resume or launch a replacement while it remains detached.`;
3356
+ const blockingRecovery = `subagent_wait({ id: "${runId}" }). Use ${statusRecovery}`;
3357
+ const message = r.detachedReason === "intercom coordination"
3358
+ ? `Detached for intercom coordination: ${params.agent}. Reply to the supervisor request first, then wait with ${blockingRecovery}`
3359
+ : r.detachedReason === "user request"
3360
+ ? `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}`
3361
+ : `Detached before task completion: ${params.agent}. Wait with ${blockingRecovery}`;
4458
3362
  return {
4459
- content: [{ type: "text", text: `Detached for intercom coordination: ${params.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.` }],
3363
+ content: [{ type: "text", text: `${message}${worktreeSuffix}` }],
4460
3364
  details,
4461
3365
  };
4462
3366
  }
4463
3367
 
4464
3368
  if (r.interrupted) {
4465
3369
  return {
4466
- content: [{ type: "text", text: `Run paused after interrupt (${params.agent}). Waiting for explicit next action.` }],
3370
+ content: [{ type: "text", text: `Run paused after interrupt (${params.agent}). Waiting for explicit next action.${worktreeSuffix}` }],
4467
3371
  details,
4468
3372
  };
4469
3373
  }
4470
3374
 
4471
3375
  if (r.exitCode !== 0)
4472
3376
  return {
4473
- content: [{ type: "text", text: formatFailedSingleRunOutput(r, finalizedOutput.displayOutput) }],
3377
+ content: [{ type: "text", text: `${formatFailedSingleRunOutput(r, finalizedOutput.displayOutput)}${worktreeSuffix}` }],
4474
3378
  details,
4475
3379
  isError: true,
4476
3380
  };
4477
3381
  return {
4478
- content: [{ type: "text", text: finalizedOutput.displayOutput || "(no output)" }],
3382
+ content: [{ type: "text", text: `${finalizedOutput.displayOutput || "(no output)"}${worktreeSuffix}` }],
4479
3383
  details,
4480
3384
  };
4481
3385
  }
@@ -4500,17 +3404,28 @@ function duplicateSubagentCallResult(params: SubagentParamsLike): AgentToolResul
4500
3404
 
4501
3405
  const workflowLaunchObservers = new WeakMap<object, (launch: { agent: string; sessionFile?: string; async: boolean; runId?: string }) => void>();
4502
3406
 
3407
+ /**
3408
+ * Terminal-mission retention can remove a mission while its children still
3409
+ * report. Remember it to prevent later heartbeats from warning repeatedly.
3410
+ */
3411
+ const missionsMissingFromStore = new Set<string>();
3412
+
4503
3413
  function recordMissionWorkflowChild(
4504
3414
  binding: MissionLaunchBinding | undefined,
4505
3415
  workflowRunId: string,
4506
3416
  key: string,
4507
3417
  update: Omit<MissionWorkflowChildUpdate, "workflowRunId" | "key">,
4508
3418
  ): void {
4509
- if (!binding) return;
3419
+ if (!binding || missionsMissingFromStore.has(binding.missionId)) return;
4510
3420
  const { task: _task, ...durableUpdate } = update;
4511
3421
  try {
4512
3422
  updateMission(binding.location, binding.missionId, { upsertWorkflowChildren: [{ workflowRunId, key, ...durableUpdate }] });
4513
3423
  } catch (error) {
3424
+ if (error instanceof MissionNotFoundError) {
3425
+ missionsMissingFromStore.add(binding.missionId);
3426
+ console.warn(`[pi-subagents] Mission '${binding.missionId}' is no longer in the mission store; stopped recording its workflow children. Terminal-mission retention can prune a mission while its run is still active.`);
3427
+ return;
3428
+ }
4514
3429
  console.warn(`[pi-subagents] Failed to record mission workflow child '${key}': ${error instanceof Error ? error.message : String(error)}`);
4515
3430
  }
4516
3431
  }
@@ -4580,19 +3495,110 @@ function workflowChildResult(key: string, result: AgentToolResult<Details>): Wor
4580
3495
  ...(result.details.runId || result.details.asyncId ? { runId: result.details.runId ?? result.details.asyncId } : {}),
4581
3496
  output,
4582
3497
  ...(!ok ? { error: receiptOutput || output || "Child run failed." } : {}),
3498
+ ...(detached ? { detached: true } : {}),
4583
3499
  ...(structured.length === 1 ? { structuredOutput: structured[0] } : structured.length > 1 ? { structuredOutput: structured } : {}),
4584
3500
  artifactPaths: [...artifactPaths],
4585
3501
  results: result.details.results,
4586
3502
  };
4587
3503
  }
4588
3504
 
3505
+ function workflowSteerReceipt(key: string, result: AgentToolResult<Details>): WorkflowSteerResult {
3506
+ const steering = result.details.steering;
3507
+ const error = result.content.map((part) => part.type === "text" ? part.text : "").filter(Boolean).join("\n") || undefined;
3508
+ if (!steering) return { key, state: "failed", ...(error ? { error } : {}) };
3509
+ const state = result.isError === true || steering.state === "failed" || steering.state === "partial"
3510
+ ? "failed"
3511
+ : steering.deliveryStatus === "delivered" ? "delivered" : "queued";
3512
+ return {
3513
+ key,
3514
+ state,
3515
+ requestId: steering.requestId,
3516
+ deliveryStatus: steering.deliveryStatus,
3517
+ targets: steering.targets.map((target) => ({ index: target.index, state: target.state, ...(target.reason ? { reason: target.reason } : {}) })),
3518
+ ...(state === "failed" && error ? { error } : {}),
3519
+ };
3520
+ }
3521
+
3522
+ export async function steerWorkflowChildByKey(input: {
3523
+ state: SubagentState;
3524
+ workflowRunId: string;
3525
+ key: string;
3526
+ message: string;
3527
+ options: WorkflowSteerOptions;
3528
+ signal?: AbortSignal;
3529
+ asyncDirRoot?: string;
3530
+ resolveRunId?: () => string | undefined;
3531
+ }): Promise<WorkflowSteerResult> {
3532
+ const asyncDirRoot = input.asyncDirRoot ?? DIRS.async;
3533
+ const ackTimeoutMs = input.options.ackTimeoutMs ?? 3_000;
3534
+ const deadline = Date.now() + ackTimeoutMs;
3535
+ while (true) {
3536
+ const control = [...input.state.foregroundControls.values()].find((candidate) => candidate.parentWorkflowRunId === input.workflowRunId
3537
+ && candidate.workflowKey === input.key
3538
+ && Boolean(candidate.workflowSteeringDir)
3539
+ && (candidate.activeChildren?.size ?? 0) > 0);
3540
+ if (control) {
3541
+ const result = await steerWorkflowForegroundTarget({
3542
+ target: { control, workflowRunId: input.workflowRunId, sourceRunId: control.runId },
3543
+ message: input.message,
3544
+ mode: input.options.mode,
3545
+ index: input.options.index,
3546
+ ackTimeoutMs: Math.max(1, deadline - Date.now()),
3547
+ signal: input.signal,
3548
+ });
3549
+ return workflowSteerReceipt(input.key, result);
3550
+ }
3551
+
3552
+ const workflowStatus = readStatus(path.join(asyncDirRoot, input.workflowRunId));
3553
+ const step = workflowStatus?.steps?.find((candidate) => candidate.workflowKey === input.key);
3554
+ const childRunId = step?.runId ?? input.resolveRunId?.();
3555
+ if (childRunId) {
3556
+ const asyncDir = path.join(asyncDirRoot, childRunId);
3557
+ const childStatus = reconcileAsyncRun(asyncDir).status;
3558
+ if (childStatus && childStatus.state !== "running" && childStatus.state !== "queued" && (input.options.mode !== "follow_up" || !canQueueRetainedAsyncFollowUp(childStatus, input.options.index))) {
3559
+ return { key: input.key, state: "missed", error: `Workflow child '${input.key}' is ${childStatus.state}.` };
3560
+ }
3561
+ if (childStatus) {
3562
+ const result = await steerAsyncRun({
3563
+ state: input.state,
3564
+ runId: childRunId,
3565
+ message: input.message,
3566
+ mode: input.options.mode,
3567
+ index: input.options.index,
3568
+ ackTimeoutMs: Math.max(1, deadline - Date.now()),
3569
+ location: { asyncDir },
3570
+ signal: input.signal,
3571
+ });
3572
+ return workflowSteerReceipt(input.key, result);
3573
+ }
3574
+ }
3575
+ if (step && step.status !== "running" && step.status !== "pending") {
3576
+ return { key: input.key, state: "missed", error: `Workflow child '${input.key}' is ${step.status}.` };
3577
+ }
3578
+ if (workflowStatus && workflowStatus.state !== "running" && workflowStatus.state !== "queued") {
3579
+ return { key: input.key, state: "missed", error: `Workflow '${input.workflowRunId}' is ${workflowStatus.state}.` };
3580
+ }
3581
+ if (input.signal?.aborted || Date.now() >= deadline) {
3582
+ return { key: input.key, state: "missed", error: `Workflow child '${input.key}' had no live steering route.` };
3583
+ }
3584
+ await new Promise<void>((resolve) => setTimeout(resolve, Math.min(10, Math.max(1, deadline - Date.now()))));
3585
+ }
3586
+ }
3587
+
4589
3588
  export function prepareWorkflowLaunchParams(
4590
3589
  workflowDefaults: SubagentParamsLike,
4591
3590
  childParams: Record<string, unknown>,
4592
3591
  parentWorkflowRunId: string,
4593
3592
  workflowKey: string,
4594
- options: { missionDetached?: boolean; suppressRoutineResultIntercom?: boolean; runFanoutBudget?: RunFanoutBudgetDescriptor } = {},
3593
+ options: { missionDetached?: boolean; suppressRoutineResultIntercom?: boolean; runFanoutBudget?: RunFanoutBudgetDescriptor; parentDeadlineAt?: number } = {},
4595
3594
  ): SubagentParamsLike {
3595
+ const parentTimeoutMs = options.parentDeadlineAt === undefined
3596
+ || childParams.timeoutMs !== undefined
3597
+ || childParams.maxRuntimeMs !== undefined
3598
+ || workflowDefaults.timeoutMs !== undefined
3599
+ || workflowDefaults.maxRuntimeMs !== undefined
3600
+ ? undefined
3601
+ : Math.max(1, options.parentDeadlineAt - Date.now());
4596
3602
  if (typeof childParams.resume === "string") {
4597
3603
  if (childParams.gate !== undefined || workflowDefaults.gate !== undefined) {
4598
3604
  throw new Error("gate is not supported with retained resume; resume uses the retained child contract.");
@@ -4601,12 +3607,14 @@ export function prepareWorkflowLaunchParams(
4601
3607
  const turnBudget = childParams.turnBudget ?? workflowDefaults.turnBudget;
4602
3608
  const toolBudget = childParams.toolBudget ?? workflowDefaults.toolBudget;
4603
3609
  const intercomBridge = childParams.intercomBridge ?? workflowDefaults.intercomBridge;
3610
+ const worktree = childParams.worktree ?? workflowDefaults.worktree;
4604
3611
  return {
4605
3612
  action: "resume",
4606
3613
  id: childParams.resume.trim(),
4607
3614
  message: typeof childParams.task === "string" ? childParams.task.trim() : "",
4608
3615
  workflowParentRunId: parentWorkflowRunId,
4609
3616
  workflowKey,
3617
+ ...(worktree !== undefined ? { worktree: worktree as boolean } : {}),
4610
3618
  ...(options.runFanoutBudget ? { runFanoutBudget: { ...options.runFanoutBudget, parentPath: `${options.runFanoutBudget.parentPath ? `${options.runFanoutBudget.parentPath}/` : ""}workflow[${workflowKey}]` } } : {}),
4611
3619
  ...(options.missionDetached ? { mission: false } : {}),
4612
3620
  ...(timeoutMs !== undefined ? { timeoutMs: timeoutMs as number } : {}),
@@ -4622,12 +3630,13 @@ export function prepareWorkflowLaunchParams(
4622
3630
  ...(options.missionDetached ? { mission: false } : {}),
4623
3631
  workflowParentRunId: parentWorkflowRunId,
4624
3632
  workflowKey,
3633
+ ...(parentTimeoutMs !== undefined ? { workflowParentDeadlineAt: options.parentDeadlineAt } : {}),
4625
3634
  ...(options.runFanoutBudget ? { runFanoutBudget: { ...options.runFanoutBudget, parentPath: `${options.runFanoutBudget.parentPath ? `${options.runFanoutBudget.parentPath}/` : ""}workflow[${workflowKey}]` } } : {}),
4626
3635
  ...(options.suppressRoutineResultIntercom ? { suppressRoutineResultIntercom: true } : {}),
4627
3636
  } as SubagentParamsLike;
4628
3637
  const normalizedGate = normalizeGateParams(launchParams);
4629
3638
  if (!normalizedGate.ok) throw new Error(normalizedGate.error);
4630
- return prepareWorkflowChildParams(normalizedGate.params);
3639
+ return normalizedGate.params;
4631
3640
  }
4632
3641
 
4633
3642
  type GateParamsNormalizationResult =
@@ -4645,41 +3654,6 @@ function normalizeGateParams(params: SubagentParamsLike): GateParamsNormalizatio
4645
3654
  return { ok: true, params: { ...rest, ...(normalized.acceptance !== undefined ? { acceptance: normalized.acceptance } : {}) } };
4646
3655
  }
4647
3656
 
4648
- function prepareWorkflowChildParams(params: SubagentParamsLike): SubagentParamsLike {
4649
- if (params.worktree !== true || !params.agent) return params;
4650
- const {
4651
- agent,
4652
- task = "",
4653
- model,
4654
- skill,
4655
- output,
4656
- outputMode,
4657
- outputSchema,
4658
- acceptance,
4659
- agentContract,
4660
- toolBudget,
4661
- reads,
4662
- ...runParams
4663
- } = params;
4664
- return {
4665
- ...runParams,
4666
- worktree: true,
4667
- tasks: [{
4668
- agent,
4669
- task,
4670
- ...(model !== undefined ? { model } : {}),
4671
- ...(skill !== undefined ? { skill } : {}),
4672
- ...(output !== undefined ? { output } : {}),
4673
- ...(reads !== undefined ? { reads } : {}),
4674
- ...(outputMode !== undefined ? { outputMode } : {}),
4675
- ...(outputSchema !== undefined ? { outputSchema } : {}),
4676
- ...(acceptance !== undefined ? { acceptance } : {}),
4677
- ...(agentContract !== undefined ? { agentContract } : {}),
4678
- ...(toolBudget !== undefined ? { toolBudget } : {}),
4679
- }],
4680
- };
4681
- }
4682
-
4683
3657
  function formatWorkflowValue(value: unknown): string {
4684
3658
  if (value === undefined) return "(undefined)";
4685
3659
  if (typeof value === "string") return value;
@@ -4810,7 +3784,6 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4810
3784
  const warning = getProjectArtifactPackagingWarning(workflowCwd);
4811
3785
  if (warning) console.warn(`[pi-subagents] ${warning}`);
4812
3786
  }
4813
- const workflowPrompts = { render: (ref: string, vars?: unknown) => renderWorkflowPrompt(ref, vars, workflowCwd) };
4814
3787
  const chatProgressResult = resolveWorkflowChatProgress({ requested: requestParams.chatProgress, parentCwd, workflowCwd, background: requestParams.async !== false });
4815
3788
  if (chatProgressResult.error) return { content: [{ type: "text", text: chatProgressResult.error }], isError: true, details: { mode: "workflow", results: [] } };
4816
3789
  const chatProgress = chatProgressResult.projection!;
@@ -4887,12 +3860,17 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4887
3860
  };
4888
3861
  if (workflowRunId) {
4889
3862
  const toolCallId = _id;
3863
+ const workflowSessionRoot = requestParams.sessionDir
3864
+ ? path.resolve(deps.expandTilde(requestParams.sessionDir))
3865
+ : trustedSessionRootsForStatus(ctx, deps)[0];
4890
3866
  const asyncDir = path.join(DIRS.async, workflowRunId);
4891
- const resultPath = path.join(DIRS.results, `${workflowRunId}.json`);
3867
+ const resultPath = resultFilePath(DIRS.results, workflowRunId);
4892
3868
  const statusPath = path.join(asyncDir, "status.json");
4893
3869
  const eventsPath = path.join(asyncDir, "events.jsonl");
4894
3870
  const startedAt = Date.now();
4895
3871
  const currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
3872
+ const completionOwnerId = deps.state.completionOwnerId ?? currentCompletionOwnerId();
3873
+ deps.state.completionOwnerId = completionOwnerId;
4896
3874
  try {
4897
3875
  fs.mkdirSync(asyncDir, { recursive: true });
4898
3876
  writeRunFanoutBudgetDescriptor(asyncDir, workflowFanoutBudget);
@@ -4910,25 +3888,57 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4910
3888
  runId: workflowRunId,
4911
3889
  toolCallId,
4912
3890
  sessionId: currentSessionId ?? undefined,
3891
+ completionOwnerId,
4913
3892
  mode: "workflow",
4914
3893
  state: "running",
4915
3894
  startedAt,
4916
3895
  lastUpdate: startedAt,
4917
3896
  ...(timeout !== undefined ? { deadlineAt: startedAt + timeout, timeoutMs: timeout } : {}),
4918
3897
  cwd: workflowCwd,
3898
+ ...(workflowSessionRoot ? { sessionRoot: workflowSessionRoot } : {}),
4919
3899
  pid: process.pid,
4920
3900
  steps: [],
4921
3901
  workflow: { trace: [], emits: [], console: [] },
4922
3902
  runFanoutBudget: getRunFanoutBudgetSnapshot(workflowFanoutBudget),
4923
3903
  };
4924
- const appendWorkflowEvent = (event: Record<string, unknown>) => fs.appendFileSync(eventsPath, `${JSON.stringify({ ts: Date.now(), runId: workflowRunId, ...event })}\n`, "utf-8");
3904
+ const appendWorkflowEvent = (event: Record<string, unknown>) => {
3905
+ try {
3906
+ fs.appendFileSync(eventsPath, `${JSON.stringify({ ts: Date.now(), runId: workflowRunId, ...event })}\n`, "utf-8");
3907
+ } catch (error) {
3908
+ if (!isStorageCapacityError(error)) throw error;
3909
+ console.error(`Failed to append async workflow event while storage is full:`, error);
3910
+ }
3911
+ };
4925
3912
  let indexedState: AsyncStatus["state"] | undefined;
3913
+ const indexPersistence = createCapacityResilientJsonWriter({
3914
+ keepAlive: true,
3915
+ onSuccess: (_filePath, payload) => { indexedState = (payload as { state: AsyncStatus["state"] }).state; },
3916
+ onError: (error, filePath) => console.error(`Failed to update async workflow index '${filePath}':`, error),
3917
+ });
3918
+ const queueActiveRunIndex = (): void => {
3919
+ const state = status.state;
3920
+ if (indexedState === state && indexPersistence.pendingCount() === 0) return;
3921
+ indexPersistence.write(asyncDir, { state, toolCallId: status.toolCallId }, (_filePath, payload) => {
3922
+ const indexPayload = payload as { state: AsyncStatus["state"]; toolCallId?: string };
3923
+ updateActiveRunIndex(asyncDir, indexPayload.state, indexPayload.toolCallId, { retryCapacityErrors: true });
3924
+ });
3925
+ };
3926
+ const runPersistence = createCapacityResilientJsonWriter({
3927
+ keepAlive: true,
3928
+ onSuccess: (filePath) => { if (filePath === statusPath) queueActiveRunIndex(); },
3929
+ onError: (error, filePath) => console.error(`Failed to persist async workflow state '${filePath}':`, error),
3930
+ write: (filePath, payload) => filePath === resultPath
3931
+ ? writeAsyncResultFile(filePath, payload as Record<string, unknown>)
3932
+ : writeAtomicJson(filePath, payload),
3933
+ });
3934
+ let initialPersistenceComplete = false;
4926
3935
  const persist = () => {
4927
3936
  status.lastUpdate = Date.now();
4928
- writeAtomicJson(statusPath, status);
4929
- if (indexedState !== status.state) {
4930
- updateActiveRunIndex(asyncDir, status.state);
4931
- indexedState = status.state;
3937
+ if (initialPersistenceComplete) runPersistence.write(statusPath, status);
3938
+ else {
3939
+ writeAtomicJson(statusPath, status);
3940
+ initialPersistenceComplete = true;
3941
+ queueActiveRunIndex();
4932
3942
  }
4933
3943
  const job = deps.state.asyncJobs.get(workflowRunId);
4934
3944
  if (job) {
@@ -4952,6 +3962,17 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4952
3962
  job.workflow = status.workflow;
4953
3963
  }
4954
3964
  };
3965
+ const writeWorkflowResult = (payload: Record<string, unknown>): boolean => {
3966
+ try {
3967
+ runPersistence.write(resultPath, payload);
3968
+ return true;
3969
+ } catch (error) {
3970
+ const message = `Failed to write async workflow result ${resultPath}: ${error instanceof Error ? error.message : String(error)}`;
3971
+ console.error(message, error);
3972
+ appendWorkflowEvent({ type: "subagent.workflow.result_write_failed", error: message });
3973
+ return false;
3974
+ }
3975
+ };
4955
3976
  const projectWorkflowActivity = () => {
4956
3977
  const steps = status.steps ?? [];
4957
3978
  const runningSteps = steps.filter((step) => step.status === "running");
@@ -4973,20 +3994,32 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4973
3994
  status.toolCount = toolCounts.length > 0 ? toolCounts.reduce((total, count) => total + count, 0) : undefined;
4974
3995
  status.currentStep = runningSteps.length === 1 ? steps.indexOf(runningSteps[0]!) : undefined;
4975
3996
  };
4976
- const workflowJob: AsyncJobState = { asyncId: workflowRunId, asyncDir, cwd: workflowCwd, status: "running", sessionId: currentSessionId ?? undefined, mode: "workflow", agents: [], steps: [], startedAt, updatedAt: startedAt, ...(timeout !== undefined ? { timeoutMs: timeout, deadlineAt: startedAt + timeout } : {}), workflow: status.workflow };
3997
+ const workflowJob: AsyncJobState = { asyncId: workflowRunId, asyncDir, toolCallId, cwd: workflowCwd, ...(workflowSessionRoot ? { sessionRoot: workflowSessionRoot } : {}), status: "running", sessionId: currentSessionId ?? undefined, mode: "workflow", agents: [], steps: [], startedAt, updatedAt: startedAt, ...(timeout !== undefined ? { timeoutMs: timeout, deadlineAt: startedAt + timeout } : {}), workflow: status.workflow };
4977
3998
  deps.state.asyncJobs.set(workflowRunId, workflowJob);
4978
3999
  deps.state.fleetJobs ??= new Map();
4979
4000
  deps.state.fleetJobs.set(workflowRunId, workflowJob);
4980
- persist();
4001
+ try {
4002
+ persist();
4003
+ } catch (error) {
4004
+ deps.state.workflowControllers?.delete(workflowRunId);
4005
+ deps.state.asyncJobs.delete(workflowRunId);
4006
+ deps.state.fleetJobs?.delete(workflowRunId);
4007
+ workflowCapacity?.rollback();
4008
+ indexPersistence.dispose();
4009
+ return { content: [{ type: "text", text: `Failed to create async workflow storage: ${error instanceof Error ? error.message : String(error)}` }], isError: true, details: { mode: "workflow", results: [] } };
4010
+ }
4981
4011
  appendWorkflowEvent({ type: "subagent.workflow.started" });
4982
4012
  const { workflowScript, async: _workflowAsync, chatProgress: _chatProgress, ...workflowRequest } = requestParams;
4983
4013
  void Promise.resolve().then(async () => {
4014
+ const workflowDeadlineAt = timeout === undefined ? undefined : Date.now() + timeout;
4984
4015
  const workflowResults: SingleResult[] = [];
4016
+ const workflowChildRunIds = new Map<string, string>();
4985
4017
  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;
4986
4018
  const workflowOutput = typeof workflowChildDefaults.output === "string" || typeof workflowChildDefaults.output === "boolean" ? workflowChildDefaults.output : undefined;
4987
4019
  const configuredOutputBaseDir = resolveConfiguredSingleRunOutputBaseDir(deps);
4988
4020
  const workflowAggregateOutputPath = resolveWorkflowAggregateOutputPath(workflowOutput, ctx.cwd, workflowCwd, resolveSingleRunOutputBaseDir(deps, workflowArtifactsDir, workflowRunId), configuredOutputBaseDir);
4989
4021
  const claimedOutputPaths = new Map<string, string>();
4022
+ const producedChildOutputPaths = new Set<string>();
4990
4023
  const workflowSteps = new Map<string, NonNullable<AsyncStatus["steps"]>[number]>();
4991
4024
  let projectedTraceLength = 0;
4992
4025
  let projectedTraceTail: NonNullable<Details["workflow"]>["trace"][number] | undefined;
@@ -5012,12 +4045,16 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5012
4045
  ? "completed"
5013
4046
  : entry.state === "stopped"
5014
4047
  ? "stopped"
5015
- : "failed";
4048
+ : entry.state === "detached"
4049
+ ? "paused"
4050
+ : "failed";
5016
4051
  if (existing) {
5017
4052
  existing.status = mapped;
5018
4053
  if (entry.agent) existing.agent = entry.agent;
5019
4054
  if (entry.runId) existing.runId = entry.runId;
5020
4055
  if (entry.state === "failed" && !entry.runId && existing.async === undefined) existing.async = false;
4056
+ if (entry.state === "detached") existing.activityState = "needs_attention";
4057
+ else if (existing.status !== "running") delete existing.activityState;
5021
4058
  if (entry.state === "stopped") existing.stopped = true;
5022
4059
  else delete existing.stopped;
5023
4060
  if (entry.error === undefined) delete existing.error;
@@ -5034,6 +4071,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5034
4071
  startedAt: Date.now(),
5035
4072
  ...(entry.runId ? { runId: entry.runId } : {}),
5036
4073
  ...(entry.state === "failed" && !entry.runId ? { async: false } : {}),
4074
+ ...(entry.state === "detached" ? { activityState: "needs_attention" as const } : {}),
5037
4075
  ...(entry.state === "stopped" ? { stopped: true } : {}),
5038
4076
  };
5039
4077
  status.steps?.push(step);
@@ -5051,7 +4089,6 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5051
4089
  script: workflowScript,
5052
4090
  timeoutMs: timeout,
5053
4091
  signal: controller.signal,
5054
- prompts: workflowPrompts,
5055
4092
  ...(workflowState ? { state: workflowState } : {}),
5056
4093
  onTrace: updateTrace,
5057
4094
  admit: (calls) => {
@@ -5082,7 +4119,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5082
4119
  });
5083
4120
  const result = await runMissionWorkflowChild(missionBinding, workflowRunId, key, childPhase, () => {
5084
4121
  const childRequest = bindMissionWorkflowChildAsyncLaunch(
5085
- { ...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 },
4122
+ { ...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, parentDeadlineAt: workflowDeadlineAt } }), runFanoutAdmitted: admission.admitted },
5086
4123
  missionBinding,
5087
4124
  deps.asyncByDefault,
5088
4125
  );
@@ -5124,7 +4161,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5124
4161
  }, ctx, preserveActiveSession);
5125
4162
  });
5126
4163
  workflowResults.push(...result.details.results);
4164
+ for (const childResult of result.details.results) {
4165
+ if (childResult.savedOutputPath) producedChildOutputPaths.add(childResult.savedOutputPath);
4166
+ }
5127
4167
  const child = workflowChildResult(key, result);
4168
+ if (child.runId) workflowChildRunIds.set(key, child.runId);
5128
4169
  const step = status.steps?.find((candidate) => candidate.workflowKey === key);
5129
4170
  if (step) {
5130
4171
  step.async = Boolean(result.details.asyncId || result.details.asyncDir);
@@ -5148,26 +4189,37 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5148
4189
  return child;
5149
4190
  },
5150
4191
  status: async (keyOrRunId, workflowSignal) => workflowChildResult(keyOrRunId, await execute(randomUUID(), { action: "status", id: keyOrRunId }, workflowSignal, undefined, ctx, preserveActiveSession)),
4192
+ steer: (key, message, options, workflowSignal) => steerWorkflowChildByKey({ state: deps.state, workflowRunId, key, message, options, signal: workflowSignal, resolveRunId: () => workflowChildRunIds.get(key) }),
5151
4193
  });
5152
4194
  const returnPreview = formatWorkflowValue(workflow.value).slice(0, 1_000);
5153
4195
  const emitPreview = workflow.emits.length > 0 ? ` Emitted: ${workflow.emits.map(formatWorkflowValue).join(", ").slice(0, 1_000)}` : "";
5154
4196
  const summary = `Workflow completed with ${workflow.children.length} child run(s). Return: ${returnPreview}${emitPreview} Trace: ${workflow.trace.length} event(s).`;
5155
- const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, summary);
4197
+ const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, summary, producedChildOutputPaths);
5156
4198
  const resultSummary = appendWorkflowOutputWarning(summary, outputWarning);
5157
4199
  const workflowUsage = sumResultsUsage(workflowResults);
5158
4200
  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) };
5159
- writeAtomicJson(resultPath, { 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 });
4201
+ 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, timestamp: Date.now(), durationMs: Date.now() - startedAt })) return;
5160
4202
  persist();
5161
- appendWorkflowEvent({ type: "subagent.workflow.completed", state: "complete" });
4203
+ appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, ...(status.error ? { error: status.error } : {}) });
5162
4204
  } catch (error) {
5163
4205
  const partial = error instanceof WorkflowScriptError ? error.partial : { trace: [], emits: [], console: [], children: [] };
5164
4206
  const stopped = controller.signal.aborted;
5165
- status = compactOptional<AsyncStatus>({ ...status, state: stopped ? "stopped" : "failed", stopped: stopped || undefined, error: error instanceof Error ? error.message : String(error), endedAt: Date.now(), workflow: { trace: partial.trace, emits: partial.emits, console: partial.console } });
5166
- const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, status.error ?? "Workflow failed.");
5167
- const resultSummary = appendWorkflowOutputWarning(status.error ?? "Workflow failed.", outputWarning);
5168
- writeAtomicJson(resultPath, { id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: false, state: status.state, summary: resultSummary, error: status.error, stopped: status.stopped, 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.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: workflowCwd, sessionId: currentSessionId, timestamp: Date.now(), durationMs: Date.now() - startedAt });
4207
+ const detachedChildKeys = new Set(partial.children.filter((child) => child.detached).map((child) => child.key));
4208
+ const hasRealFailedChild = partial.children.some((child) => !child.ok && !child.detached);
4209
+ const pauseForDetached = !stopped && error instanceof WorkflowScriptError && error.errorKind === "detached-child" && detachedChildKeys.size > 0 && !hasRealFailedChild;
4210
+ const state = stopped ? "stopped" : pauseForDetached ? "paused" : "failed";
4211
+ for (const step of status.steps ?? []) {
4212
+ if (step.workflowKey && detachedChildKeys.has(step.workflowKey)) {
4213
+ step.status = "paused";
4214
+ step.activityState = "needs_attention";
4215
+ }
4216
+ }
4217
+ 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 } });
4218
+ const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, status.error ?? (pauseForDetached ? "Workflow paused." : "Workflow failed."), producedChildOutputPaths);
4219
+ const resultSummary = appendWorkflowOutputWarning(status.error ?? (pauseForDetached ? "Workflow paused." : "Workflow failed."), outputWarning);
4220
+ 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, completionOwnerId, timestamp: Date.now(), durationMs: Date.now() - startedAt })) return;
5169
4221
  persist();
5170
- appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, error: status.error });
4222
+ appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, error: status.error, ...(status.activityState ? { activityState: status.activityState } : {}) });
5171
4223
  } finally {
5172
4224
  deps.state.workflowControllers?.delete(workflowRunId);
5173
4225
  deps.state.activeAsyncCapacity = workflowCapacity?.reconcile(new Set(deps.state.workflowControllers?.keys() ?? []))
@@ -5184,8 +4236,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5184
4236
  const configuredOutputBaseDir = resolveConfiguredSingleRunOutputBaseDir(deps);
5185
4237
  const workflowAggregateOutputPath = resolveWorkflowAggregateOutputPath(workflowOutput, ctx.cwd, workflowCwd, resolveSingleRunOutputBaseDir(deps, workflowArtifactsDir, _id), configuredOutputBaseDir);
5186
4238
  const claimedOutputPaths = new Map<string, string>();
4239
+ const producedChildOutputPaths = new Set<string>();
5187
4240
  const workflowResults: SingleResult[] = [];
4241
+ const workflowChildRunIds = new Map<string, string>();
5188
4242
  let liveWorkflow: NonNullable<Details["workflow"]> = { trace: [], emits: [], console: [] };
4243
+ const workflowDeadlineAt = timeout === undefined ? undefined : Date.now() + timeout;
5189
4244
  const sendWorkflowProgress = () => {
5190
4245
  const update = workflowChatProgressUpdate(_id, chatProgress, liveWorkflow);
5191
4246
  if (update) onUpdate?.(update);
@@ -5195,7 +4250,6 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5195
4250
  script: requestParams.workflowScript,
5196
4251
  timeoutMs: timeout,
5197
4252
  signal,
5198
- prompts: workflowPrompts,
5199
4253
  ...(workflowState ? { state: workflowState } : {}),
5200
4254
  onTrace: (trace) => {
5201
4255
  liveWorkflow = { ...liveWorkflow, trace };
@@ -5226,7 +4280,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5226
4280
  });
5227
4281
  const result = await runMissionWorkflowChild(missionBinding, _id, key, childPhase, () => {
5228
4282
  const childRequest = bindMissionWorkflowChildAsyncLaunch(
5229
- { ...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 },
4283
+ { ...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, parentDeadlineAt: workflowDeadlineAt } }), runFanoutAdmitted: admission.admitted },
5230
4284
  missionBinding,
5231
4285
  deps.asyncByDefault,
5232
4286
  );
@@ -5246,8 +4300,12 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5246
4300
  }, ctx, preserveActiveSession);
5247
4301
  });
5248
4302
  workflowResults.push(...result.details.results);
4303
+ for (const childResult of result.details.results) {
4304
+ if (childResult.savedOutputPath) producedChildOutputPaths.add(childResult.savedOutputPath);
4305
+ }
5249
4306
  if (result.details.asyncDir && missionBinding) writeMissionAsyncBinding(result.details.asyncDir, missionBinding);
5250
4307
  const child = workflowChildResult(key, result);
4308
+ if (child.runId) workflowChildRunIds.set(key, child.runId);
5251
4309
  const childStatus = missionWorkflowChildStatus(result);
5252
4310
  recordMissionWorkflowChild(missionBinding, _id, key, {
5253
4311
  status: childStatus,
@@ -5261,6 +4319,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5261
4319
  return child;
5262
4320
  },
5263
4321
  status: async (keyOrRunId, workflowSignal) => workflowChildResult(keyOrRunId, await execute(randomUUID(), { action: "status", id: keyOrRunId }, workflowSignal, undefined, ctx, preserveActiveSession)),
4322
+ steer: (key, message, options, workflowSignal) => steerWorkflowChildByKey({ state: deps.state, workflowRunId: _id, key, message, options, signal: workflowSignal, resolveRunId: () => workflowChildRunIds.get(key) }),
5264
4323
  });
5265
4324
  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}` : ""}`);
5266
4325
  const sections = ["Workflow completed.", `Return:\n${formatWorkflowValue(workflow.value)}`];
@@ -5268,7 +4327,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5268
4327
  if (workflow.console.length > 0) sections.push(`Console:\n${workflow.console.map((entry) => `[${entry.level}] ${entry.text}`).join("\n")}`);
5269
4328
  if (traceLines.length > 0) sections.push(`Call trace:\n${traceLines.join("\n")}`);
5270
4329
  const workflowText = sections.join("\n\n");
5271
- const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, workflowText);
4330
+ const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, workflowText, producedChildOutputPaths);
5272
4331
  const displayText = appendWorkflowOutputWarning(workflowText, outputWarning);
5273
4332
  return attachWorkflowMission(withRunFanoutBudget({
5274
4333
  content: [{ type: "text", text: displayText }],
@@ -5283,7 +4342,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5283
4342
  if (partial.console.length > 0) sections.push(`Console:\n${partial.console.map((entry) => `[${entry.level}] ${entry.text}`).join("\n")}`);
5284
4343
  if (traceLines.length > 0) sections.push(`Call trace:\n${traceLines.join("\n")}`);
5285
4344
  const workflowText = sections.join("\n\n");
5286
- const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, workflowText);
4345
+ const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, workflowText, producedChildOutputPaths);
5287
4346
  const displayText = appendWorkflowOutputWarning(workflowText, outputWarning);
5288
4347
  return attachWorkflowMission(withRunFanoutBudget({
5289
4348
  content: [{ type: "text", text: displayText }],
@@ -5292,7 +4351,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5292
4351
  }, workflowFanoutBudget));
5293
4352
  }
5294
4353
  }
5295
- const directParams = prepareWorkflowChildParams(requestParams);
4354
+ const directParams = requestParams;
5296
4355
  const requestCwd = resolveRequestedCwd(ctx.cwd, directParams.cwd);
5297
4356
  const paramsWithResolvedCwd = directParams.cwd === undefined ? directParams : { ...directParams, cwd: requestCwd };
5298
4357
  const action = paramsWithResolvedCwd.action;
@@ -5609,61 +4668,6 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5609
4668
  }
5610
4669
  return withBudget(inspectSubagentStatus(paramsWithResolvedCwd, omitUndefinedProperties({ state: deps.state, nested: nestedScope, sessionRoots })));
5611
4670
  }
5612
-
5613
- if (action === "approve-checkpoint" || action === "reject-checkpoint") {
5614
- deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
5615
- const targetRunId = paramsWithResolvedCwd.runId ?? paramsWithResolvedCwd.id;
5616
- if (!targetRunId && !paramsWithResolvedCwd.dir) {
5617
- return { content: [{ type: "text", text: `action='${action}' requires id or dir.` }], isError: true, details: { mode: "management", results: [] } };
5618
- }
5619
- try {
5620
- const resolved = targetRunId ? resolveSubagentRunId(targetRunId, omitUndefinedProperties({ state: deps.state, nested: nestedResolutionScopeForExecutor(deps) })) : undefined;
5621
- const decision = action === "approve-checkpoint" ? "approved" : "rejected";
5622
- if (resolved?.kind === "foreground") {
5623
- const run = deps.state.foregroundRuns?.get(resolved.id);
5624
- if (!run?.checkpoint || run.checkpoint.status !== "pending") {
5625
- return { content: [{ type: "text", text: `Run '${resolved.id}' is not paused at an approval checkpoint.` }], isError: true, details: { mode: "management", results: [] } };
5626
- }
5627
- if (deps.state.currentSessionId && run.sessionId !== deps.state.currentSessionId) {
5628
- return { content: [{ type: "text", text: `Run '${resolved.id}' was not found in the active session.` }], isError: true, details: { mode: "management", results: [] } };
5629
- }
5630
- const decidedAt = Date.now();
5631
- const checkpoint: NonNullable<Details["checkpoint"]> = decision === "approved"
5632
- ? { ...run.checkpoint, status: "approved", approvedAt: decidedAt }
5633
- : { ...run.checkpoint, status: "rejected", rejectedAt: decidedAt };
5634
- run.checkpoint = checkpoint;
5635
- run.updatedAt = decidedAt;
5636
- return {
5637
- content: [{ type: "text", text: `Checkpoint '${checkpoint.name}' ${decision} for foreground run ${resolved.id}.` }],
5638
- details: { mode: "management", results: [], checkpoint },
5639
- };
5640
- }
5641
- const location = paramsWithResolvedCwd.dir
5642
- ? resolveAsyncRunLocation(paramsWithResolvedCwd, DIRS.async, DIRS.results)
5643
- : resolved?.kind === "async"
5644
- ? resolved.location
5645
- : undefined;
5646
- if (!location?.asyncDir) {
5647
- return { content: [{ type: "text", text: `action='${action}' targets paused foreground or async checkpoints. No matching run found.` }], isError: true, details: { mode: "management", results: [] } };
5648
- }
5649
- const status = readStatus(location.asyncDir);
5650
- const runId = status?.runId ?? location.resolvedId ?? targetRunId ?? path.basename(location.asyncDir);
5651
- if (!status?.checkpoint || status.state !== "paused") {
5652
- return { content: [{ type: "text", text: `Run '${runId}' is not paused at an approval checkpoint.` }], isError: true, details: { mode: "management", results: [] } };
5653
- }
5654
- if (deps.state.currentSessionId && status.sessionId !== deps.state.currentSessionId) {
5655
- return { content: [{ type: "text", text: `Run '${runId}' was not found in the active session.` }], isError: true, details: { mode: "management", results: [] } };
5656
- }
5657
- deliverCheckpointDecisionRequest({ asyncDir: location.asyncDir, decision, source: "subagent-action", ...(paramsWithResolvedCwd.message ? { reason: paramsWithResolvedCwd.message } : {}) });
5658
- return {
5659
- content: [{ type: "text", text: `Checkpoint '${status.checkpoint.name}' ${decision} for run ${runId}.` }],
5660
- details: { mode: "management", results: [], checkpoint: { ...status.checkpoint, status: decision } },
5661
- };
5662
- } catch (error) {
5663
- const text = error instanceof Error ? error.message : String(error);
5664
- return { content: [{ type: "text", text }], isError: true, details: { mode: "management", results: [] } };
5665
- }
5666
- }
5667
4671
  if (action === "resume") {
5668
4672
  return resumeAsyncRun(omitUndefinedProperties({ params: paramsWithResolvedCwd, requestCwd, ctx, deps, parentModel: requestParentModel, signal }));
5669
4673
  }
@@ -5952,14 +4956,23 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5952
4956
  const parentSessionFile = ctx.sessionManager.getSessionFile() ?? null;
5953
4957
  const discovered = deps.discoverAgents(effectiveCwd, scope);
5954
4958
  const discoveredAgents = discovered.agents;
5955
- const canonicalParams = canonicalizeExecutionParams(effectiveParams, discoveredAgents);
4959
+ const canonicalParams = canonicalizeExecutionParams(effectiveParams, discoveredAgents, discovered.agentDiagnostics);
5956
4960
  if (canonicalParams.error) return buildRequestedModeError(effectiveParams, canonicalParams.error);
5957
4961
  effectiveParams = canonicalParams.params!;
5958
4962
  const modelScope = discovered.modelScope;
5959
4963
  effectiveParams = applySingleAgentLaunchDefaults(effectiveParams, discoveredAgents);
5960
4964
  const turnBudget = resolveTurnBudgetConfig(effectiveParams.turnBudget ?? deps.config.turnBudget);
5961
4965
  if (turnBudget.error) return buildRequestedModeError(effectiveParams, turnBudget.error);
5962
- const contextPolicy = resolveAgentDefaultContextPolicy(effectiveParams, discoveredAgents);
4966
+ // An agent-level defaultContext is a preference, unlike an explicit request.
4967
+ // Prefer fork only when the parent session is persisted and has a current leaf;
4968
+ // otherwise use fresh immediately instead of launching a guaranteed-to-fail fork.
4969
+ // Explicit context:"fork" remains strict.
4970
+ const contextPolicy = resolveAgentDefaultContextPolicy(
4971
+ effectiveParams,
4972
+ discoveredAgents,
4973
+ deps.config.defaultSubagentContext,
4974
+ canPreferFork(ctx.sessionManager),
4975
+ );
5963
4976
  effectiveParams = contextPolicy.params;
5964
4977
  const sessionName = resolveIntercomSessionTarget(deps.pi.getSessionName(), ctx.sessionManager.getSessionId());
5965
4978
  const intercomBridge = resolveIntercomBridge({
@@ -5971,7 +4984,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5971
4984
  const agents = intercomBridge.active
5972
4985
  ? discoveredAgents.map((agent) => applyIntercomBridgeToAgent(agent, intercomBridge))
5973
4986
  : discoveredAgents;
5974
- const runId = randomUUID().slice(0, 8);
4987
+ const runId = randomUUID();
5975
4988
  const inheritedNestedRoute = resolveInheritedNestedRouteFromEnv();
5976
4989
  const nestedParentAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
5977
4990
  const shareEnabled = effectiveParams.share === true;
@@ -6013,6 +5026,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6013
5026
  const parentModel = requestParentModel;
6014
5027
  prepareForkThinking = (agentName, index, modelOverride) => {
6015
5028
  const agentConfig = agents.find((agent) => agent.name === agentName);
5029
+ if (agentConfig?.runner?.type === "external-cli" || agentConfig?.runner?.type === "external-job") {
5030
+ forkThinkingRequirements.set(index, true);
5031
+ return;
5032
+ }
6016
5033
  const primaryModel = resolveEffectiveSubagentModel(
6017
5034
  modelOverride,
6018
5035
  agentConfig?.model,
@@ -6050,9 +5067,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6050
5067
  : (effectiveParams.chain ?? []).flatMap((step) => getStepAgents(step as ChainStep));
6051
5068
  const externalAgent = selectedAgentNames
6052
5069
  .map((name) => agents.find((agent) => agent.name === name))
6053
- .find((agent) => agent?.runner?.type === "external-cli");
5070
+ .find((agent) => agent?.runner?.type === "external-cli" || agent?.runner?.type === "external-job");
6054
5071
  if (externalAgent && (!effectiveAsync || effectiveParams.foregroundOnly === true)) {
6055
- 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.`);
5072
+ 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.`);
6056
5073
  }
6057
5074
  const foregroundTimeout = resolveSingleAgentLaunchTimeout(
6058
5075
  effectiveParams,
@@ -6249,6 +5266,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6249
5266
  usageBudget: usageBudget.budget,
6250
5267
  allowZeroToolBudget,
6251
5268
  configToolBudget: configToolBudget.toolBudget,
5269
+ configToolTimeoutMs: deps.config.toolTimeoutMs,
6252
5270
  contextPolicy,
6253
5271
  modelScope,
6254
5272
  parentModel: requestParentModel,
@@ -6330,6 +5348,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6330
5348
  };
6331
5349
  deps.state.foregroundControls.set(runId, foregroundControl);
6332
5350
  deps.state.lastForegroundControlId = runId;
5351
+ deps.activateSupervisorTransport?.();
5352
+ deps.refreshResultDelivery?.();
6333
5353
  }
6334
5354
 
6335
5355
  const writeNestedForegroundEvent = (type: "subagent.nested.started" | "subagent.nested.completed", result?: AgentToolResult<Details>): void => {
@@ -6446,16 +5466,6 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
6446
5466
  nestedForegroundStarted = true;
6447
5467
  }
6448
5468
  const runFanoutAnnotateContent = !delegatedExecution;
6449
- if (hasChain && effectiveParams.chain) {
6450
- const result = await runChainPath(execData, deps);
6451
- writeNestedForegroundEvent("subagent.nested.completed", result);
6452
- return attachMission(withRunFanoutBudget(withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary), runFanoutBudget, { annotateContent: runFanoutAnnotateContent }));
6453
- }
6454
- if (hasTasks && effectiveParams.tasks) {
6455
- const result = await runParallelPath(execData, deps);
6456
- writeNestedForegroundEvent("subagent.nested.completed", result);
6457
- return attachMission(withRunFanoutBudget(withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary), runFanoutBudget, { annotateContent: runFanoutAnnotateContent }));
6458
- }
6459
5469
  if (hasSingle) {
6460
5470
  const result = await runSinglePath(execData, deps);
6461
5471
  writeNestedForegroundEvent("subagent.nested.completed", result);