pi-subagents 0.51.0 → 0.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/agents/oracle.md +3 -1
  3. package/agents/reviewer.md +1 -0
  4. package/agents/scout.md +2 -2
  5. package/agents/worker.md +2 -1
  6. package/docs/agents.md +8 -3
  7. package/docs/extension-api.md +1 -1
  8. package/docs/models.md +2 -0
  9. package/docs/observability.md +37 -1
  10. package/docs/workflows.md +2 -0
  11. package/package.json +1 -1
  12. package/skills/pi-subagents/SKILL.md +1 -0
  13. package/skills/pi-subagents/references/constraints-and-recipes.md +6 -2
  14. package/skills/pi-subagents/references/execution-controls.md +12 -1
  15. package/skills/pi-subagents/references/prompting-and-roles.md +2 -0
  16. package/src/agents/agent-management.ts +24 -6
  17. package/src/agents/agents.ts +37 -11
  18. package/src/agents/skills.ts +1 -1
  19. package/src/api/preflight.ts +5 -2
  20. package/src/extension/index.ts +1 -0
  21. package/src/extension/public-execution.ts +3 -1
  22. package/src/intercom/native-supervisor-channel.ts +2 -1
  23. package/src/missions/workflow-state.ts +2 -2
  24. package/src/runs/background/async-execution.ts +19 -9
  25. package/src/runs/background/async-resume.ts +19 -2
  26. package/src/runs/background/async-retention.ts +2 -4
  27. package/src/runs/background/completion-dedupe.ts +5 -1
  28. package/src/runs/background/completion-replay.ts +22 -12
  29. package/src/runs/background/fleet-view.ts +68 -19
  30. package/src/runs/background/inspect-rpc.ts +443 -0
  31. package/src/runs/background/notify.ts +28 -4
  32. package/src/runs/background/result-watcher.ts +44 -0
  33. package/src/runs/background/resume-guidance.ts +8 -5
  34. package/src/runs/background/scheduled-runs.ts +2 -0
  35. package/src/runs/background/subagent-runner.ts +3 -2
  36. package/src/runs/background/subagent-wait.ts +1 -1
  37. package/src/runs/background/wait-subscriptions.ts +1 -1
  38. package/src/runs/foreground/execution.ts +1 -1
  39. package/src/runs/foreground/subagent-executor.ts +239 -76
  40. package/src/runs/foreground/workflow-detach-reconcile.ts +194 -0
  41. package/src/runs/shared/acceptance.ts +4 -4
  42. package/src/runs/shared/external-job-bridge.ts +0 -6
  43. package/src/runs/shared/model-fallback.ts +77 -34
  44. package/src/runs/shared/session-lease.ts +0 -6
  45. package/src/runs/shared/worktree.ts +2 -1
  46. package/src/shared/types.ts +11 -0
  47. package/src/slash/slash-bridge.ts +2 -1
  48. package/src/slash/slash-commands.ts +18 -0
  49. package/src/watchdog/change-signature.ts +1 -1
  50. package/src/watchdog/lsp-diagnostics.ts +1 -0
  51. package/src/workflows/chat-progress.ts +1 -1
  52. package/src/workflows/scripted-workflow.ts +13 -2
  53. package/agents/gpt-pro.md +0 -17
@@ -7,7 +7,7 @@ import { findBlockingAgentDiagnostic, resolveAgentName, type AgentConfig, type A
7
7
  import { getArtifactsDir, getChainRunsDir, getProjectArtifactPackagingWarning, getProjectSubagentsDir } from "../../shared/artifacts.ts";
8
8
  import { writeAtomicJson } from "../../shared/atomic-json.ts";
9
9
  import { createCapacityResilientJsonWriter } from "../../shared/capacity-resilient-json.ts";
10
- import { isStorageCapacityError } from "../../shared/file-system-retry.ts";
10
+ import { isRetryableFileSystemError, isStorageCapacityError } from "../../shared/file-system-retry.ts";
11
11
  import { resolveEffectiveThinking, toModelInfo, type ModelInfo } from "../../shared/model-info.ts";
12
12
  import {
13
13
  beginForegroundChild,
@@ -28,7 +28,7 @@ import { normalizePublicSubagentExecution } from "../../extension/public-executi
28
28
  import { runSync } from "./execution.ts";
29
29
  import { handleWatchdogToolAction, WATCHDOG_TOOL_ACTIONS } from "../../watchdog/tool-actions.ts";
30
30
  import type { MainWatchdogRuntime } from "../../watchdog/runtime.ts";
31
- import { buildModelCandidates, normalizeParentModel, resolveEffectiveSubagentModel, resolveModelCandidate, type ParentModel } from "../shared/model-fallback.ts";
31
+ import { buildModelCandidates, inheritsParentModel, normalizeParentModel, resolveEffectiveSubagentModel, resolveModelCandidate, type ParentModel } from "../shared/model-fallback.ts";
32
32
  import { formatRetainedChildren, listRetainedChildren } from "../background/retained-children.ts";
33
33
  import type { ModelScopeConfig } from "../shared/model-scope.ts";
34
34
  import { aggregateParallelOutputs } from "../shared/parallel-utils.ts";
@@ -88,7 +88,7 @@ import {
88
88
  resolveSubagentResultStatus,
89
89
  stripDetailsOutputsForIntercomReceipt,
90
90
  } from "../../intercom/result-intercom.ts";
91
- import { applySteeringRecoveryAgentConfig, buildRevivedAsyncTask, resolveAsyncResumeTarget, resolveAsyncRunLocation } from "../background/async-resume.ts";
91
+ import { applySteeringRecoveryAgentConfig, asyncReviveRequiresRecoveryDescriptor, buildRevivedAsyncTask, resolveAsyncResumeTarget, resolveAsyncRunLocation } from "../background/async-resume.ts";
92
92
  import { deliverInterruptRequest, readRevivalBriefs, requestAsyncSteer, type SteerDeliveryMode } from "../background/control-channel.ts";
93
93
  import { updateSteeringTarget, waitForSteeringAction } from "../background/steering.ts";
94
94
  import { canQueueRetainedAsyncFollowUp, steerAsyncRun } from "./async-steering-action.ts";
@@ -101,6 +101,7 @@ import {
101
101
  } from "./workflow-foreground-steering.ts";
102
102
  import { stopAsyncRun } from "./async-stop-action.ts";
103
103
  import { dismissRecoveredWorkflow } from "./async-dismiss-action.ts";
104
+ import { promotePausedWorkflowIfSettled, reconcileDetachedWorkflowChildCompletion } from "./workflow-detach-reconcile.ts";
104
105
  import { reconcileAsyncRun } from "../background/stale-run-reconciler.ts";
105
106
  import { resolveAsyncRootResultPath, waitForImportedAsyncRoot } from "../background/chain-root-attachment.ts";
106
107
  import { resultFilePath, writeAsyncResultFile } from "../background/result-files.ts";
@@ -171,6 +172,7 @@ import {
171
172
  resolveCurrentMaxSubagentDepth,
172
173
  resolveMaxSubagentSpawnsPerRun,
173
174
  wrapForkTask,
175
+ type ScheduleOrigin,
174
176
  } from "../../shared/types.ts";
175
177
 
176
178
  const MUTATING_MANAGEMENT_ACTIONS = new Set(["create", "update", "delete", "eject", "disable", "enable", "reset", "grant-spawn-budget", "watchdog.configure", "mission.create", "mission.update", "mission.resolve-decision", "mission.attach-run", "mission.close", "inspector.open", "inspector.close", "project.open", "project.close", "worktree.discard", "refine", "refine.rollback", "dismiss", "schedule.create", "schedule.pause", "schedule.resume", "schedule.run", "schedule.run-due", "schedule.delete"]);
@@ -292,6 +294,8 @@ export interface SubagentParamsLike {
292
294
  /** Internal workflow ownership metadata; not part of the public schema. */
293
295
  workflowParentRunId?: string;
294
296
  workflowKey?: string;
297
+ /** Set by the scheduler so this run's completion can name the schedule that produced it. */
298
+ scheduleOrigin?: ScheduleOrigin;
295
299
  workflowChildAsyncId?: string;
296
300
  workflowParentDeadlineAt?: number;
297
301
  suppressRoutineResultIntercom?: boolean;
@@ -386,8 +390,8 @@ interface ExecutorDeps {
386
390
  kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
387
391
  }
388
392
 
389
- type ForkSessionFileForTask = (agentName: string, idx?: number, modelOverride?: string) => string | undefined;
390
- type ForkThinkingOverrideForTask = (agentName: string, idx?: number, modelOverride?: string) => AgentConfig["thinking"] | undefined;
393
+ type ForkSessionFileForTask = (agentName: string, idx?: number, modelOverride?: string, modelOverrideFromParent?: boolean) => string | undefined;
394
+ type ForkThinkingOverrideForTask = (agentName: string, idx?: number, modelOverride?: string, modelOverrideFromParent?: boolean) => AgentConfig["thinking"] | undefined;
391
395
 
392
396
  interface ExecutionContextData {
393
397
  params: SubagentParamsLike;
@@ -953,11 +957,48 @@ function emitAdvisoryControlEvent(pi: ExtensionAPI, channel: string, payload: un
953
957
  }
954
958
  }
955
959
 
960
+ function persistAsyncWorkflowControlEvent(input: {
961
+ job: AsyncJobState;
962
+ event: ControlEvent;
963
+ controlConfig: ResolvedControlConfig;
964
+ intercomBridge: IntercomBridgeState;
965
+ childIntercomTarget?: string;
966
+ }): void {
967
+ const channels = input.event.type === "active_long_running"
968
+ ? input.controlConfig.notifyChannels.filter((channel) => channel !== "intercom")
969
+ : input.controlConfig.notifyChannels;
970
+ if (channels.length === 0) return;
971
+ const record = {
972
+ ts: Date.now(),
973
+ runId: input.job.asyncId,
974
+ type: "subagent.control",
975
+ event: input.event,
976
+ channels,
977
+ childIntercomTarget: input.childIntercomTarget,
978
+ noticeText: formatControlNoticeMessage(input.event, input.childIntercomTarget),
979
+ ...(input.intercomBridge.active && input.intercomBridge.orchestratorTarget && channels.includes("intercom")
980
+ ? {
981
+ intercom: {
982
+ to: input.intercomBridge.orchestratorTarget,
983
+ message: formatControlIntercomMessage(input.event, input.childIntercomTarget),
984
+ },
985
+ }
986
+ : {}),
987
+ };
988
+ try {
989
+ fs.appendFileSync(path.join(input.job.asyncDir, "events.jsonl"), `${JSON.stringify(record)}\n`, "utf-8");
990
+ } catch (error) {
991
+ if (!isStorageCapacityError(error)) throw error;
992
+ console.error("Failed to append async workflow control event while storage is full:", error);
993
+ }
994
+ }
995
+
956
996
  function emitControlNotification(input: {
957
997
  pi: ExtensionAPI;
958
998
  controlConfig: ResolvedControlConfig;
959
999
  intercomBridge: IntercomBridgeState;
960
1000
  event: ControlEvent;
1001
+ source?: "foreground" | "async";
961
1002
  }): void {
962
1003
  if (!shouldNotifyControlEvent(input.controlConfig, input.event)) return;
963
1004
  const childIntercomTarget = input.intercomBridge.active
@@ -965,7 +1006,7 @@ function emitControlNotification(input: {
965
1006
  : undefined;
966
1007
  const payload = {
967
1008
  event: input.event,
968
- source: "foreground" as const,
1009
+ source: input.source ?? "foreground",
969
1010
  childIntercomTarget,
970
1011
  noticeText: formatControlNoticeMessage(input.event, childIntercomTarget),
971
1012
  };
@@ -1660,7 +1701,7 @@ async function resumeAsyncRun(input: {
1660
1701
  if (!revivalSessionFile) {
1661
1702
  return { content: [{ type: "text", text: `Async child '${target.runId}' has no persisted session file to resume.` }], isError: true, details: { mode: "management", results: [] } };
1662
1703
  }
1663
- if (target.source === "async" && !recoveryDescriptor) {
1704
+ if (target.source === "async" && asyncReviveRequiresRecoveryDescriptor(target)) {
1664
1705
  return { content: [{ type: "text", text: `Async child '${target.runId}' is missing its required run fan-out recovery identity. Start a new run instead.` }], isError: true, details: { mode: "management", results: [] } };
1665
1706
  }
1666
1707
  const runId = randomUUID();
@@ -1725,6 +1766,7 @@ async function resumeAsyncRun(input: {
1725
1766
  ...(input.deps.state.currentSessionId ? { parentSessionId: input.deps.state.currentSessionId } : {}),
1726
1767
  },
1727
1768
  modelOverride: recoveryDescriptor?.model ?? target.model,
1769
+ modelOverrideFromParent: recoveryDescriptor?.modelOverrideFromParent,
1728
1770
  thinkingOverride: recoveryDescriptor?.thinking ?? target.thinking,
1729
1771
  outputBaseDir: resolveSingleRunOutputBaseDir(input.deps, artifactsDir, runId),
1730
1772
  maxSubagentDepth: recoveryDescriptor?.maxSubagentDepth ?? resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth),
@@ -1874,14 +1916,32 @@ function formatFailedSingleRunOutput(result: SingleResult, displayOutput: string
1874
1916
  return lines.join("\n");
1875
1917
  }
1876
1918
 
1877
- function createForegroundControlNotifier(data: Pick<ExecutionContextData, "controlConfig" | "intercomBridge">, deps: Pick<ExecutorDeps, "pi" | "state">): (event: ControlEvent) => void {
1919
+ function createForegroundControlNotifier(data: Pick<ExecutionContextData, "controlConfig" | "intercomBridge" | "params">, deps: Pick<ExecutorDeps, "pi" | "state">): (event: ControlEvent) => void {
1878
1920
  return (event) => {
1879
1921
  applyControlEventToRememberedForegroundRun(deps.state, event);
1922
+ const parentWorkflowRunId = data.params.workflowParentRunId;
1923
+ const asyncWorkflow = typeof parentWorkflowRunId === "string" ? deps.state.asyncJobs.get(parentWorkflowRunId) : undefined;
1924
+ const workflowKey = typeof data.params.workflowKey === "string" && data.params.workflowKey.trim()
1925
+ ? data.params.workflowKey.trim()
1926
+ : undefined;
1927
+ const enriched = workflowKey && !event.workflowKey ? { ...event, workflowKey } : event;
1928
+ if (asyncWorkflow) {
1929
+ persistAsyncWorkflowControlEvent({
1930
+ job: asyncWorkflow,
1931
+ event: enriched,
1932
+ controlConfig: data.controlConfig,
1933
+ intercomBridge: data.intercomBridge,
1934
+ childIntercomTarget: data.intercomBridge.active
1935
+ ? resolveSubagentIntercomTarget(enriched.runId, enriched.agent, enriched.index)
1936
+ : undefined,
1937
+ });
1938
+ }
1880
1939
  emitControlNotification({
1881
1940
  pi: deps.pi,
1882
1941
  controlConfig: data.controlConfig,
1883
1942
  intercomBridge: data.intercomBridge,
1884
- event,
1943
+ event: enriched,
1944
+ source: asyncWorkflow ? "async" : "foreground",
1885
1945
  });
1886
1946
  };
1887
1947
  }
@@ -2775,6 +2835,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2775
2835
  const modelOverride = a.runner?.type === "external-cli" || a.runner?.type === "external-job"
2776
2836
  ? params.model ?? (externalRunnerWithoutExplicitModel ? undefined : a.model)
2777
2837
  : resolveEffectiveSubagentModel(params.model as string | undefined, a.model, parentModel, availableModels, currentProvider, data.modelScope === undefined ? {} : { scope: data.modelScope });
2838
+ const modelOverrideFromParent = inheritsParentModel(params.model as string | undefined, a.model, parentModel);
2778
2839
  return executeAsyncSingle(id, compactOptional<Parameters<typeof executeAsyncSingle>[1]>({
2779
2840
  agent: params.agent!,
2780
2841
  task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(params.task ?? "") : (params.task ?? ""),
@@ -2790,7 +2851,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2790
2851
  shareEnabled,
2791
2852
  activeAsyncCapacity: data.activeAsyncCapacity,
2792
2853
  sessionRoot,
2793
- sessionFile: sessionFileForTask(params.agent!, 0, modelOverride),
2854
+ sessionFile: sessionFileForTask(params.agent!, 0, modelOverride, modelOverrideFromParent),
2794
2855
  context: contextPolicy.contextForAgent(params.agent!),
2795
2856
  skills,
2796
2857
  output: effectiveOutput,
@@ -2798,7 +2859,8 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2798
2859
  ...(params.reads !== undefined ? { reads: params.reads } : {}),
2799
2860
  outputBaseDir: resolveSingleRunOutputBaseDir(deps, artifactsDir, id),
2800
2861
  modelOverride,
2801
- thinkingOverride: externalRunnerWithoutExplicitModel ? undefined : thinkingOverrideForTask(params.agent!, 0, modelOverride),
2862
+ modelOverrideFromParent,
2863
+ thinkingOverride: externalRunnerWithoutExplicitModel ? undefined : thinkingOverrideForTask(params.agent!, 0, modelOverride, modelOverrideFromParent),
2802
2864
  maxSubagentDepth,
2803
2865
  waitToolEnabled: deps.waitToolEnabled,
2804
2866
  ...(params.worktree === true ? { worktree: true } : {}),
@@ -2917,9 +2979,19 @@ function resolveWorkflowChildOutputPath(input: {
2917
2979
  configuredOutputBaseDir?: string;
2918
2980
  discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[] };
2919
2981
  workflowAgentScope?: unknown;
2982
+ state?: SubagentState;
2920
2983
  key: string;
2921
2984
  params: Record<string, unknown>;
2922
- }): string | undefined {
2985
+ }): { path?: string; inherited: boolean } {
2986
+ if (typeof input.params.resume === "string") {
2987
+ if (!input.state) return { path: undefined, inherited: false };
2988
+ const index = input.params.index;
2989
+ const target = resolveResumeTarget({
2990
+ id: input.params.resume.trim(),
2991
+ ...(typeof index === "number" && Number.isInteger(index) ? { index } : {}),
2992
+ }, input.state);
2993
+ return { path: "recoveryDescriptor" in target ? target.recoveryDescriptor?.outputPath : undefined, inherited: false };
2994
+ }
2923
2995
  const rawOutput = input.params.output;
2924
2996
  const hasExplicitOutput = typeof rawOutput === "string" || typeof rawOutput === "boolean";
2925
2997
  const childCwd = typeof input.params.cwd === "string" ? resolveChildCwd(input.workflowCwd, input.params.cwd) : input.workflowCwd;
@@ -2934,7 +3006,10 @@ function resolveWorkflowChildOutputPath(input: {
2934
3006
  : input.aggregateOutputPath
2935
3007
  ? workflowChildDefaultOutput(input.aggregateOutputPath, input.artifactsDir, input.workflowRunId, input.key)
2936
3008
  : agentOutput;
2937
- return resolveSingleOutputPath(output, input.ctxCwd, childCwd, input.configuredOutputBaseDir);
3009
+ return {
3010
+ path: resolveSingleOutputPath(output, input.ctxCwd, childCwd, input.configuredOutputBaseDir),
3011
+ inherited: !hasExplicitOutput && !input.aggregateOutputPath && agentOutput !== undefined,
3012
+ };
2938
3013
  }
2939
3014
 
2940
3015
  function workflowChildOutputClaims(input: {
@@ -2946,18 +3021,35 @@ function workflowChildOutputClaims(input: {
2946
3021
  configuredOutputBaseDir?: string;
2947
3022
  discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[] };
2948
3023
  workflowAgentScope?: unknown;
3024
+ state: SubagentState;
2949
3025
  claimedOutputPaths: Map<string, string>;
2950
3026
  entries: Array<{ key: string; params: Record<string, unknown> }>;
2951
- }): { error?: string; claims?: Map<string, string> } {
3027
+ }): { error?: string; claims?: Map<string, string>; overrides?: Map<string, string> } {
3028
+ const resolvedEntries = input.entries.map(({ key, params }) => ({
3029
+ key,
3030
+ ...resolveWorkflowChildOutputPath({ ...input, key, params }),
3031
+ }));
3032
+ const paths = new Map<string, number>();
3033
+ for (const claimedPath of input.claimedOutputPaths.keys()) paths.set(claimedPath, 1);
3034
+ for (const { path: resolved } of resolvedEntries) {
3035
+ if (resolved) paths.set(resolved, (paths.get(resolved) ?? 0) + 1);
3036
+ }
3037
+ const overrides = new Map<string, string>();
3038
+ for (const entry of resolvedEntries) {
3039
+ if (entry.inherited && entry.path && (paths.get(entry.path) ?? 0) > 1) {
3040
+ const output = workflowChildDefaultOutput(input.aggregateOutputPath, input.artifactsDir, input.workflowRunId, entry.key);
3041
+ overrides.set(entry.key, output);
3042
+ entry.path = output;
3043
+ }
3044
+ }
2952
3045
  const claims = new Map(input.claimedOutputPaths);
2953
- for (const { key, params } of input.entries) {
2954
- const resolved = resolveWorkflowChildOutputPath({ ...input, key, params });
3046
+ for (const { key, path: resolved } of resolvedEntries) {
2955
3047
  if (!resolved) continue;
2956
3048
  const previous = claims.get(resolved);
2957
3049
  if (previous) return { error: `Workflow children '${previous}' and '${key}' resolve output to the same path: ${resolved}. Use distinct child output paths.` };
2958
3050
  claims.set(resolved, key);
2959
3051
  }
2960
- return { claims };
3052
+ return { claims, overrides };
2961
3053
  }
2962
3054
 
2963
3055
  function applyWorkflowChildOutputClaims(target: Map<string, string>, claims: Map<string, string>): void {
@@ -2977,14 +3069,17 @@ function prepareWorkflowChildLaunchParams(input: {
2977
3069
  configuredOutputBaseDir?: string;
2978
3070
  discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[] };
2979
3071
  workflowAgentScope?: unknown;
3072
+ outputOverride?: string;
2980
3073
  options?: { missionDetached?: boolean; suppressRoutineResultIntercom?: boolean; runFanoutBudget?: RunFanoutBudgetDescriptor; parentDeadlineAt?: number };
2981
3074
  }): SubagentParamsLike {
2982
3075
  let childParams = input.childParams;
2983
- if (input.childParams.output === undefined && input.childParams.resume === undefined && input.aggregateOutputPath !== undefined) {
3076
+ if (input.childParams.output === undefined && input.childParams.resume === undefined && input.outputOverride !== undefined) {
3077
+ childParams = { ...input.childParams, output: input.outputOverride };
3078
+ } else if (input.childParams.output === undefined && input.childParams.resume === undefined && input.aggregateOutputPath !== undefined) {
2984
3079
  childParams = { ...input.childParams, output: workflowChildDefaultOutput(input.aggregateOutputPath, input.artifactsDir, input.parentWorkflowRunId, input.workflowKey) };
2985
3080
  } else if (input.childParams.resume === undefined) {
2986
3081
  const resolvedOutput = resolveWorkflowChildOutputPath({ ctxCwd: input.ctxCwd, workflowCwd: input.workflowCwd, artifactsDir: input.artifactsDir, workflowRunId: input.parentWorkflowRunId, aggregateOutputPath: input.aggregateOutputPath, configuredOutputBaseDir: input.configuredOutputBaseDir, discoverAgents: input.discoverAgents, workflowAgentScope: input.workflowAgentScope, key: input.workflowKey, params: input.childParams });
2987
- if (resolvedOutput) childParams = { ...input.childParams, output: resolvedOutput };
3082
+ if (resolvedOutput.path) childParams = { ...input.childParams, output: resolvedOutput.path };
2988
3083
  }
2989
3084
  return prepareWorkflowLaunchParams(input.workflowDefaults, childParams, input.parentWorkflowRunId, input.workflowKey, input.options);
2990
3085
  }
@@ -3089,6 +3184,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3089
3184
  currentProvider,
3090
3185
  data.modelScope === undefined ? {} : { scope: data.modelScope },
3091
3186
  );
3187
+ const modelOverrideFromParent = inheritsParentModel(params.model as string | undefined, agentConfig.model, parentModel);
3092
3188
  let skillOverride: string[] | false | undefined = normalizeSkillInput(params.skill);
3093
3189
  let readsOverride: string[] | false | undefined = params.reads;
3094
3190
  const rawOutput = params.output !== undefined ? params.output : agentConfig.output;
@@ -3163,7 +3259,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3163
3259
  let detachForeground: ((reason?: string) => boolean) | undefined;
3164
3260
  const foregroundControl = deps.state.foregroundControls.get(runId);
3165
3261
  if (foregroundControl) {
3166
- const thinking = resolveEffectiveThinking(modelOverride, thinkingOverrideForTask(params.agent!, 0, modelOverride));
3262
+ const thinking = resolveEffectiveThinking(modelOverride, thinkingOverrideForTask(params.agent!, 0, modelOverride, modelOverrideFromParent));
3167
3263
  beginForegroundChild(foregroundControl, omitUndefinedProperties({
3168
3264
  index: 0,
3169
3265
  agent: params.agent!,
@@ -3208,7 +3304,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3208
3304
  intercomEvents: deps.pi.events,
3209
3305
  runId,
3210
3306
  sessionDir: sessionDirForIndex(0),
3211
- sessionFile: sessionFileForTask(params.agent!, 0, modelOverride),
3307
+ sessionFile: sessionFileForTask(params.agent!, 0, modelOverride, modelOverrideFromParent),
3212
3308
  share: shareEnabled,
3213
3309
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
3214
3310
  artifactConfig,
@@ -3225,7 +3321,8 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3225
3321
  nestedRoute: foregroundControl?.nestedRoute,
3226
3322
  index: 0,
3227
3323
  modelOverride,
3228
- thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride),
3324
+ modelOverrideFromParent,
3325
+ thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride, modelOverrideFromParent),
3229
3326
  availableModels,
3230
3327
  preferredModelProvider: currentProvider,
3231
3328
  modelScope: data.modelScope,
@@ -3241,10 +3338,21 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3241
3338
  onDetachedExit: (result) => {
3242
3339
  try {
3243
3340
  try {
3244
- updateRememberedForegroundChild(deps.state, { runId, mode: "single", cwd: singleCwd, sessionId: data.parentSessionId, index: 0, result, events: deps.pi.events, notify: params.workflowParentRunId === undefined });
3341
+ updateRememberedForegroundChild(deps.state, { runId, mode: "single", cwd: singleCwd, sessionId: data.parentSessionId, index: 0, result, events: deps.pi.events, notify: true });
3245
3342
  } catch {
3246
3343
  // Remembered foreground state is best-effort; run history and cleanup must still complete.
3247
3344
  }
3345
+ const workflowParentRunId = params.workflowParentRunId ?? foregroundControl?.parentWorkflowRunId;
3346
+ if (workflowParentRunId) {
3347
+ reconcileDetachedWorkflowChildCompletion({
3348
+ state: deps.state,
3349
+ workflowRunId: workflowParentRunId,
3350
+ childRunId: runId,
3351
+ result,
3352
+ events: deps.pi.events,
3353
+ workflowKey: params.workflowKey ?? foregroundControl?.workflowKey,
3354
+ });
3355
+ }
3248
3356
  } finally {
3249
3357
  try {
3250
3358
  if (!artifactConfig.enabled) cleanupStructuredOutputRuntime(structuredRuntime);
@@ -3611,6 +3719,7 @@ export function prepareWorkflowLaunchParams(
3611
3719
  return {
3612
3720
  action: "resume",
3613
3721
  id: childParams.resume.trim(),
3722
+ ...(typeof childParams.index === "number" && Number.isInteger(childParams.index) ? { index: childParams.index } : {}),
3614
3723
  message: typeof childParams.task === "string" ? childParams.task.trim() : "",
3615
3724
  workflowParentRunId: parentWorkflowRunId,
3616
3725
  workflowKey,
@@ -3896,6 +4005,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3896
4005
  ...(timeout !== undefined ? { deadlineAt: startedAt + timeout, timeoutMs: timeout } : {}),
3897
4006
  cwd: workflowCwd,
3898
4007
  ...(workflowSessionRoot ? { sessionRoot: workflowSessionRoot } : {}),
4008
+ ...(requestParams.scheduleOrigin ? { scheduleOrigin: requestParams.scheduleOrigin } : {}),
3899
4009
  pid: process.pid,
3900
4010
  steps: [],
3901
4011
  workflow: { trace: [], emits: [], console: [] },
@@ -3905,8 +4015,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3905
4015
  try {
3906
4016
  fs.appendFileSync(eventsPath, `${JSON.stringify({ ts: Date.now(), runId: workflowRunId, ...event })}\n`, "utf-8");
3907
4017
  } catch (error) {
3908
- if (!isStorageCapacityError(error)) throw error;
3909
- console.error(`Failed to append async workflow event while storage is full:`, error);
4018
+ // The event log is a journal, not workflow truth. Callers append from
4019
+ // inside run-result handling, so losing an entry to a full disk or to a
4020
+ // transient Windows lock must not fail the run being recorded.
4021
+ if (!isStorageCapacityError(error) && !isRetryableFileSystemError(error)) throw error;
4022
+ console.error(`Failed to append async workflow event '${eventsPath}':`, error);
3910
4023
  }
3911
4024
  };
3912
4025
  let indexedState: AsyncStatus["state"] | undefined;
@@ -3932,34 +4045,62 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3932
4045
  : writeAtomicJson(filePath, payload),
3933
4046
  });
3934
4047
  let initialPersistenceComplete = false;
3935
- const persist = () => {
4048
+ let persistClosed = false;
4049
+ let statusPersistenceDegraded = false;
4050
+ // Progress journalling runs from admit, launch, the launch and progress observers,
4051
+ // onTrace and onEmit -- all inside the promises a workflowScript awaits. Those
4052
+ // callers pass tolerateStatusWriteFailure so a transient lock on status.json cannot
4053
+ // mark a finished child failed or abort its still-running siblings. Initial and
4054
+ // terminal writes stay fail-fast on purpose: no child work is at risk by then, and
4055
+ // silently dropping a terminal write would leave status.json and the active-run
4056
+ // index pinned at "running" after the result already says complete.
4057
+ const persist = (options: { tolerateStatusWriteFailure?: boolean } = {}) => {
4058
+ if (persistClosed) return;
4059
+ const liveJob = deps.state.asyncJobs.get(workflowRunId);
4060
+ if (liveJob && (liveJob.status === "complete" || liveJob.status === "failed") && status.state !== "complete" && status.state !== "failed") return;
3936
4061
  status.lastUpdate = Date.now();
3937
- if (initialPersistenceComplete) runPersistence.write(statusPath, status);
3938
- else {
4062
+ if (!initialPersistenceComplete) {
3939
4063
  writeAtomicJson(statusPath, status);
3940
4064
  initialPersistenceComplete = true;
3941
4065
  queueActiveRunIndex();
4066
+ } else if (options.tolerateStatusWriteFailure) {
4067
+ try {
4068
+ runPersistence.write(statusPath, status);
4069
+ statusPersistenceDegraded = false;
4070
+ } catch (error) {
4071
+ const message = `Failed to persist async workflow state ${statusPath}: ${error instanceof Error ? error.message : String(error)}`;
4072
+ console.error(message, error);
4073
+ if (!statusPersistenceDegraded) {
4074
+ statusPersistenceDegraded = true;
4075
+ try {
4076
+ appendWorkflowEvent({ type: "subagent.workflow.status_write_failed", error: message });
4077
+ } catch (eventError) {
4078
+ console.error(`Failed to record degraded status persistence for '${statusPath}':`, eventError);
4079
+ }
4080
+ }
4081
+ }
4082
+ } else {
4083
+ runPersistence.write(statusPath, status);
3942
4084
  }
3943
- const job = deps.state.asyncJobs.get(workflowRunId);
3944
- if (job) {
3945
- job.status = status.state;
3946
- job.updatedAt = status.lastUpdate;
3947
- job.activityState = status.activityState;
3948
- job.lastActivityAt = status.lastActivityAt;
3949
- job.currentTool = status.currentTool;
3950
- job.currentToolStartedAt = status.currentToolStartedAt;
3951
- job.currentPath = status.currentPath;
3952
- job.turnCount = status.turnCount;
3953
- job.toolCount = status.toolCount;
3954
- job.currentStep = status.currentStep;
4085
+ if (liveJob) {
4086
+ liveJob.status = status.state;
4087
+ liveJob.updatedAt = status.lastUpdate;
4088
+ liveJob.activityState = status.activityState;
4089
+ liveJob.lastActivityAt = status.lastActivityAt;
4090
+ liveJob.currentTool = status.currentTool;
4091
+ liveJob.currentToolStartedAt = status.currentToolStartedAt;
4092
+ liveJob.currentPath = status.currentPath;
4093
+ liveJob.turnCount = status.turnCount;
4094
+ liveJob.toolCount = status.toolCount;
4095
+ liveJob.currentStep = status.currentStep;
3955
4096
  if (status.steps) {
3956
- job.steps = status.steps.map((step, index) => ({ ...step, index }));
3957
- job.agents = status.steps.map((step) => step.agent);
4097
+ liveJob.steps = status.steps.map((step, index) => ({ ...step, index }));
4098
+ liveJob.agents = status.steps.map((step) => step.agent);
3958
4099
  } else {
3959
- delete job.steps;
3960
- delete job.agents;
4100
+ delete liveJob.steps;
4101
+ delete liveJob.agents;
3961
4102
  }
3962
- job.workflow = status.workflow;
4103
+ liveJob.workflow = status.workflow;
3963
4104
  }
3964
4105
  };
3965
4106
  const writeWorkflowResult = (payload: Record<string, unknown>): boolean => {
@@ -3994,7 +4135,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
3994
4135
  status.toolCount = toolCounts.length > 0 ? toolCounts.reduce((total, count) => total + count, 0) : undefined;
3995
4136
  status.currentStep = runningSteps.length === 1 ? steps.indexOf(runningSteps[0]!) : undefined;
3996
4137
  };
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 };
4138
+ const workflowJob: AsyncJobState = { asyncId: workflowRunId, asyncDir, toolCallId, cwd: workflowCwd, ...(workflowSessionRoot ? { sessionRoot: workflowSessionRoot } : {}), status: "running", sessionId: currentSessionId ?? undefined, mode: "workflow", agents: [], steps: [], startedAt, updatedAt: startedAt, ...(requestParams.scheduleOrigin ? { scheduleOrigin: requestParams.scheduleOrigin } : {}), ...(timeout !== undefined ? { timeoutMs: timeout, deadlineAt: startedAt + timeout } : {}), workflow: status.workflow };
3998
4139
  deps.state.asyncJobs.set(workflowRunId, workflowJob);
3999
4140
  deps.state.fleetJobs ??= new Map();
4000
4141
  deps.state.fleetJobs.set(workflowRunId, workflowJob);
@@ -4019,6 +4160,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4019
4160
  const configuredOutputBaseDir = resolveConfiguredSingleRunOutputBaseDir(deps);
4020
4161
  const workflowAggregateOutputPath = resolveWorkflowAggregateOutputPath(workflowOutput, ctx.cwd, workflowCwd, resolveSingleRunOutputBaseDir(deps, workflowArtifactsDir, workflowRunId), configuredOutputBaseDir);
4021
4162
  const claimedOutputPaths = new Map<string, string>();
4163
+ const childOutputOverrides = new Map<string, string>();
4022
4164
  const producedChildOutputPaths = new Set<string>();
4023
4165
  const workflowSteps = new Map<string, NonNullable<AsyncStatus["steps"]>[number]>();
4024
4166
  let projectedTraceLength = 0;
@@ -4081,7 +4223,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4081
4223
  projectedTraceLength = trace.length;
4082
4224
  projectedTraceTail = trace.at(-1);
4083
4225
  projectWorkflowActivity();
4084
- persist();
4226
+ persist({ tolerateStatusWriteFailure: true });
4085
4227
  appendWorkflowEvent({ type: "subagent.workflow.trace", trace });
4086
4228
  };
4087
4229
  try {
@@ -4092,16 +4234,17 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4092
4234
  ...(workflowState ? { state: workflowState } : {}),
4093
4235
  onTrace: updateTrace,
4094
4236
  admit: (calls) => {
4095
- const outputClaims = workflowChildOutputClaims({ ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, workflowRunId, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: deps.discoverAgents, workflowAgentScope: workflowChildDefaults.agentScope, claimedOutputPaths, entries: calls });
4237
+ const outputClaims = workflowChildOutputClaims({ ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, workflowRunId, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: deps.discoverAgents, workflowAgentScope: workflowChildDefaults.agentScope, state: deps.state, claimedOutputPaths, entries: calls });
4096
4238
  if (outputClaims.error) throw new Error(outputClaims.error);
4097
4239
  status.runFanoutBudget = claimRunFanoutBatch(workflowFanoutBudget, calls.map(({ key }) => `workflow[${key}]`));
4098
4240
  if (outputClaims.claims) applyWorkflowChildOutputClaims(claimedOutputPaths, outputClaims.claims);
4099
- persist();
4241
+ if (outputClaims.overrides) for (const [key, output] of outputClaims.overrides) childOutputOverrides.set(key, output);
4242
+ persist({ tolerateStatusWriteFailure: true });
4100
4243
  },
4101
4244
  onEmit: (emits) => {
4102
4245
  // Each emit is validated at the host boundary in runWorkflowScript before onEmit fires.
4103
4246
  status.workflow = { ...(status.workflow ?? { trace: [], console: [] }), emits };
4104
- persist();
4247
+ persist({ tolerateStatusWriteFailure: true });
4105
4248
  appendWorkflowEvent({ type: "subagent.workflow.emit", value: emits.at(-1) });
4106
4249
  },
4107
4250
  launch: async (key, childParams, workflowSignal, admission) => {
@@ -4119,7 +4262,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4119
4262
  });
4120
4263
  const result = await runMissionWorkflowChild(missionBinding, workflowRunId, key, childPhase, () => {
4121
4264
  const childRequest = bindMissionWorkflowChildAsyncLaunch(
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 },
4265
+ { ...prepareWorkflowChildLaunchParams({ workflowDefaults: workflowChildDefaults, childParams, parentWorkflowRunId: workflowRunId, workflowKey: key, ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: deps.discoverAgents, workflowAgentScope: workflowChildDefaults.agentScope, outputOverride: childOutputOverrides.get(key), options: { missionDetached: detachWorkflowChildMissions, runFanoutBudget: workflowFanoutBudget, parentDeadlineAt: workflowDeadlineAt } }), runFanoutAdmitted: admission.admitted },
4123
4266
  missionBinding,
4124
4267
  deps.asyncByDefault,
4125
4268
  );
@@ -4130,7 +4273,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4130
4273
  step.sessionFile = launch.sessionFile;
4131
4274
  step.async = launch.async;
4132
4275
  if (launch.runId) step.runId = launch.runId;
4133
- persist();
4276
+ persist({ tolerateStatusWriteFailure: true });
4134
4277
  }
4135
4278
  recordMissionWorkflowChild(missionBinding, workflowRunId, key, { status: "running", agent: launch.agent, ...(launch.sessionFile ? { sessionPath: launch.sessionFile } : {}) });
4136
4279
  });
@@ -4153,7 +4296,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4153
4296
  step.thinking = progress.thinking;
4154
4297
  step.error = progress.error;
4155
4298
  projectWorkflowActivity();
4156
- persist();
4299
+ persist({ tolerateStatusWriteFailure: true });
4157
4300
  recordMissionWorkflowChild(missionBinding, workflowRunId, key, {
4158
4301
  status: step.status,
4159
4302
  heartbeat: { status: step.status, ...(childPhase ? { phase: childPhase } : {}) },
@@ -4198,8 +4341,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4198
4341
  const resultSummary = appendWorkflowOutputWarning(summary, outputWarning);
4199
4342
  const workflowUsage = sumResultsUsage(workflowResults);
4200
4343
  status = { ...status, state: "complete", endedAt: Date.now(), workflow: { value: workflow.value, trace: workflow.trace, emits: workflow.emits, console: workflow.console }, totalTokens: { input: workflowUsage.input, output: workflowUsage.output, total: workflowUsage.input + workflowUsage.output }, totalCost: sumResultsCost(workflowResults) };
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;
4344
+ if (!writeWorkflowResult({ id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: true, state: "complete", summary: resultSummary, output: resultSummary, results: workflow.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: workflowCwd, sessionId: currentSessionId, completionOwnerId, ...(requestParams.scheduleOrigin ? { scheduleOrigin: requestParams.scheduleOrigin } : {}), timestamp: Date.now(), durationMs: Date.now() - startedAt })) return;
4202
4345
  persist();
4346
+ persistClosed = true;
4203
4347
  appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, ...(status.error ? { error: status.error } : {}) });
4204
4348
  } catch (error) {
4205
4349
  const partial = error instanceof WorkflowScriptError ? error.partial : { trace: [], emits: [], console: [], children: [] };
@@ -4210,17 +4354,31 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4210
4354
  const state = stopped ? "stopped" : pauseForDetached ? "paused" : "failed";
4211
4355
  for (const step of status.steps ?? []) {
4212
4356
  if (step.workflowKey && detachedChildKeys.has(step.workflowKey)) {
4357
+ if (step.status === "completed" || step.status === "complete" || step.status === "failed") continue;
4213
4358
  step.status = "paused";
4214
4359
  step.activityState = "needs_attention";
4360
+ } else if (pauseForDetached && step.status === "running") {
4361
+ step.status = "stopped";
4362
+ step.stopped = true;
4363
+ delete step.activityState;
4215
4364
  }
4216
4365
  }
4217
4366
  status = compactOptional<AsyncStatus>({ ...status, state, stopped: stopped || undefined, activityState: pauseForDetached ? "needs_attention" : undefined, error: error instanceof Error ? error.message : String(error), endedAt: Date.now(), workflow: { trace: partial.trace, emits: partial.emits, console: partial.console } });
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;
4367
+ if (pauseForDetached) {
4368
+ const promoted = promotePausedWorkflowIfSettled(status);
4369
+ if (promoted) status = promoted;
4370
+ }
4371
+ const terminalSummary = status.state === "complete"
4372
+ ? "Workflow completed after detached child finished."
4373
+ : status.error ?? (pauseForDetached ? "Workflow paused." : "Workflow failed.");
4374
+ const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, terminalSummary, producedChildOutputPaths);
4375
+ const resultSummary = appendWorkflowOutputWarning(terminalSummary, outputWarning);
4376
+ if (!writeWorkflowResult({ id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: status.state === "complete", state: status.state, summary: resultSummary, error: status.state === "complete" ? undefined : status.error, stopped: status.stopped, activityState: status.activityState, results: partial.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.detached && status.state !== "complete" ? { detached: true } : {}), ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: workflowCwd, sessionId: currentSessionId, completionOwnerId, ...(requestParams.scheduleOrigin ? { scheduleOrigin: requestParams.scheduleOrigin } : {}), timestamp: Date.now(), durationMs: Date.now() - startedAt })) return;
4221
4377
  persist();
4222
- appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, error: status.error, ...(status.activityState ? { activityState: status.activityState } : {}) });
4378
+ persistClosed = true;
4379
+ appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, ...(status.error ? { error: status.error } : {}), ...(status.activityState ? { activityState: status.activityState } : {}) });
4223
4380
  } finally {
4381
+ persistClosed = true;
4224
4382
  deps.state.workflowControllers?.delete(workflowRunId);
4225
4383
  deps.state.activeAsyncCapacity = workflowCapacity?.reconcile(new Set(deps.state.workflowControllers?.keys() ?? []))
4226
4384
  ?? deps.state.activeAsyncCapacity;
@@ -4236,6 +4394,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4236
4394
  const configuredOutputBaseDir = resolveConfiguredSingleRunOutputBaseDir(deps);
4237
4395
  const workflowAggregateOutputPath = resolveWorkflowAggregateOutputPath(workflowOutput, ctx.cwd, workflowCwd, resolveSingleRunOutputBaseDir(deps, workflowArtifactsDir, _id), configuredOutputBaseDir);
4238
4396
  const claimedOutputPaths = new Map<string, string>();
4397
+ const childOutputOverrides = new Map<string, string>();
4239
4398
  const producedChildOutputPaths = new Set<string>();
4240
4399
  const workflowResults: SingleResult[] = [];
4241
4400
  const workflowChildRunIds = new Map<string, string>();
@@ -4256,10 +4415,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4256
4415
  sendWorkflowProgress();
4257
4416
  },
4258
4417
  admit: (calls) => {
4259
- const outputClaims = workflowChildOutputClaims({ ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, workflowRunId: _id, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: deps.discoverAgents, workflowAgentScope: workflowChildDefaults.agentScope, claimedOutputPaths, entries: calls });
4418
+ const outputClaims = workflowChildOutputClaims({ ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, workflowRunId: _id, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: deps.discoverAgents, workflowAgentScope: workflowChildDefaults.agentScope, state: deps.state, claimedOutputPaths, entries: calls });
4260
4419
  if (outputClaims.error) throw new Error(outputClaims.error);
4261
4420
  claimRunFanoutBatch(workflowFanoutBudget, calls.map(({ key }) => `workflow[${key}]`));
4262
4421
  if (outputClaims.claims) applyWorkflowChildOutputClaims(claimedOutputPaths, outputClaims.claims);
4422
+ if (outputClaims.overrides) for (const [key, output] of outputClaims.overrides) childOutputOverrides.set(key, output);
4263
4423
  },
4264
4424
  onEmit: (emits) => {
4265
4425
  liveWorkflow = { ...liveWorkflow, emits };
@@ -4280,7 +4440,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4280
4440
  });
4281
4441
  const result = await runMissionWorkflowChild(missionBinding, _id, key, childPhase, () => {
4282
4442
  const childRequest = bindMissionWorkflowChildAsyncLaunch(
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 },
4443
+ { ...prepareWorkflowChildLaunchParams({ workflowDefaults: workflowChildDefaults, childParams, parentWorkflowRunId: _id, workflowKey: key, ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: deps.discoverAgents, workflowAgentScope: workflowChildDefaults.agentScope, outputOverride: childOutputOverrides.get(key), options: { missionDetached: detachWorkflowChildMissions, suppressRoutineResultIntercom: chatProgress.mode === "live-card", runFanoutBudget: workflowFanoutBudget, parentDeadlineAt: workflowDeadlineAt } }), runFanoutAdmitted: admission.admitted },
4284
4444
  missionBinding,
4285
4445
  deps.asyncByDefault,
4286
4446
  );
@@ -5018,30 +5178,33 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5018
5178
 
5019
5179
  let forkSessionFileForIndex: (idx?: number) => string | undefined = () => undefined;
5020
5180
  let forkThinkingOverrideForIndex: (idx?: number) => AgentConfig["thinking"] | undefined = () => undefined;
5021
- let prepareForkThinking = (_agentName: string, _index: number, _modelOverride?: string): void => {};
5181
+ let prepareForkThinking = (_agentName: string, _index: number, _modelOverride?: string, _modelOverrideFromParent?: boolean): void => {};
5022
5182
  const forkThinkingRequirements = new Map<number, boolean>();
5023
5183
  const forkThinkingDowngrades = new Map<number, string>();
5024
5184
  try {
5025
5185
  const forkAvailableModels = contextPolicy.usesFork ? ctx.modelRegistry.getAvailable().map(toModelInfo) : [];
5026
5186
  const parentModel = requestParentModel;
5027
- prepareForkThinking = (agentName, index, modelOverride) => {
5187
+ prepareForkThinking = (agentName, index, modelOverride, modelOverrideFromParent) => {
5028
5188
  const agentConfig = agents.find((agent) => agent.name === agentName);
5029
5189
  if (agentConfig?.runner?.type === "external-cli" || agentConfig?.runner?.type === "external-job") {
5030
5190
  forkThinkingRequirements.set(index, true);
5031
5191
  return;
5032
5192
  }
5033
- const primaryModel = resolveEffectiveSubagentModel(
5034
- modelOverride,
5035
- agentConfig?.model,
5036
- parentModel,
5037
- forkAvailableModels,
5038
- parentModel?.provider,
5039
- );
5193
+ const primaryModel = modelOverrideFromParent
5194
+ ? modelOverride
5195
+ : resolveEffectiveSubagentModel(
5196
+ modelOverride,
5197
+ agentConfig?.model,
5198
+ parentModel,
5199
+ forkAvailableModels,
5200
+ parentModel?.provider,
5201
+ );
5040
5202
  const candidates = buildModelCandidates(
5041
5203
  primaryModel,
5042
5204
  agentConfig?.fallbackModels,
5043
5205
  forkAvailableModels,
5044
5206
  parentModel?.provider,
5207
+ { primaryModelFromParent: modelOverrideFromParent ?? inheritsParentModel(modelOverride, agentConfig?.model, parentModel) },
5045
5208
  );
5046
5209
  forkThinkingRequirements.set(
5047
5210
  index,
@@ -5153,20 +5316,20 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5153
5316
  }
5154
5317
  const sessionDirForIndex = (idx?: number) =>
5155
5318
  path.join(sessionRoot, `run-${idx ?? 0}`);
5156
- const forkSessionFileForTask: ForkSessionFileForTask = (agentName, idx = 0, modelOverride) => {
5319
+ const forkSessionFileForTask: ForkSessionFileForTask = (agentName, idx = 0, modelOverride, modelOverrideFromParent) => {
5157
5320
  if (!shouldForkAgent(contextPolicy, agentName)) return undefined;
5158
- prepareForkThinking(agentName, idx, modelOverride);
5321
+ prepareForkThinking(agentName, idx, modelOverride, modelOverrideFromParent);
5159
5322
  return forkSessionFileForIndex(idx);
5160
5323
  };
5161
- const forkThinkingOverrideForTask: ForkThinkingOverrideForTask = (agentName, idx = 0, modelOverride) => {
5324
+ const forkThinkingOverrideForTask: ForkThinkingOverrideForTask = (agentName, idx = 0, modelOverride, modelOverrideFromParent) => {
5162
5325
  if (!shouldForkAgent(contextPolicy, agentName)) return delegatedThinkingOverride;
5163
- prepareForkThinking(agentName, idx, modelOverride);
5326
+ prepareForkThinking(agentName, idx, modelOverride, modelOverrideFromParent);
5164
5327
  const override = forkThinkingOverrideForIndex(idx);
5165
5328
  if (override === "off") forkThinkingDowngrades.set(idx, agentName);
5166
5329
  return override ?? delegatedThinkingOverride;
5167
5330
  };
5168
- const childSessionFileForTask: ForkSessionFileForTask = (agentName, idx, modelOverride) =>
5169
- forkSessionFileForTask(agentName, idx, modelOverride) ?? path.join(sessionDirForIndex(idx), "session.jsonl");
5331
+ const childSessionFileForTask: ForkSessionFileForTask = (agentName, idx, modelOverride, modelOverrideFromParent) =>
5332
+ forkSessionFileForTask(agentName, idx, modelOverride, modelOverrideFromParent) ?? path.join(sessionDirForIndex(idx), "session.jsonl");
5170
5333
  const childSessionFileForIndex = (idx?: number) =>
5171
5334
  path.join(sessionDirForIndex(idx), "session.jsonl");
5172
5335
  try {
@@ -5522,7 +5685,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
5522
5685
  onUpdate: ((r: AgentToolResult<Details>) => void) | undefined,
5523
5686
  ctx: ExtensionContext,
5524
5687
  ): Promise<AgentToolResult<Details>> => {
5525
- const normalized = normalizePublicSubagentExecution(params);
5688
+ const normalized = normalizePublicSubagentExecution(params, { asyncByDefault: deps.asyncByDefault });
5526
5689
  if (!normalized.ok) {
5527
5690
  return Promise.resolve({ content: [{ type: "text", text: normalized.error }], isError: true, details: { mode: normalized.mode, results: [] } });
5528
5691
  }