pi-subagents 0.40.0 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +246 -525
  3. package/agents/oracle.md +1 -0
  4. package/package.json +12 -4
  5. package/prompts/parallel-context-build.md +1 -1
  6. package/prompts/parallel-handoff-plan.md +1 -1
  7. package/prompts/review-loop.md +1 -1
  8. package/skills/pi-subagents/SKILL.md +6 -6
  9. package/skills/pi-subagents/references/constraints-and-recipes.md +19 -26
  10. package/skills/pi-subagents/references/execution-controls.md +98 -97
  11. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  12. package/skills/pi-subagents/references/prompting-and-roles.md +18 -27
  13. package/src/agents/agent-management.ts +155 -65
  14. package/src/agents/agent-serializer.ts +19 -0
  15. package/src/agents/agents.ts +154 -71
  16. package/src/agents/chain-serializer.ts +10 -7
  17. package/src/agents/frontmatter.ts +5 -3
  18. package/src/agents/identity.ts +1 -1
  19. package/src/agents/proactive-skills.ts +13 -10
  20. package/src/agents/skills.ts +23 -6
  21. package/src/api/control-channel.ts +4 -0
  22. package/src/api/delegation.ts +26 -194
  23. package/src/api/external-runs.ts +129 -0
  24. package/src/api/intercom-bridge.ts +3 -0
  25. package/src/api/pi-args.ts +5 -0
  26. package/src/api/preflight.ts +3 -3
  27. package/src/api/shared-types.ts +19 -0
  28. package/src/extension/config.ts +10 -0
  29. package/src/extension/control-notices.ts +5 -39
  30. package/src/extension/doctor.ts +10 -9
  31. package/src/extension/fanout-child.ts +7 -4
  32. package/src/extension/index.ts +232 -68
  33. package/src/extension/rpc.ts +18 -12
  34. package/src/extension/schemas.ts +48 -37
  35. package/src/extension/tool-description.ts +36 -86
  36. package/src/inspectors/herdr/actions.ts +229 -0
  37. package/src/inspectors/herdr/client.ts +130 -0
  38. package/src/inspectors/herdr/inspector-runner.ts +141 -0
  39. package/src/inspectors/herdr/project-panes.ts +154 -0
  40. package/src/integrations/herdr-status.ts +330 -0
  41. package/src/intercom/intercom-bridge.ts +3 -2
  42. package/src/intercom/result-intercom.ts +5 -1
  43. package/src/missions/actions.ts +372 -0
  44. package/src/missions/lifecycle.ts +314 -0
  45. package/src/missions/store.ts +442 -0
  46. package/src/missions/types.ts +135 -0
  47. package/src/policy/authority.ts +46 -0
  48. package/src/profiles/profiles.ts +29 -3
  49. package/src/runs/background/async-execution.ts +98 -49
  50. package/src/runs/background/async-job-tracker.ts +10 -2
  51. package/src/runs/background/async-resume.ts +6 -6
  52. package/src/runs/background/async-status.ts +29 -1
  53. package/src/runs/background/auto-drain.ts +3 -3
  54. package/src/runs/background/chain-append.ts +3 -2
  55. package/src/runs/background/control-channel.ts +9 -7
  56. package/src/runs/background/fleet-view.ts +3 -4
  57. package/src/runs/background/notify.ts +2 -1
  58. package/src/runs/background/process-terminal.ts +5 -5
  59. package/src/runs/background/result-watcher.ts +13 -5
  60. package/src/runs/background/run-id-resolver.ts +3 -3
  61. package/src/runs/background/run-status.ts +35 -8
  62. package/src/runs/background/scheduled-runs.ts +602 -375
  63. package/src/runs/background/stale-run-reconciler.ts +3 -3
  64. package/src/runs/background/subagent-runner.ts +608 -445
  65. package/src/runs/background/subagent-wait.ts +50 -9
  66. package/src/runs/background/wait-subscriptions.ts +253 -0
  67. package/src/runs/background/wait-tool.ts +12 -4
  68. package/src/runs/foreground/async-steering-action.ts +3 -3
  69. package/src/runs/foreground/chain-clarify.ts +8 -4
  70. package/src/runs/foreground/chain-execution.ts +56 -30
  71. package/src/runs/foreground/execution.ts +15 -2
  72. package/src/runs/foreground/subagent-executor.ts +1023 -273
  73. package/src/runs/shared/acceptance.ts +28 -6
  74. package/src/runs/shared/child-protocol.ts +302 -22
  75. package/src/runs/shared/dynamic-fanout.ts +1 -1
  76. package/src/runs/shared/external-cli-runner.ts +130 -0
  77. package/src/runs/shared/long-running-guard.ts +42 -1
  78. package/src/runs/shared/nested-events.ts +59 -5
  79. package/src/runs/shared/nested-render.ts +9 -4
  80. package/src/runs/shared/parallel-handoff.ts +86 -2
  81. package/src/runs/shared/parallel-utils.ts +11 -2
  82. package/src/runs/shared/permissions.ts +95 -0
  83. package/src/runs/shared/pi-args.ts +11 -1
  84. package/src/runs/shared/pi-spawn.ts +11 -1
  85. package/src/runs/shared/run-history.ts +1 -1
  86. package/src/runs/shared/subagent-prompt-runtime.ts +36 -5
  87. package/src/runs/shared/subagent-startup-retry.ts +5 -2
  88. package/src/runs/shared/turn-budget.ts +6 -6
  89. package/src/runs/shared/worktree.ts +122 -12
  90. package/src/shared/accessible-dir.ts +29 -7
  91. package/src/shared/artifacts.ts +18 -1
  92. package/src/shared/fork-context.ts +3 -2
  93. package/src/shared/launch-contract.ts +1 -0
  94. package/src/shared/settings.ts +10 -0
  95. package/src/shared/types.ts +156 -14
  96. package/src/shared/utils.ts +8 -6
  97. package/src/slash/delegation-adapters.ts +32 -194
  98. package/src/slash/delegation-request.ts +43 -126
  99. package/src/slash/prompt-template-bridge.ts +158 -205
  100. package/src/slash/prompt-workflows.ts +21 -57
  101. package/src/slash/slash-bridge.ts +14 -0
  102. package/src/slash/slash-commands.ts +31 -632
  103. package/src/slash/subagents-admin.ts +18 -14
  104. package/src/tui/fleet-status.ts +156 -21
  105. package/src/tui/fleet-transcript.ts +110 -5
  106. package/src/tui/fleet.ts +56 -24
  107. package/src/tui/render.ts +291 -109
  108. package/src/types/pi-runtime-compat.d.ts +14 -0
  109. package/src/watchdog/lsp-diagnostics.ts +12 -7
  110. package/src/watchdog/model-selection.ts +2 -2
  111. package/src/watchdog/permission-arbiter.ts +145 -0
  112. package/src/watchdog/register-child.ts +1 -1
  113. package/src/watchdog/register-main.ts +1 -1
  114. package/src/watchdog/review.ts +4 -1
  115. package/src/watchdog/runtime.ts +3 -2
  116. package/src/workflows/chat-progress.ts +140 -0
  117. package/src/workflows/scripted-workflow.ts +415 -0
  118. package/agents/advisor.md +0 -73
  119. package/src/extension/chain-validation.ts +0 -181
@@ -7,12 +7,14 @@ import type { Message } from "@earendil-works/pi-ai";
7
7
  import { writeAtomicJson } from "../../shared/atomic-json.ts";
8
8
  import { createChildTranscriptWriter, type ChildTranscriptWriter } from "../../shared/child-transcript.ts";
9
9
  import { closeSteerInbox, consumeInterruptRequest, consumeSteerRequests, deliverInterruptRequest, deliverStopRequest, deliverTimeoutRequest, enqueueStepSteer, steerAcksDir, steerCapabilityPath, stepSteerInboxDir, watchAsyncControlInbox, type SteerAck, type SteerCapability, type SteerRequest } from "./control-channel.ts";
10
- import { appendJsonl as appendRawJsonl, formatOutputArtifactContent, getArtifactPaths } from "../../shared/artifacts.ts";
10
+ import { appendJsonl as appendRawJsonl, formatOutputArtifactContent, getArtifactPaths, writeArtifact, writeMetadata } from "../../shared/artifacts.ts";
11
11
  import { PI_CODING_AGENT_PACKAGE, getPiSpawnCommand, resolveInstalledPiPackageRoot } from "../shared/pi-spawn.ts";
12
12
  import { captureSingleOutputSnapshot, extractChildWrittenOutput, finalizeSingleOutput, formatSavedOutputReference, injectOutputPathSystemPrompt, injectSingleOutputInstruction, resolveSingleOutput, type SingleOutputSnapshot } from "../shared/single-output.ts";
13
13
  import {
14
14
  type ActivityState,
15
15
  type ArtifactConfig,
16
+ type ExternalCliRunnerStatus,
17
+ type ExternalProcessStatus,
16
18
  type ArtifactPaths,
17
19
  type AsyncParallelGroupStatus,
18
20
  type AsyncStatus,
@@ -118,10 +120,11 @@ import { appendRunnerStepsToStatus, consumeChainAppendRequests, countPendingChai
118
120
  import { appendTurnBudgetSystemPrompt, formatTurnBudgetOutput, initialTurnBudgetState, turnBudgetDecision, turnBudgetDeferredNote, turnBudgetDeferredState, turnBudgetExceededMessage, turnBudgetSoftNote, turnBudgetState } from "../shared/turn-budget.ts";
119
121
  import { initialToolBudgetState, toolBudgetState } from "../shared/tool-budget.ts";
120
122
  import { usageBudgetExceededMessage, usageBudgetState } from "../shared/usage-budget.ts";
121
- import { formatParallelHandoffError, formatParallelHandoffReference, parallelHandoffPath, writeParallelHandoffGroup } from "../shared/parallel-handoff.ts";
123
+ import { formatParallelHandoffError, formatParallelHandoffReference, parallelHandoffPath, writeParallelHandoffGroup, writePendingParallelHandoff } from "../shared/parallel-handoff.ts";
122
124
  import { resolveWatchdogConfig } from "../../watchdog/settings.ts";
123
- import { createBoundedByteTail, createBoundedLineReader, formatProtocolOutputLimit, MAX_CHILD_STDERR_BYTES, projectChildLifecycle, type ChildLifecycleAction, type ProtocolOutputLimit } from "../shared/child-protocol.ts";
125
+ import { createBoundedByteTail, createBoundedLineReader, formatProtocolOutputLimit, MAX_CHILD_STDERR_BYTES, PI_AGGREGATE_EVENT_PROJECTOR, projectChildLifecycle, type ChildLifecycleAction, type ProtocolOutputLimit } from "../shared/child-protocol.ts";
124
126
  import { acquireSessionLease, type SessionLeaseRequest } from "../shared/session-lease.ts";
127
+ import { buildExternalCliPrompt, runExternalCli } from "../shared/external-cli-runner.ts";
125
128
  import { decodeSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
126
129
  import {
127
130
  CHILD_WATCHDOG_CONFIG_ENV,
@@ -157,6 +160,7 @@ interface SubagentRunConfig {
157
160
  controlIntercomTarget?: string;
158
161
  childIntercomTargets?: Array<string | undefined>;
159
162
  resultMode?: SubagentRunMode;
163
+ mode?: SubagentRunMode;
160
164
  dynamicFanoutMaxItems?: number;
161
165
  workflowGraph?: WorkflowGraphSnapshot;
162
166
  nestedRoute?: NestedRouteInfo;
@@ -175,6 +179,8 @@ interface SubagentRunConfig {
175
179
  launchResolvedExtensions?: LaunchResolvedChildExtensionsV1;
176
180
  runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
177
181
  runnerProcessInstanceId?: string;
182
+ parentWorkflowRunId?: string;
183
+ workflowKey?: string;
178
184
  }
179
185
 
180
186
  interface StepResult {
@@ -188,8 +194,10 @@ interface StepResult {
188
194
  outputState?: SubagentOutputState;
189
195
  error?: string;
190
196
  protocolError?: ProtocolOutputLimit;
191
- success: boolean;
192
- exitCode?: number | null;
197
+ success?: boolean;
198
+ exitCode: number | null;
199
+ usage?: Usage;
200
+ savedOutputPath?: string;
193
201
  skipped?: boolean;
194
202
  interrupted?: boolean;
195
203
  timedOut?: boolean;
@@ -207,6 +215,8 @@ interface StepResult {
207
215
  modelAttempts?: ModelAttempt[];
208
216
  totalCost?: CostSummary;
209
217
  artifactPaths?: ArtifactPaths;
218
+ outputSaveError?: string;
219
+ metadataSaveError?: string;
210
220
  truncated?: boolean;
211
221
  transcriptPath?: string;
212
222
  transcriptError?: string;
@@ -222,6 +232,32 @@ interface StepResult {
222
232
  watchdog?: import("../../shared/types.ts").ChildWatchdogProgress;
223
233
  writerProcesses?: Array<{ processInstanceId: string; kind: "pi-writer"; attempt: number; closeObservedAt: number; exitCode: number | null; signal: string | null }>;
224
234
  writerAttemptCount?: number;
235
+ runner?: ExternalCliRunnerStatus;
236
+ externalProcess?: ExternalProcessStatus;
237
+ }
238
+
239
+ function persistStepArtifacts(input: {
240
+ artifactPaths: ArtifactPaths;
241
+ artifactConfig?: Partial<ArtifactConfig>;
242
+ output: string;
243
+ metadata: object;
244
+ }): { outputSaveError?: string; metadataSaveError?: string } {
245
+ const errors: { outputSaveError?: string; metadataSaveError?: string } = {};
246
+ if (input.artifactConfig?.includeOutput !== false) {
247
+ try {
248
+ writeArtifact(input.artifactPaths.outputPath, input.output);
249
+ } catch (error) {
250
+ errors.outputSaveError = `Artifact output post-processing failed: ${error instanceof Error ? error.message : String(error)}`;
251
+ }
252
+ }
253
+ if (input.artifactConfig?.includeMetadata !== false) {
254
+ try {
255
+ writeMetadata(input.artifactPaths.metadataPath, input.metadata);
256
+ } catch (error) {
257
+ errors.metadataSaveError = `Artifact metadata post-processing failed: ${error instanceof Error ? error.message : String(error)}`;
258
+ }
259
+ }
260
+ return errors;
225
261
  }
226
262
 
227
263
  const ASYNC_INTERRUPT_SIGNAL: NodeJS.Signals = process.platform === "win32" ? "SIGBREAK" : "SIGUSR2";
@@ -373,11 +409,44 @@ function isTerminalAssistantStop(message: Message): boolean {
373
409
  return (message as { stopReason?: string }).stopReason === "stop" && !assistantStartsToolCall(message);
374
410
  }
375
411
 
412
+ type UndefinedOmitted<T extends object> = {
413
+ [K in keyof T]: Exclude<T[K], undefined>;
414
+ };
415
+
416
+ function omitUndefinedProperties<const T extends object>(value: T): UndefinedOmitted<T> {
417
+ for (const key in value) {
418
+ if (value[key] === undefined) delete value[key];
419
+ }
420
+ return value as UndefinedOmitted<T>;
421
+ }
422
+
423
+ type WithUndefinedOptionals<T extends object> = {
424
+ [K in keyof T]: {} extends Pick<T, K> ? T[K] | undefined : T[K];
425
+ };
426
+
427
+ type RequiredKeysAllowingUndefined<T extends object> = {
428
+ [K in keyof T]-?: {} extends Pick<T, K> ? never : undefined extends T[K] ? K : never;
429
+ }[keyof T];
430
+
431
+ function compactOptional<T extends object>(
432
+ value: WithUndefinedOptionals<T> & (RequiredKeysAllowingUndefined<T> extends never ? unknown : never),
433
+ ): T {
434
+ for (const key of Object.keys(value) as Array<keyof T>) {
435
+ if (value[key] === undefined) delete value[key];
436
+ }
437
+ return value as T;
438
+ }
439
+
440
+ function setOptionalProperty<T extends object, K extends keyof T>(target: T, key: K, value: T[K]): void {
441
+ if (value === undefined) delete target[key];
442
+ else target[key] = value;
443
+ }
444
+
376
445
  function resetStepLiveDetail(step: RunnerStatusStep): void {
377
- step.currentTool = undefined;
378
- step.currentToolArgs = undefined;
379
- step.currentToolStartedAt = undefined;
380
- step.currentPath = undefined;
446
+ delete step.currentTool;
447
+ delete step.currentToolArgs;
448
+ delete step.currentToolStartedAt;
449
+ delete step.currentPath;
381
450
  step.recentTools = [];
382
451
  step.recentOutput = [];
383
452
  }
@@ -549,7 +618,7 @@ function runPiStreaming(
549
618
  return;
550
619
  }
551
620
 
552
- appendChildEvent(event);
621
+ appendChildEvent(event as unknown as Record<string, unknown>);
553
622
  transcriptWriter?.writeChildEvent(event);
554
623
  if (event.type === "agent_settled") agentSettledReceived = true;
555
624
  applyChildLifecycle(projectChildLifecycle(event));
@@ -559,9 +628,11 @@ function runPiStreaming(
559
628
  const next = acceptChildWatchdogEvent({
560
629
  current: childWatchdogState,
561
630
  event,
562
- runId: childEventContext?.runId,
563
- agent: childEventContext?.agent,
564
- childIndex: childEventContext?.stepIndex,
631
+ ...(childEventContext ? {
632
+ runId: childEventContext.runId,
633
+ agent: childEventContext.agent,
634
+ childIndex: childEventContext.stepIndex,
635
+ } : {}),
565
636
  });
566
637
  if (!next) return;
567
638
  updateChildWatchdogState(next);
@@ -663,7 +734,11 @@ function runPiStreaming(
663
734
  protocolHardKillTimer.unref?.();
664
735
  }
665
736
  };
666
- const stdoutReader = createBoundedLineReader({ onLine: processStdoutLine, onLimit: failProtocol });
737
+ const stdoutReader = createBoundedLineReader({
738
+ oversizedLineProjector: PI_AGGREGATE_EVENT_PROJECTOR,
739
+ onLine: processStdoutLine,
740
+ onLimit: failProtocol,
741
+ });
667
742
  const stderrReader = createBoundedLineReader({
668
743
  stream: "stderr",
669
744
  maxPendingLineBytes: MAX_CHILD_STDERR_BYTES,
@@ -829,7 +904,7 @@ function runPiStreaming(
829
904
  turnBudgetExceeded,
830
905
  forcedDrainAfterFinalSuccess,
831
906
  }) ? formatProcessSignalError(signal!) : undefined;
832
- resolve({
907
+ resolve(omitUndefinedProperties({
833
908
  stderr,
834
909
  exitCode: timedOut || stopped ? 1 : turnBudgetExceeded ? 1 : interrupted || forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
835
910
  messages,
@@ -852,7 +927,7 @@ function runPiStreaming(
852
927
  processInstanceId,
853
928
  processCloseObservedAt,
854
929
  processSignal: signal,
855
- });
930
+ }));
856
931
  });
857
932
 
858
933
  child.on("error", (spawnError) => {
@@ -874,7 +949,7 @@ function runPiStreaming(
874
949
  const stderr = stderrTail.text();
875
950
  const finalOutput = getFinalOutput(messages) || rawStdoutTail.text().trim();
876
951
  const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
877
- resolve({ stderr, exitCode: 1, messages, usage, toolCount, durationMs: Date.now() - startedAt, model, error: stopped ? (stopMessage ?? "Subagent stopped by user.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : error ?? assistantError ?? spawnErrorMessage, protocolError, finalOutput: (timedOut || stopped) && !finalOutput.trim() ? (stopped ? stopMessage ?? "Subagent stopped by user." : timeoutMessage ?? "Subagent timed out.") : finalOutput, outputState: finalOutput.trim() ? "present" : "absent", timedOut, stopped, turnBudget, turnBudgetExceeded, wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudget?.outcome === "termination-deferred" || turnBudgetExceeded || undefined, observedMutationAttempt, watchdog: childWatchdogState, processInstanceId });
952
+ resolve(omitUndefinedProperties({ stderr, exitCode: 1, messages, usage, toolCount, durationMs: Date.now() - startedAt, model, error: stopped ? (stopMessage ?? "Subagent stopped by user.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : error ?? assistantError ?? spawnErrorMessage, protocolError, finalOutput: (timedOut || stopped) && !finalOutput.trim() ? (stopped ? stopMessage ?? "Subagent stopped by user." : timeoutMessage ?? "Subagent timed out.") : finalOutput, outputState: finalOutput.trim() ? "present" : "absent", timedOut, stopped, turnBudget, turnBudgetExceeded, wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudget?.outcome === "termination-deferred" || turnBudgetExceeded || undefined, observedMutationAttempt, watchdog: childWatchdogState, processInstanceId }));
878
953
  });
879
954
  });
880
955
  }
@@ -1021,6 +1096,7 @@ interface SingleStepContext {
1021
1096
  onAttemptStart?: (attempt: { model?: string; thinking?: string }) => void;
1022
1097
  onChildEvent?: (event: ChildEvent) => void;
1023
1098
  onWriterProcess?: (writer: { state: "none" | "spawning" } | { state: "running"; pid: number }) => void;
1099
+ onExternalProcess?: (process: ExternalProcessStatus) => void;
1024
1100
  skipAcceptance?: () => boolean;
1025
1101
  }
1026
1102
 
@@ -1028,42 +1104,7 @@ interface SingleStepContext {
1028
1104
  async function runSingleStep(
1029
1105
  step: SubagentStep,
1030
1106
  ctx: SingleStepContext,
1031
- ): Promise<{
1032
- agent: string;
1033
- context?: "fresh" | "fork";
1034
- agentContract?: import("../../shared/types.ts").AgentContract;
1035
- launchContractDigest?: string;
1036
- launchResolvedExtensions?: LaunchResolvedChildExtensionsV1;
1037
- runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
1038
- output: string;
1039
- exitCode: number | null;
1040
- error?: string;
1041
- model?: string;
1042
- attemptedModels?: string[];
1043
- modelAttempts?: ModelAttempt[];
1044
- artifactPaths?: ArtifactPaths;
1045
- transcriptPath?: string;
1046
- transcriptError?: string;
1047
- interrupted?: boolean;
1048
- timedOut?: boolean;
1049
- stopped?: boolean;
1050
- turnBudget?: TurnBudgetState;
1051
- turnBudgetExceeded?: boolean;
1052
- wrapUpRequested?: boolean;
1053
- toolBudget?: ToolBudgetState;
1054
- toolBudgetBlocked?: boolean;
1055
- sessionFile?: string;
1056
- intercomTarget?: string;
1057
- completionGuardTriggered?: boolean;
1058
- effects?: import("../../shared/types.ts").EffectsProjection;
1059
- execution?: import("../../shared/types.ts").ExecutionProjection;
1060
- review?: import("../../shared/types.ts").ReviewProjection;
1061
- structuredOutput?: unknown;
1062
- structuredOutputPath?: string;
1063
- structuredOutputSchemaPath?: string;
1064
- acceptance?: import("../../shared/types.ts").AcceptanceLedger;
1065
- writerAttemptCount?: number;
1066
- }> {
1107
+ ): Promise<StepResult & { completionGuardTriggered?: boolean }> {
1067
1108
  if (step.importAsyncRoot) {
1068
1109
  let importTimedOut = false;
1069
1110
  let importStopped = false;
@@ -1076,7 +1117,7 @@ async function runSingleStep(
1076
1117
  pid = undefined;
1077
1118
  }
1078
1119
  try {
1079
- deliverTimeoutRequest({ asyncDir: step.importAsyncRoot!.asyncDir, pid, source: "ancestor-timeout" });
1120
+ deliverTimeoutRequest(omitUndefinedProperties({ asyncDir: step.importAsyncRoot!.asyncDir, pid, source: "ancestor-timeout" }));
1080
1121
  } catch {
1081
1122
  // The parent runner's own timeout result is authoritative for the attached step.
1082
1123
  }
@@ -1090,16 +1131,16 @@ async function runSingleStep(
1090
1131
  pid = undefined;
1091
1132
  }
1092
1133
  try {
1093
- deliverStopRequest({ asyncDir: step.importAsyncRoot!.asyncDir, pid, source: "ancestor-stop" });
1134
+ deliverStopRequest(omitUndefinedProperties({ asyncDir: step.importAsyncRoot!.asyncDir, pid, source: "ancestor-stop" }));
1094
1135
  } catch {
1095
1136
  // The parent runner's own stopped result is authoritative for the attached step.
1096
1137
  }
1097
1138
  });
1098
1139
  try {
1099
- const imported = await waitForImportedAsyncRoot(step.importAsyncRoot, {
1140
+ const imported = await waitForImportedAsyncRoot(step.importAsyncRoot, omitUndefinedProperties({
1100
1141
  shouldAbort: () => importTimedOut || importStopped || ctx.timeoutSignal?.aborted === true || ctx.stopSignal?.aborted === true || ctx.skipAcceptance?.() === true,
1101
1142
  timeoutMessage: importStopped || ctx.stopSignal?.aborted === true ? ctx.stopMessage : ctx.timeoutMessage,
1102
- });
1143
+ }));
1103
1144
  try {
1104
1145
  fs.writeFileSync(ctx.outputFile, imported.output, "utf-8");
1105
1146
  } catch {
@@ -1108,7 +1149,7 @@ async function runSingleStep(
1108
1149
  const stopped = importStopped || imported.stopped === true || ctx.stopSignal?.aborted === true;
1109
1150
  const timedOut = !stopped && (importTimedOut || imported.timedOut === true || ctx.timeoutSignal?.aborted === true || ctx.skipAcceptance?.() === true);
1110
1151
  const message = stopped ? ctx.stopMessage ?? "Subagent stopped by user." : ctx.timeoutMessage ?? "Subagent timed out.";
1111
- return {
1152
+ return omitUndefinedProperties({
1112
1153
  agent: imported.agent,
1113
1154
  output: timedOut || stopped ? message : imported.output,
1114
1155
  exitCode: timedOut || stopped ? 1 : imported.exitCode,
@@ -1125,7 +1166,7 @@ async function runSingleStep(
1125
1166
  structuredOutputPath: timedOut || stopped ? undefined : imported.structuredOutputPath,
1126
1167
  structuredOutputSchemaPath: timedOut || stopped ? undefined : imported.structuredOutputSchemaPath,
1127
1168
  acceptance: timedOut || stopped ? undefined : imported.acceptance,
1128
- };
1169
+ });
1129
1170
  } finally {
1130
1171
  ctx.registerTimeout?.(undefined);
1131
1172
  ctx.registerStop?.(undefined);
@@ -1168,6 +1209,68 @@ async function runSingleStep(
1168
1209
  }
1169
1210
  transcriptWriter?.writeInitialUserMessage(task);
1170
1211
 
1212
+ if (step.runner?.type === "external-cli") {
1213
+ const runner: ExternalCliRunnerStatus = {
1214
+ type: "external-cli",
1215
+ command: step.runner.command,
1216
+ args: step.runner.args ?? [],
1217
+ promptDelivery: step.runner.promptDelivery ?? "stdin",
1218
+ capabilities: { stop: true, steer: false, resume: false, structuredOutput: false, toolEvents: false },
1219
+ };
1220
+ const outputSnapshot = captureSingleOutputSnapshot(step.outputPath);
1221
+ const external = await runExternalCli(omitUndefinedProperties({
1222
+ command: runner.command,
1223
+ args: runner.args,
1224
+ cwd: step.cwd ?? ctx.cwd,
1225
+ prompt: buildExternalCliPrompt(step.systemPrompt ?? "", task),
1226
+ asyncDir: path.dirname(ctx.outputFile),
1227
+ stepIndex: ctx.flatIndex,
1228
+ registerTimeout: ctx.registerTimeout,
1229
+ registerStop: ctx.registerStop,
1230
+ timeoutMessage: ctx.timeoutMessage,
1231
+ stopMessage: ctx.stopMessage,
1232
+ onProcess: ctx.onExternalProcess,
1233
+ }));
1234
+ try { fs.writeFileSync(ctx.outputFile, external.output, "utf-8"); } catch { /* Observability output is best-effort. */ }
1235
+ const resolvedOutput = step.outputPath && external.exitCode === 0
1236
+ ? resolveSingleOutput(step.outputPath, external.output, outputSnapshot)
1237
+ : { fullOutput: external.output };
1238
+ const outputReference = resolvedOutput.savedPath ? formatSavedOutputReference(resolvedOutput.savedPath, resolvedOutput.fullOutput) : undefined;
1239
+ const finalizedOutput = finalizeSingleOutput(omitUndefinedProperties({
1240
+ fullOutput: resolvedOutput.fullOutput,
1241
+ outputPath: step.outputPath,
1242
+ outputMode: step.outputMode,
1243
+ exitCode: external.exitCode ?? 1,
1244
+ savedPath: resolvedOutput.savedPath,
1245
+ outputReference,
1246
+ saveError: resolvedOutput.saveError,
1247
+ }));
1248
+ const artifactErrors = artifactPaths && ctx.artifactConfig?.enabled !== false
1249
+ ? persistStepArtifacts({
1250
+ artifactPaths,
1251
+ artifactConfig: ctx.artifactConfig,
1252
+ output: formatOutputArtifactContent(omitUndefinedProperties({ output: resolvedOutput.fullOutput, error: external.error, metadataPath: ctx.artifactConfig?.includeMetadata === false ? undefined : artifactPaths.metadataPath })),
1253
+ metadata: { runId: ctx.id, agent: step.agent, task, runner, externalProcess: external.externalProcess, exitCode: external.exitCode, error: external.error, timestamp: Date.now() },
1254
+ })
1255
+ : {};
1256
+ return omitUndefinedProperties({
1257
+ agent: step.agent,
1258
+ context: step.context,
1259
+ output: finalizedOutput.displayOutput,
1260
+ outputState: external.output.trim() ? "present" : "absent",
1261
+ exitCode: external.exitCode,
1262
+ error: external.error,
1263
+ timedOut: external.timedOut,
1264
+ stopped: external.stopped,
1265
+ processSignal: external.processSignal,
1266
+ artifactPaths,
1267
+ outputSaveError: artifactErrors.outputSaveError,
1268
+ metadataSaveError: artifactErrors.metadataSaveError,
1269
+ runner,
1270
+ externalProcess: external.externalProcess,
1271
+ });
1272
+ }
1273
+
1171
1274
  const candidates = step.modelCandidates && step.modelCandidates.length > 0
1172
1275
  ? step.modelCandidates
1173
1276
  : step.model
@@ -1194,7 +1297,7 @@ async function runSingleStep(
1194
1297
  modelAttemptsLoop: while (modelIndex < candidates.length) {
1195
1298
  if (ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break;
1196
1299
  const candidate = candidates[modelIndex];
1197
- ctx.onAttemptStart?.({ model: candidate, thinking: resolveEffectiveThinking(candidate, step.thinking) });
1300
+ ctx.onAttemptStart?.(omitUndefinedProperties({ model: candidate, thinking: resolveEffectiveThinking(candidate, step.thinking) }));
1198
1301
  const outputSnapshot = captureSingleOutputSnapshot(step.outputPath);
1199
1302
  if (effectiveStructuredOutput) {
1200
1303
  try {
@@ -1212,7 +1315,7 @@ async function runSingleStep(
1212
1315
  childIndex: ctx.flatIndex,
1213
1316
  })
1214
1317
  : undefined;
1215
- const { args, env, tempDir, toolDiagnosticPath, runtimeAcknowledgedExtensionsPath, capabilityAudit: attemptCapabilityAudit } = buildPiArgs({
1318
+ const { args, env, tempDir, toolDiagnosticPath, runtimeAcknowledgedExtensionsPath, capabilityAudit: attemptCapabilityAudit } = buildPiArgs(omitUndefinedProperties({
1216
1319
  parentSessionId: step.parentSessionId,
1217
1320
  baseArgs: ["--mode", "json", "-p"],
1218
1321
  task,
@@ -1246,11 +1349,15 @@ async function runSingleStep(
1246
1349
  steerAckDir: ctx.steerAckDir,
1247
1350
  structuredOutput: effectiveStructuredOutput,
1248
1351
  toolBudget: step.toolBudget,
1352
+ permissionRules: step.permissionRules,
1353
+ permissionAuditPath: step.permissionRules && ctx.artifactsDir
1354
+ ? path.join(ctx.artifactsDir, "permission-audit", `${ctx.id}-${ctx.flatIndex}.jsonl`)
1355
+ : undefined,
1249
1356
  childWatchdog,
1250
1357
  waitToolEnabled: step.waitToolEnabled,
1251
- });
1358
+ }));
1252
1359
  if (step.definitionDigest) {
1253
- const toolPlan = resolvePiLaunchToolPlan({
1360
+ const toolPlan = resolvePiLaunchToolPlan(omitUndefinedProperties({
1254
1361
  tools: step.tools,
1255
1362
  extensions: step.extensions,
1256
1363
  subagentOnlyExtensions: step.subagentOnlyExtensions,
@@ -1260,13 +1367,13 @@ async function runSingleStep(
1260
1367
  structuredOutput: Boolean(effectiveStructuredOutput),
1261
1368
  capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
1262
1369
  inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
1263
- });
1370
+ }));
1264
1371
  launchResolvedExtensions = projectLaunchResolvedChildExtensions(toolPlan);
1265
- actualLaunchContractDigest = launchBindingDigest({
1372
+ actualLaunchContractDigest = launchBindingDigest(omitUndefinedProperties({
1266
1373
  definitionDigest: step.definitionDigest,
1267
1374
  task: step.launchBindingTask ?? task,
1268
1375
  ...(candidate ? { model: candidate } : {}),
1269
- modelCandidates: candidates,
1376
+ modelCandidates: candidates as string[],
1270
1377
  ...(resolveEffectiveThinking(candidate, step.thinking) ? { thinking: resolveEffectiveThinking(candidate, step.thinking) } : {}),
1271
1378
  systemPrompt: appendTurnBudgetSystemPrompt(step.systemPrompt ?? "", ctx.turnBudget),
1272
1379
  systemPromptMode: step.systemPromptMode,
@@ -1279,7 +1386,7 @@ async function runSingleStep(
1279
1386
  ...(step.outputPath ? { outputPath: step.outputPath } : {}),
1280
1387
  ...(step.outputMode ? { outputMode: step.outputMode } : {}),
1281
1388
  ...(step.structuredOutputSchema ? { structuredOutputSchema: step.structuredOutputSchema } : {}),
1282
- });
1389
+ }));
1283
1390
  }
1284
1391
  capabilityAudit = attemptCapabilityAudit;
1285
1392
  writerAttemptCount += 1;
@@ -1362,13 +1469,13 @@ async function runSingleStep(
1362
1469
  }
1363
1470
  const completionGuardEnabled = isAgentContractV1(step.agentContract) ? step.completionGuard === true : step.completionGuard !== false;
1364
1471
  const completionGuard = run.exitCode === 0 && !run.error && !toolAvailabilityError && !hiddenError?.hasError && !emptyOutputError && completionGuardEnabled
1365
- ? evaluateCompletionMutationGuard({
1472
+ ? evaluateCompletionMutationGuard(omitUndefinedProperties({
1366
1473
  agent: step.agent,
1367
1474
  task: taskForCompletionGuard,
1368
1475
  messages: run.messages,
1369
1476
  tools: step.tools,
1370
1477
  mcpDirectTools: step.mcpDirectTools,
1371
- })
1478
+ }))
1372
1479
  : undefined;
1373
1480
  const completionGuardTriggered = completionGuard?.triggered === true && !run.observedMutationAttempt;
1374
1481
  const fileMutationEffect = completionGuard
@@ -1389,13 +1496,13 @@ async function runSingleStep(
1389
1496
  : run.error && run.exitCode === 0
1390
1497
  ? 1
1391
1498
  : run.exitCode;
1392
- const signalError = run.exitCode !== 0 && isUnexplainedProcessSignal({
1499
+ const signalError = run.exitCode !== 0 && isUnexplainedProcessSignal(omitUndefinedProperties({
1393
1500
  processSignal: run.processSignal,
1394
1501
  interrupted: run.interrupted,
1395
1502
  timedOut: run.timedOut,
1396
1503
  stopped: run.stopped,
1397
1504
  turnBudgetExceeded: run.turnBudgetExceeded,
1398
- }) ? formatProcessSignalError(run.processSignal!) : undefined;
1505
+ })) ? formatProcessSignalError(run.processSignal!) : undefined;
1399
1506
  const error = toolAvailabilityError
1400
1507
  ?? completionGuardError
1401
1508
  ?? structuredError
@@ -1405,13 +1512,13 @@ async function runSingleStep(
1405
1512
  ? `${hiddenError.errorType} failed (exit ${effectiveExitCode}): ${hiddenError.details}`
1406
1513
  : `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
1407
1514
  : run.error || signalError || (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined));
1408
- const attempt: ModelAttempt = {
1515
+ const attempt: ModelAttempt = omitUndefinedProperties({
1409
1516
  model: candidate ?? run.model ?? step.model ?? "default",
1410
1517
  success: effectiveExitCode === 0 && !error,
1411
1518
  exitCode: effectiveExitCode,
1412
1519
  error,
1413
1520
  usage: run.usage,
1414
- };
1521
+ });
1415
1522
  modelAttempts.push(attempt);
1416
1523
  if (candidate && startupAttemptIndex === 0) attemptedModels.push(candidate);
1417
1524
  completionGuardTriggeredFinal = completionGuardTriggered;
@@ -1427,7 +1534,7 @@ async function runSingleStep(
1427
1534
  if (run.stopped || run.timedOut || ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break modelAttemptsLoop;
1428
1535
  if (attempt.success || completionGuardTriggered) break modelAttemptsLoop;
1429
1536
 
1430
- const startupFailure = isRetryableSubagentStartupFailure({
1537
+ const startupFailure = isRetryableSubagentStartupFailure(omitUndefinedProperties({
1431
1538
  exitCode: effectiveExitCode,
1432
1539
  error,
1433
1540
  finalOutput: run.finalOutput,
@@ -1442,7 +1549,7 @@ async function runSingleStep(
1442
1549
  timedOut: run.timedOut,
1443
1550
  stopped: run.stopped,
1444
1551
  turnBudgetExceeded: run.turnBudgetExceeded,
1445
- });
1552
+ }));
1446
1553
  const retryDelayMs = SUBAGENT_STARTUP_RETRY_DELAYS_MS[startupAttemptIndex];
1447
1554
  if (startupFailure && retryDelayMs !== undefined) {
1448
1555
  const retryNote = formatSubagentStartupRetryNote({
@@ -1507,7 +1614,7 @@ async function runSingleStep(
1507
1614
  : resolvedOutput.savedPath
1508
1615
  ? "unknown"
1509
1616
  : finalResult?.outputState ?? "unknown";
1510
- const finalizedOutput = finalizeSingleOutput({
1617
+ const finalizedOutput = finalizeSingleOutput(omitUndefinedProperties({
1511
1618
  fullOutput: outputForSummary,
1512
1619
  outputPath: step.outputPath,
1513
1620
  outputMode: step.outputMode,
@@ -1515,10 +1622,10 @@ async function runSingleStep(
1515
1622
  savedPath: resolvedOutput.savedPath,
1516
1623
  outputReference,
1517
1624
  saveError: resolvedOutput.saveError,
1518
- });
1625
+ }));
1519
1626
  outputForSummary = finalizedOutput.displayOutput;
1520
1627
  const acceptance = step.effectiveAcceptance && !finalResult?.stopped && !finalResult?.turnBudgetExceeded && !ctx.timeoutSignal?.aborted && !ctx.stopSignal?.aborted && !ctx.skipAcceptance?.()
1521
- ? await evaluateAcceptance({
1628
+ ? await evaluateAcceptance(omitUndefinedProperties({
1522
1629
  acceptance: step.effectiveAcceptance,
1523
1630
  output: outputForAcceptance,
1524
1631
  fileOutput: childWrittenOutput !== undefined && step.outputPath
@@ -1528,7 +1635,7 @@ async function runSingleStep(
1528
1635
  signal: combinedAbortSignal([ctx.timeoutSignal, ctx.stopSignal]),
1529
1636
  abortMessage: ctx.stopSignal?.aborted ? ctx.stopMessage ?? "Subagent stopped by user." : ctx.timeoutMessage ?? "Subagent timed out.",
1530
1637
  reportOptional: isAgentContractV1(step.agentContract),
1531
- })
1638
+ }))
1532
1639
  : undefined;
1533
1640
  const stoppedAfterAcceptance = finalResult?.stopped === true || ctx.stopSignal?.aborted === true;
1534
1641
  const timedOutAfterAcceptance = !stoppedAfterAcceptance && (finalResult?.timedOut === true || ctx.timeoutSignal?.aborted === true);
@@ -1555,43 +1662,39 @@ async function runSingleStep(
1555
1662
  ? (finalResult?.error ? `${finalResult.error}\n${acceptanceFailure}` : acceptanceFailure)
1556
1663
  : finalResult?.error;
1557
1664
 
1558
- if (artifactPaths && ctx.artifactConfig?.enabled !== false) {
1559
- if (ctx.artifactConfig?.includeOutput !== false) {
1560
- fs.writeFileSync(artifactPaths.outputPath, formatOutputArtifactContent({
1665
+ const artifactErrors = artifactPaths && ctx.artifactConfig?.enabled !== false
1666
+ ? persistStepArtifacts({
1667
+ artifactPaths,
1668
+ artifactConfig: ctx.artifactConfig,
1669
+ output: formatOutputArtifactContent(omitUndefinedProperties({
1561
1670
  output,
1562
1671
  error: effectiveFinalError,
1563
1672
  transcriptPath: transcriptWriter ? artifactPaths.transcriptPath : undefined,
1564
1673
  metadataPath: ctx.artifactConfig?.includeMetadata === false ? undefined : artifactPaths.metadataPath,
1565
- }), "utf-8");
1566
- }
1567
- if (ctx.artifactConfig?.includeMetadata !== false) {
1568
- fs.writeFileSync(
1569
- artifactPaths.metadataPath,
1570
- JSON.stringify({
1571
- runId: ctx.id,
1572
- agent: step.agent,
1573
- task,
1574
- exitCode: effectiveFinalExitCode,
1575
- model: finalResult?.model,
1576
- attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
1577
- modelAttempts,
1578
- error: effectiveFinalError,
1579
- acceptance: effectiveAcceptance,
1580
- ...(capabilityAudit ? { capabilityCeiling: capabilityAudit.ceiling, capabilityAudit } : {}),
1581
- launchContractDigest: actualLaunchContractDigest,
1582
- launchResolvedExtensions,
1583
- ...((finalResult as (RunPiStreamingResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }) | undefined)?.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: (finalResult as RunPiStreamingResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }).runtimeAcknowledgedExtensions } : {}),
1584
- ...(transcriptWriter ? { transcriptPath: artifactPaths.transcriptPath } : {}),
1585
- transcriptError: transcriptWriter?.getError(),
1586
- skills: step.skills,
1587
- timestamp: Date.now(),
1588
- }, null, 2),
1589
- "utf-8",
1590
- );
1591
- }
1592
- }
1674
+ })),
1675
+ metadata: {
1676
+ runId: ctx.id,
1677
+ agent: step.agent,
1678
+ task,
1679
+ exitCode: effectiveFinalExitCode,
1680
+ model: finalResult?.model,
1681
+ attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
1682
+ modelAttempts,
1683
+ error: effectiveFinalError,
1684
+ acceptance: effectiveAcceptance,
1685
+ ...(capabilityAudit ? { capabilityCeiling: capabilityAudit.ceiling, capabilityAudit } : {}),
1686
+ launchContractDigest: actualLaunchContractDigest,
1687
+ launchResolvedExtensions,
1688
+ ...((finalResult as (RunPiStreamingResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }) | undefined)?.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: (finalResult as RunPiStreamingResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }).runtimeAcknowledgedExtensions } : {}),
1689
+ ...(transcriptWriter ? { transcriptPath: artifactPaths.transcriptPath } : {}),
1690
+ transcriptError: transcriptWriter?.getError(),
1691
+ skills: step.skills,
1692
+ timestamp: Date.now(),
1693
+ },
1694
+ })
1695
+ : {};
1593
1696
 
1594
- const result = {
1697
+ const result: StepResult & { completionGuardTriggered?: boolean } = omitUndefinedProperties({
1595
1698
  agent: step.agent,
1596
1699
  context: step.context,
1597
1700
  ...(step.agentContract ? { agentContract: step.agentContract } : {}),
@@ -1608,6 +1711,8 @@ async function runSingleStep(
1608
1711
  modelAttempts,
1609
1712
  totalCost: costSummaryFromAttempts(modelAttempts),
1610
1713
  artifactPaths,
1714
+ outputSaveError: artifactErrors.outputSaveError,
1715
+ metadataSaveError: artifactErrors.metadataSaveError,
1611
1716
  transcriptPath: transcriptWriter ? artifactPaths?.transcriptPath : undefined,
1612
1717
  transcriptError: transcriptWriter?.getError(),
1613
1718
  interrupted: timedOutAfterAcceptance || stoppedAfterAcceptance || turnBudgetExceeded ? false : finalResult?.interrupted,
@@ -1631,7 +1736,7 @@ async function runSingleStep(
1631
1736
  ...((finalResult as (RunPiStreamingResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }) | undefined)?.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: (finalResult as RunPiStreamingResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }).runtimeAcknowledgedExtensions } : {}),
1632
1737
  writerProcesses,
1633
1738
  writerAttemptCount,
1634
- };
1739
+ });
1635
1740
  return isAgentContractV1(step.agentContract) ? attachContractProjections(result as unknown as import("../../shared/types.ts").SingleResult) as unknown as typeof result : result;
1636
1741
  }
1637
1742
 
@@ -1640,6 +1745,17 @@ type RunnerStatusStep = NonNullable<AsyncStatus["steps"]>[number] & {
1640
1745
  description?: string;
1641
1746
  };
1642
1747
 
1748
+ function externalRunnerStatus(runner: SubagentStep["runner"]): ExternalCliRunnerStatus | undefined {
1749
+ if (runner?.type !== "external-cli") return undefined;
1750
+ return {
1751
+ type: "external-cli",
1752
+ command: runner.command,
1753
+ args: runner.args ?? [],
1754
+ promptDelivery: runner.promptDelivery ?? "stdin",
1755
+ capabilities: { stop: true, steer: false, resume: false, structuredOutput: false, toolEvents: false },
1756
+ };
1757
+ }
1758
+
1643
1759
  function appendCapabilityCeilingAppliedEvent(eventsPath: string, runId: string, stepIndex: number, agent: string, result: StepResult): void {
1644
1760
  if (!result.capabilityCeiling) return;
1645
1761
  appendJsonl(eventsPath, JSON.stringify({
@@ -1668,6 +1784,12 @@ type RunnerStatusPayload = Omit<AsyncStatus, "steps" | "parallelGroups" | "pid"
1668
1784
  error?: string;
1669
1785
  };
1670
1786
 
1787
+ function requiredStatusStep(statusPayload: RunnerStatusPayload, index: number): RunnerStatusStep {
1788
+ const step = statusPayload.steps[index];
1789
+ if (!step) throw new Error(`Missing status step at index ${index}`);
1790
+ return step;
1791
+ }
1792
+
1671
1793
  function markParallelGroupSetupFailure(input: {
1672
1794
  statusPayload: RunnerStatusPayload;
1673
1795
  results: StepResult[];
@@ -1683,12 +1805,15 @@ function markParallelGroupSetupFailure(input: {
1683
1805
  }): void {
1684
1806
  for (let taskIndex = 0; taskIndex < input.group.parallel.length; taskIndex++) {
1685
1807
  const flatTaskIndex = input.groupStartFlatIndex + taskIndex;
1686
- input.statusPayload.steps[flatTaskIndex].status = "failed";
1687
- input.statusPayload.steps[flatTaskIndex].startedAt = input.failedAt;
1688
- input.statusPayload.steps[flatTaskIndex].endedAt = input.failedAt;
1689
- input.statusPayload.steps[flatTaskIndex].durationMs = 0;
1690
- input.statusPayload.steps[flatTaskIndex].exitCode = 1;
1691
- input.results.push({ agent: input.group.parallel[taskIndex].agent, context: input.group.parallel[taskIndex].context, output: input.setupError, success: false, exitCode: 1, sessionFile: input.group.parallel[taskIndex].sessionFile });
1808
+ const statusStep = requiredStatusStep(input.statusPayload, flatTaskIndex);
1809
+ const task = input.group.parallel[taskIndex];
1810
+ if (!task) throw new Error(`Missing parallel task at index ${taskIndex}`);
1811
+ statusStep.status = "failed";
1812
+ statusStep.startedAt = input.failedAt;
1813
+ statusStep.endedAt = input.failedAt;
1814
+ statusStep.durationMs = 0;
1815
+ statusStep.exitCode = 1;
1816
+ input.results.push(omitUndefinedProperties({ agent: task.agent, context: task.context, output: input.setupError, success: false, exitCode: 1, sessionFile: task.sessionFile }));
1692
1817
  }
1693
1818
  input.statusPayload.currentStep = input.groupStartFlatIndex;
1694
1819
  input.statusPayload.lastUpdate = input.failedAt;
@@ -1716,16 +1841,17 @@ function markParallelGroupRunning(input: {
1716
1841
  }): void {
1717
1842
  for (let taskIndex = 0; taskIndex < input.group.parallel.length; taskIndex++) {
1718
1843
  const flatTaskIndex = input.groupStartFlatIndex + taskIndex;
1719
- input.statusPayload.steps[flatTaskIndex].status = "pending";
1720
- input.statusPayload.steps[flatTaskIndex].startedAt = undefined;
1721
- input.statusPayload.steps[flatTaskIndex].endedAt = undefined;
1722
- input.statusPayload.steps[flatTaskIndex].durationMs = undefined;
1723
- input.statusPayload.steps[flatTaskIndex].lastActivityAt = undefined;
1724
- input.statusPayload.steps[flatTaskIndex].activityState = undefined;
1725
- input.statusPayload.steps[flatTaskIndex].error = undefined;
1844
+ const statusStep = requiredStatusStep(input.statusPayload, flatTaskIndex);
1845
+ statusStep.status = "pending";
1846
+ delete statusStep.startedAt;
1847
+ delete statusStep.endedAt;
1848
+ delete statusStep.durationMs;
1849
+ delete statusStep.lastActivityAt;
1850
+ delete statusStep.activityState;
1851
+ delete statusStep.error;
1726
1852
  }
1727
1853
  input.statusPayload.currentStep = input.groupStartFlatIndex;
1728
- input.statusPayload.activityState = undefined;
1854
+ delete input.statusPayload.activityState;
1729
1855
  input.statusPayload.lastActivityAt = input.groupStartTime;
1730
1856
  input.statusPayload.lastUpdate = input.groupStartTime;
1731
1857
  input.statusPayload.outputFile = path.join(input.asyncDir, `output-${input.groupStartFlatIndex}.log`);
@@ -1747,8 +1873,9 @@ function prepareParallelTaskRun(
1747
1873
  taskIndex: number,
1748
1874
  ): { taskForRun: SubagentStep; taskCwd: string } {
1749
1875
  if (!worktreeSetup) return { taskForRun: task, taskCwd: cwd };
1876
+ const { cwd: _taskCwd, ...taskForRun } = task;
1750
1877
  return {
1751
- taskForRun: { ...task, cwd: undefined },
1878
+ taskForRun,
1752
1879
  taskCwd: worktreeSetup.worktrees[taskIndex]!.agentCwd,
1753
1880
  };
1754
1881
  }
@@ -1854,7 +1981,7 @@ async function runSubagent(
1854
1981
  for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) {
1855
1982
  const step = steps[stepIndex]!;
1856
1983
  if (isCheckpointRunnerStep(step)) {
1857
- initialStatusSteps.push({
1984
+ initialStatusSteps.push(omitUndefinedProperties({
1858
1985
  agent: `checkpoint:${step.checkpoint}`,
1859
1986
  phase: step.phase,
1860
1987
  label: step.label ?? step.checkpoint,
@@ -1862,7 +1989,7 @@ async function runSubagent(
1862
1989
  checkpoint: { name: step.checkpoint, ...(step.message ? { message: step.message } : {}), status: "pending", stepIndex },
1863
1990
  recentTools: [],
1864
1991
  recentOutput: [],
1865
- });
1992
+ }));
1866
1993
  flatStepCount++;
1867
1994
  continue;
1868
1995
  }
@@ -1870,9 +1997,10 @@ async function runSubagent(
1870
1997
  parallelGroups.push({ start: flatStepCount, count: step.parallel.length, stepIndex });
1871
1998
  for (const task of step.parallel) {
1872
1999
  const taskFlatIndex = flatStepCount;
1873
- const transcriptPath = resolveAsyncStepTranscriptPath({ artifactsDir, artifactConfig, runId: id, agent: task.agent, flatIndex: taskFlatIndex, flatStepCount: initialFlatStepCount });
1874
- initialStatusSteps.push({
2000
+ const transcriptPath = resolveAsyncStepTranscriptPath(omitUndefinedProperties({ artifactsDir, artifactConfig, runId: id, agent: task.agent, flatIndex: taskFlatIndex, flatStepCount: initialFlatStepCount }));
2001
+ initialStatusSteps.push(omitUndefinedProperties({
1875
2002
  agent: task.agent,
2003
+ ...(externalRunnerStatus(task.runner) ? { runner: externalRunnerStatus(task.runner) } : {}),
1876
2004
  ...(statusStepDescription(task.task) ? { description: statusStepDescription(task.task) } : {}),
1877
2005
  ...(task.context ? { context: task.context } : {}),
1878
2006
  phase: task.phase,
@@ -1893,13 +2021,14 @@ async function runSubagent(
1893
2021
  attemptedModels: task.modelCandidates && task.modelCandidates.length > 0 ? task.modelCandidates : task.model ? [task.model] : undefined,
1894
2022
  recentTools: [],
1895
2023
  recentOutput: [],
1896
- });
2024
+ }));
1897
2025
  flatStepCount++;
1898
2026
  }
1899
2027
  } else if (isDynamicRunnerGroup(step)) {
1900
2028
  parallelGroups.push({ start: flatStepCount, count: 1, stepIndex });
1901
- initialStatusSteps.push({
2029
+ initialStatusSteps.push(omitUndefinedProperties({
1902
2030
  agent: `expand:${step.parallel.agent}`,
2031
+ ...(externalRunnerStatus(step.parallel.runner) ? { runner: externalRunnerStatus(step.parallel.runner) } : {}),
1903
2032
  ...(step.parallel.context ? { context: step.parallel.context } : {}),
1904
2033
  phase: step.phase ?? step.parallel.phase,
1905
2034
  label: step.label ?? step.parallel.label ?? `Dynamic fanout (${step.collect.as})`,
@@ -1911,13 +2040,14 @@ async function runSubagent(
1911
2040
  ...(step.parallel.toolBudget ? { toolBudget: initialToolBudgetState(step.parallel.toolBudget) } : {}),
1912
2041
  recentTools: [],
1913
2042
  recentOutput: [],
1914
- });
2043
+ }));
1915
2044
  flatStepCount++;
1916
2045
  } else {
1917
2046
  const stepFlatIndex = flatStepCount;
1918
- const transcriptPath = resolveAsyncStepTranscriptPath({ artifactsDir, artifactConfig, runId: id, agent: step.agent, flatIndex: stepFlatIndex, flatStepCount: initialFlatStepCount });
1919
- initialStatusSteps.push({
2047
+ const transcriptPath = resolveAsyncStepTranscriptPath(omitUndefinedProperties({ artifactsDir, artifactConfig, runId: id, agent: step.agent, flatIndex: stepFlatIndex, flatStepCount: initialFlatStepCount }));
2048
+ initialStatusSteps.push(omitUndefinedProperties({
1920
2049
  agent: step.agent,
2050
+ ...(externalRunnerStatus(step.runner) ? { runner: externalRunnerStatus(step.runner) } : {}),
1921
2051
  ...(statusStepDescription(step.task) ? { description: statusStepDescription(step.task) } : {}),
1922
2052
  ...(step.context ? { context: step.context } : {}),
1923
2053
  phase: step.phase,
@@ -1938,7 +2068,7 @@ async function runSubagent(
1938
2068
  attemptedModels: step.modelCandidates && step.modelCandidates.length > 0 ? step.modelCandidates : step.model ? [step.model] : undefined,
1939
2069
  recentTools: [],
1940
2070
  recentOutput: [],
1941
- });
2071
+ }));
1942
2072
  flatStepCount++;
1943
2073
  }
1944
2074
  }
@@ -1950,7 +2080,7 @@ async function runSubagent(
1950
2080
  step.processTerminal = { version: 1, state: "pending", runId: id, runnerProcessInstanceId: config.runnerProcessInstanceId };
1951
2081
  }
1952
2082
  }
1953
- const statusPayload: RunnerStatusPayload = {
2083
+ const statusPayload: RunnerStatusPayload = omitUndefinedProperties({
1954
2084
  lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
1955
2085
  runId: id,
1956
2086
  ...(config.sessionId ? { sessionId: config.sessionId } : {}),
@@ -1975,12 +2105,14 @@ async function runSubagent(
1975
2105
  ...(config.launchContractDigest ? { launchContractDigest: config.launchContractDigest } : {}),
1976
2106
  ...(config.launchResolvedExtensions ? { launchResolvedExtensions: config.launchResolvedExtensions } : {}),
1977
2107
  ...(config.capabilityCeiling ? { capabilityCeiling: config.capabilityCeiling } : {}),
2108
+ ...(config.parentWorkflowRunId ? { parentWorkflowRunId: config.parentWorkflowRunId } : {}),
2109
+ ...(config.workflowKey ? { workflowKey: config.workflowKey } : {}),
1978
2110
  ...(config.runnerProcessInstanceId ? { processTerminal: { version: 1 as const, state: "pending" as const, runId: id, runnerProcessInstanceId: config.runnerProcessInstanceId } } : {}),
1979
2111
  steps: initialStatusSteps,
1980
2112
  artifactsDir,
1981
2113
  sessionDir: config.sessionDir,
1982
2114
  outputFile: path.join(asyncDir, "output-0.log"),
1983
- };
2115
+ });
1984
2116
 
1985
2117
  fs.mkdirSync(asyncDir, { recursive: true });
1986
2118
  writeAtomicJson(statusPath, statusPayload);
@@ -1998,18 +2130,18 @@ async function runSubagent(
1998
2130
  };
1999
2131
  };
2000
2132
  const refreshUsageBudget = () => {
2001
- statusPayload.usageBudget = usageBudgetState(config.usageBudget, currentUsageTotals());
2133
+ setOptionalProperty(statusPayload, "usageBudget", usageBudgetState(config.usageBudget, currentUsageTotals()));
2002
2134
  return statusPayload.usageBudget;
2003
2135
  };
2004
2136
  const emitNestedSelfEvent = (type: "subagent.nested.updated" | "subagent.nested.completed"): void => {
2005
2137
  if (!config.nestedRoute || !config.nestedSelf) return;
2006
2138
  try {
2007
- writeNestedEvent(config.nestedRoute, {
2139
+ writeNestedEvent(config.nestedRoute, omitUndefinedProperties({
2008
2140
  type,
2009
2141
  ts: Date.now(),
2010
2142
  parentRunId: config.nestedSelf.parentRunId,
2011
2143
  parentStepIndex: config.nestedSelf.parentStepIndex,
2012
- child: nestedSummaryFromAsyncStatus(statusPayload, asyncDir, {
2144
+ child: nestedSummaryFromAsyncStatus(statusPayload, asyncDir, omitUndefinedProperties({
2013
2145
  id,
2014
2146
  parentRunId: config.nestedSelf.parentRunId,
2015
2147
  parentStepIndex: config.nestedSelf.parentStepIndex,
@@ -2017,8 +2149,8 @@ async function runSubagent(
2017
2149
  path: config.nestedSelf.path,
2018
2150
  mode: statusPayload.mode,
2019
2151
  ts: Date.now(),
2020
- }),
2021
- });
2152
+ })),
2153
+ }));
2022
2154
  } catch (error) {
2023
2155
  console.error("Failed to emit nested async status event:", error);
2024
2156
  }
@@ -2036,9 +2168,9 @@ async function runSubagent(
2036
2168
  const step = statusPayload.steps[node.flatIndex];
2037
2169
  if (step) {
2038
2170
  node.status = normalize(step.status);
2039
- node.error = step.error;
2040
- node.acceptanceStatus = step.acceptance?.status;
2041
- node.checkpoint = step.checkpoint;
2171
+ setOptionalProperty(node, "error", step.error);
2172
+ setOptionalProperty(node, "acceptanceStatus", step.acceptance?.status);
2173
+ setOptionalProperty(node, "checkpoint", step.checkpoint);
2042
2174
  }
2043
2175
  if (statusPayload.currentStep === node.flatIndex) graph.currentNodeId = node.id;
2044
2176
  }
@@ -2061,6 +2193,11 @@ async function runSubagent(
2061
2193
  writeAtomicJson(statusPath, statusPayload);
2062
2194
  emitNestedSelfEvent(statusPayload.state === "running" || statusPayload.state === "queued" ? "subagent.nested.updated" : "subagent.nested.completed");
2063
2195
  };
2196
+ const updateExternalProcess = (index: number, process: ExternalProcessStatus): void => {
2197
+ requiredStatusStep(statusPayload, index).externalProcess = process;
2198
+ statusPayload.lastUpdate = Date.now();
2199
+ writeStatusPayload();
2200
+ };
2064
2201
  const registerStepInterrupt = (flatIndex: number, interrupt: (() => void) | undefined): void => {
2065
2202
  if (!interrupt) {
2066
2203
  activeChildInterrupts.delete(flatIndex);
@@ -2127,7 +2264,7 @@ async function runSubagent(
2127
2264
  const nestedAsyncDir = run.asyncDir ?? resolveNestedAsyncDir(config.nestedRoute.rootRunId, run);
2128
2265
  if (!nestedAsyncDir) continue;
2129
2266
  try {
2130
- deliverInterruptRequest({ asyncDir: nestedAsyncDir, pid: run.pid, source: "ancestor-interrupt" });
2267
+ deliverInterruptRequest(omitUndefinedProperties({ asyncDir: nestedAsyncDir, pid: run.pid, source: "ancestor-interrupt" }));
2131
2268
  } catch (error) {
2132
2269
  appendJsonl(eventsPath, JSON.stringify({
2133
2270
  type: "subagent.nested.interrupt_failed",
@@ -2158,7 +2295,7 @@ async function runSubagent(
2158
2295
  const nestedAsyncDir = run.asyncDir ?? resolveNestedAsyncDir(config.nestedRoute.rootRunId, run);
2159
2296
  if (!nestedAsyncDir) continue;
2160
2297
  try {
2161
- deliverStopRequest({ asyncDir: nestedAsyncDir, pid: run.pid, source: "ancestor-stop" });
2298
+ deliverStopRequest(omitUndefinedProperties({ asyncDir: nestedAsyncDir, pid: run.pid, source: "ancestor-stop" }));
2162
2299
  } catch (error) {
2163
2300
  appendJsonl(eventsPath, JSON.stringify({
2164
2301
  type: "subagent.nested.stop_failed",
@@ -2189,7 +2326,7 @@ async function runSubagent(
2189
2326
  const nestedAsyncDir = run.asyncDir ?? resolveNestedAsyncDir(config.nestedRoute.rootRunId, run);
2190
2327
  if (!nestedAsyncDir) continue;
2191
2328
  try {
2192
- deliverTimeoutRequest({ asyncDir: nestedAsyncDir, pid: run.pid, source: "ancestor-timeout" });
2329
+ deliverTimeoutRequest(omitUndefinedProperties({ asyncDir: nestedAsyncDir, pid: run.pid, source: "ancestor-timeout" }));
2193
2330
  } catch (error) {
2194
2331
  appendJsonl(eventsPath, JSON.stringify({
2195
2332
  type: "subagent.nested.timeout_failed",
@@ -2201,14 +2338,14 @@ async function runSubagent(
2201
2338
  }
2202
2339
  }
2203
2340
  };
2204
- const pausedStepResult = (agent: string, context?: "fresh" | "fork"): SingleStepResult => ({
2341
+ const pausedStepResult = (agent: string, context?: "fresh" | "fork"): SingleStepResult => omitUndefinedProperties({
2205
2342
  agent,
2206
2343
  context,
2207
2344
  output: "Paused after interrupt. Waiting for explicit next action.",
2208
2345
  exitCode: 0,
2209
2346
  interrupted: true,
2210
2347
  });
2211
- const timedOutStepResult = (agent: string, context?: "fresh" | "fork"): SingleStepResult => ({
2348
+ const timedOutStepResult = (agent: string, context?: "fresh" | "fork"): SingleStepResult => omitUndefinedProperties({
2212
2349
  agent,
2213
2350
  context,
2214
2351
  output: timeoutMessage ?? "Subagent timed out.",
@@ -2216,7 +2353,7 @@ async function runSubagent(
2216
2353
  exitCode: 1,
2217
2354
  timedOut: true,
2218
2355
  });
2219
- const stoppedStepResult = (agent: string, context?: "fresh" | "fork"): SingleStepResult => ({
2356
+ const stoppedStepResult = (agent: string, context?: "fresh" | "fork"): SingleStepResult => omitUndefinedProperties({
2220
2357
  agent,
2221
2358
  context,
2222
2359
  output: stopMessage,
@@ -2267,8 +2404,8 @@ async function runSubagent(
2267
2404
  const groupNode = statusPayload.workflowGraph?.nodes.find((node) => node.id === `step-${stepIndex}`);
2268
2405
  if (!groupNode) return;
2269
2406
  groupNode.status = status;
2270
- groupNode.error = error;
2271
- groupNode.acceptanceStatus = acceptance?.status ?? groupNode.acceptanceStatus;
2407
+ setOptionalProperty(groupNode, "error", error);
2408
+ setOptionalProperty(groupNode, "acceptanceStatus", acceptance?.status ?? groupNode.acceptanceStatus);
2272
2409
  };
2273
2410
 
2274
2411
  const stepOutputActivityAt = (index: number): number => {
@@ -2315,9 +2452,9 @@ async function runSubagent(
2315
2452
  const activeStep = statusPayload.steps
2316
2453
  .filter((step) => step.status === "running" && typeof step.currentTool === "string" && step.currentTool.length > 0)
2317
2454
  .sort((left, right) => (right.currentToolStartedAt ?? 0) - (left.currentToolStartedAt ?? 0))[0];
2318
- statusPayload.currentTool = activeStep?.currentTool;
2319
- statusPayload.currentToolStartedAt = activeStep?.currentToolStartedAt;
2320
- statusPayload.currentPath = activeStep?.currentPath;
2455
+ setOptionalProperty(statusPayload, "currentTool", activeStep?.currentTool);
2456
+ setOptionalProperty(statusPayload, "currentToolStartedAt", activeStep?.currentToolStartedAt);
2457
+ setOptionalProperty(statusPayload, "currentPath", activeStep?.currentPath);
2321
2458
  };
2322
2459
  const syncAggregateActivityState = (): void => {
2323
2460
  const nextRunState = statusPayload.steps.some((step) => step.activityState === "needs_attention")
@@ -2326,7 +2463,7 @@ async function runSubagent(
2326
2463
  ? "active_long_running"
2327
2464
  : undefined;
2328
2465
  currentActivityState = nextRunState;
2329
- statusPayload.activityState = nextRunState;
2466
+ setOptionalProperty(statusPayload, "activityState", nextRunState);
2330
2467
  };
2331
2468
  const maybeEmitActiveLongRunning = (flatIndex: number, now: number): boolean => {
2332
2469
  if (!controlConfig.enabled || activeLongRunningSteps.has(flatIndex)) return false;
@@ -2343,7 +2480,7 @@ async function runSubagent(
2343
2480
  const previous = step.activityState;
2344
2481
  step.activityState = "active_long_running";
2345
2482
  statusPayload.activityState = statusPayload.activityState === "needs_attention" ? "needs_attention" : "active_long_running";
2346
- const event = buildControlEvent({
2483
+ const event = buildControlEvent(omitUndefinedProperties({
2347
2484
  type: "active_long_running",
2348
2485
  from: previous,
2349
2486
  to: "active_long_running",
@@ -2360,7 +2497,7 @@ async function runSubagent(
2360
2497
  currentToolDurationMs: step.currentToolStartedAt ? Math.max(0, now - step.currentToolStartedAt) : undefined,
2361
2498
  currentPath: step.currentPath,
2362
2499
  elapsedMs: now - (step.startedAt ?? overallStartTime),
2363
- });
2500
+ }));
2364
2501
  appendControlEvent(event);
2365
2502
  return true;
2366
2503
  };
@@ -2378,12 +2515,12 @@ async function runSubagent(
2378
2515
  };
2379
2516
  const recordSteeringLifecycle = (request: SteerRequest, targets: Array<{ index: number; state: SteeringTargetState; reason?: string }>): void => {
2380
2517
  const lifecycle = steeringStatus(statusPayload);
2381
- recordSteeringRequest(lifecycle, { id: request.id, requestedAt: request.ts, source: request.source, message: request.message, targets });
2518
+ recordSteeringRequest(lifecycle, omitUndefinedProperties({ id: request.id, requestedAt: request.ts, source: request.source, message: request.message, targets }));
2382
2519
  for (const target of targets) {
2383
2520
  const step = statusPayload.steps[target.index];
2384
2521
  if (!step) continue;
2385
2522
  step.steering ??= createSteeringStatus();
2386
- recordSteeringRequest(step.steering, { id: request.id, requestedAt: request.ts, source: request.source, message: request.message, targets: [target] });
2523
+ recordSteeringRequest(step.steering, omitUndefinedProperties({ id: request.id, requestedAt: request.ts, source: request.source, message: request.message, targets: [target] }));
2387
2524
  }
2388
2525
  };
2389
2526
  const updateSteeringLifecycleTarget = (
@@ -2473,7 +2610,7 @@ async function runSubagent(
2473
2610
  const late = fs.existsSync(steeringMarkerPath(ack.requestId));
2474
2611
  const now = Date.now();
2475
2612
  if (ack.state === "delivered") {
2476
- updateSteeringLifecycleTarget(ack.requestId, ack.index, late ? "late" : "delivered", now, { reason: late ? "acknowledged after recovery commit" : undefined });
2613
+ updateSteeringLifecycleTarget(ack.requestId, ack.index, late ? "late" : "delivered", now, omitUndefinedProperties({ reason: late ? "acknowledged after recovery commit" : undefined }));
2477
2614
  emitSteeringEvent("subagent.steer.delivered", { type: "steer", id: ack.requestId, ts: now, message: ack.message }, ack.index, { late, message: ack.message });
2478
2615
  } else {
2479
2616
  markSteeringAttention(ack.index);
@@ -2496,8 +2633,8 @@ async function runSubagent(
2496
2633
  const updateStepModel = (flatIndex: number, model: string | undefined, thinking: string | undefined, now = Date.now()): void => {
2497
2634
  const step = statusPayload.steps[flatIndex];
2498
2635
  if (!step) return;
2499
- step.model = model;
2500
- step.thinking = thinking;
2636
+ setOptionalProperty(step, "model", model);
2637
+ setOptionalProperty(step, "thinking", thinking);
2501
2638
  statusPayload.lastUpdate = now;
2502
2639
  writeStatusPayload();
2503
2640
  };
@@ -2594,8 +2731,8 @@ async function runSubagent(
2594
2731
  step.currentTool = event.toolName;
2595
2732
  step.currentToolArgs = extractToolArgsPreview(event.args ?? {});
2596
2733
  step.currentToolStartedAt = now;
2597
- step.currentPath = currentPath;
2598
- pendingToolResults[flatIndex] = { tool: event.toolName, path: currentPath, mutates, startedAt: now };
2734
+ setOptionalProperty(step, "currentPath", currentPath);
2735
+ pendingToolResults[flatIndex] = omitUndefinedProperties({ tool: event.toolName, path: currentPath, mutates, startedAt: now });
2599
2736
  statusPayload.toolCount = (statusPayload.toolCount ?? 0) + 1;
2600
2737
  syncTopLevelCurrentTool();
2601
2738
  if (controlConfig.enabled && isBlockingSupervisorTool(event.toolName, event.args) && step.activityState !== "needs_attention") {
@@ -2604,7 +2741,7 @@ async function runSubagent(
2604
2741
  supervisorAttentionSteps.set(flatIndex, previous);
2605
2742
  currentActivityState = "needs_attention";
2606
2743
  statusPayload.activityState = "needs_attention";
2607
- appendControlEvent(buildControlEvent({
2744
+ appendControlEvent(buildControlEvent(omitUndefinedProperties({
2608
2745
  type: "needs_attention",
2609
2746
  from: previous,
2610
2747
  to: "needs_attention",
@@ -2620,7 +2757,7 @@ async function runSubagent(
2620
2757
  currentTool: step.currentTool,
2621
2758
  currentToolDurationMs: 0,
2622
2759
  currentPath: step.currentPath,
2623
- }));
2760
+ })));
2624
2761
  }
2625
2762
  } else if (event.type === "tool_execution_end") {
2626
2763
  if (step.currentTool) {
@@ -2629,12 +2766,12 @@ async function runSubagent(
2629
2766
  }
2630
2767
  const supervisorPreviousActivity = supervisorAttentionSteps.get(flatIndex);
2631
2768
  const clearedSupervisorAttention = supervisorAttentionSteps.delete(flatIndex);
2632
- step.currentTool = undefined;
2633
- step.currentToolArgs = undefined;
2634
- step.currentToolStartedAt = undefined;
2635
- step.currentPath = undefined;
2769
+ delete step.currentTool;
2770
+ delete step.currentToolArgs;
2771
+ delete step.currentToolStartedAt;
2772
+ delete step.currentPath;
2636
2773
  if (clearedSupervisorAttention && step.activityState === "needs_attention") {
2637
- step.activityState = supervisorPreviousActivity;
2774
+ setOptionalProperty(step, "activityState", supervisorPreviousActivity);
2638
2775
  syncAggregateActivityState();
2639
2776
  }
2640
2777
  syncTopLevelCurrentTool();
@@ -2654,17 +2791,17 @@ async function runSubagent(
2654
2791
  appendRecentStepOutput(step, resultText.split("\n").slice(-10));
2655
2792
  if (toolSnapshot?.mutates && didMutatingToolFail(resultText)) {
2656
2793
  const state = mutatingFailureStates[flatIndex]!;
2657
- recordMutatingFailure(state, {
2794
+ recordMutatingFailure(state, omitUndefinedProperties({
2658
2795
  tool: toolSnapshot.tool,
2659
2796
  path: toolSnapshot.path,
2660
2797
  error: resultText.split("\n").find((line) => line.trim())?.trim().slice(0, 180) ?? "mutating tool failed",
2661
2798
  ts: now,
2662
- }, mutatingFailureWindowMs);
2799
+ }), mutatingFailureWindowMs);
2663
2800
  if (controlConfig.enabled && shouldEscalateMutatingFailures(state, controlConfig.failedToolAttemptsBeforeAttention) && step.activityState !== "needs_attention") {
2664
2801
  const previous = step.activityState;
2665
2802
  step.activityState = "needs_attention";
2666
2803
  statusPayload.activityState = "needs_attention";
2667
- appendControlEvent(buildControlEvent({
2804
+ appendControlEvent(buildControlEvent(omitUndefinedProperties({
2668
2805
  type: "needs_attention",
2669
2806
  from: previous,
2670
2807
  to: "needs_attention",
@@ -2681,7 +2818,7 @@ async function runSubagent(
2681
2818
  currentToolDurationMs: toolSnapshot.startedAt ? Math.max(0, now - toolSnapshot.startedAt) : undefined,
2682
2819
  currentPath: toolSnapshot.path,
2683
2820
  recentFailureSummary: summarizeRecentMutatingFailures(state),
2684
- }));
2821
+ })));
2685
2822
  }
2686
2823
  } else if (toolSnapshot?.mutates) {
2687
2824
  resetMutatingFailureState(mutatingFailureStates[flatIndex]!);
@@ -2730,18 +2867,18 @@ async function runSubagent(
2730
2867
  step.lastActivityAt = lastActivityAt;
2731
2868
  changed = true;
2732
2869
  }
2733
- const idleState = deriveActivityState({
2870
+ const idleState = deriveActivityState(omitUndefinedProperties({
2734
2871
  config: controlConfig,
2735
2872
  startedAt: step.startedAt ?? overallStartTime,
2736
2873
  lastActivityAt,
2737
2874
  currentTool: step.currentTool,
2738
2875
  now,
2739
- });
2876
+ }));
2740
2877
  if (idleState === "needs_attention") {
2741
2878
  const previous = step.activityState;
2742
2879
  step.activityState = "needs_attention";
2743
2880
  if (previous !== "needs_attention") {
2744
- appendControlEvent(buildControlEvent({
2881
+ appendControlEvent(buildControlEvent(omitUndefinedProperties({
2745
2882
  from: previous,
2746
2883
  to: "needs_attention",
2747
2884
  runId: id,
@@ -2749,7 +2886,7 @@ async function runSubagent(
2749
2886
  index,
2750
2887
  ts: now,
2751
2888
  lastActivityAt,
2752
- }));
2889
+ })));
2753
2890
  changed = true;
2754
2891
  }
2755
2892
  } else if (maybeEmitActiveLongRunning(index, now)) {
@@ -2767,7 +2904,7 @@ async function runSubagent(
2767
2904
  : undefined;
2768
2905
  if (nextRunState !== currentActivityState) {
2769
2906
  currentActivityState = nextRunState;
2770
- statusPayload.activityState = nextRunState;
2907
+ setOptionalProperty(statusPayload, "activityState", nextRunState);
2771
2908
  changed = true;
2772
2909
  }
2773
2910
  statusPayload.lastUpdate = now;
@@ -2790,14 +2927,14 @@ async function runSubagent(
2790
2927
  const now = Date.now();
2791
2928
  statusPayload.state = "paused";
2792
2929
  currentActivityState = undefined;
2793
- statusPayload.activityState = undefined;
2930
+ delete statusPayload.activityState;
2794
2931
  statusPayload.lastUpdate = now;
2795
2932
  for (const step of statusPayload.steps) {
2796
2933
  if (step.status === "running") {
2797
2934
  step.status = "paused";
2798
- step.activityState = undefined;
2935
+ delete step.activityState;
2799
2936
  step.endedAt = now;
2800
- step.durationMs = step.startedAt ? now - step.startedAt : undefined;
2937
+ setOptionalProperty(step, "durationMs", step.startedAt ? now - step.startedAt : undefined);
2801
2938
  step.lastActivityAt = now;
2802
2939
  }
2803
2940
  }
@@ -2818,7 +2955,7 @@ async function runSubagent(
2818
2955
  statusPayload.stopped = true;
2819
2956
  statusPayload.error = stopMessage;
2820
2957
  currentActivityState = undefined;
2821
- statusPayload.activityState = undefined;
2958
+ delete statusPayload.activityState;
2822
2959
  statusPayload.lastUpdate = now;
2823
2960
  for (const step of statusPayload.steps) {
2824
2961
  if (step.status !== "running" && step.status !== "pending") continue;
@@ -2826,7 +2963,7 @@ async function runSubagent(
2826
2963
  step.error = stopMessage;
2827
2964
  step.exitCode = 1;
2828
2965
  step.stopped = true;
2829
- step.activityState = undefined;
2966
+ delete step.activityState;
2830
2967
  step.endedAt = now;
2831
2968
  step.durationMs = step.startedAt ? now - step.startedAt : 0;
2832
2969
  step.lastActivityAt = now;
@@ -2851,7 +2988,7 @@ async function runSubagent(
2851
2988
  statusPayload.timedOut = true;
2852
2989
  statusPayload.error = message;
2853
2990
  currentActivityState = undefined;
2854
- statusPayload.activityState = undefined;
2991
+ delete statusPayload.activityState;
2855
2992
  statusPayload.lastUpdate = now;
2856
2993
  for (const step of statusPayload.steps) {
2857
2994
  if (step.status !== "running" && step.status !== "pending") continue;
@@ -2859,7 +2996,7 @@ async function runSubagent(
2859
2996
  step.error = message;
2860
2997
  step.exitCode = 1;
2861
2998
  step.timedOut = true;
2862
- step.activityState = undefined;
2999
+ delete step.activityState;
2863
3000
  step.endedAt = now;
2864
3001
  step.durationMs = step.startedAt ? now - step.startedAt : 0;
2865
3002
  step.lastActivityAt = now;
@@ -2977,7 +3114,7 @@ async function runSubagent(
2977
3114
  statusPayload.state = "paused";
2978
3115
  statusPayload.currentStep = flatIndex;
2979
3116
  statusPayload.checkpoint = checkpoint;
2980
- statusPayload.activityState = undefined;
3117
+ delete statusPayload.activityState;
2981
3118
  statusPayload.lastUpdate = now;
2982
3119
  if (statusStep) {
2983
3120
  statusStep.status = "paused";
@@ -3032,7 +3169,7 @@ async function runSubagent(
3032
3169
  const groupStartFlatIndex = flatIndex;
3033
3170
  let materialized: ReturnType<typeof materializeDynamicParallelStep>;
3034
3171
  try {
3035
- materialized = materializeDynamicParallelStep(step as Parameters<typeof materializeDynamicParallelStep>[0], outputs, stepIndex, { maxItems: config.dynamicFanoutMaxItems, allowRunnerFields: true });
3172
+ materialized = materializeDynamicParallelStep(step as Parameters<typeof materializeDynamicParallelStep>[0], outputs, stepIndex, omitUndefinedProperties({ maxItems: config.dynamicFanoutMaxItems, allowRunnerFields: true }));
3036
3173
  if (materialized.parallel.length > 1 && step.parallel.outputPath && !step.parallel.namespaceOutputPath) {
3037
3174
  throw new DynamicFanoutError(`Dynamic chain step ${stepIndex + 1} materialized ${materialized.parallel.length} items that resolve output to the same path: ${step.parallel.outputPath}. Remove the explicit output path or use an inherited relative agent output so each item can be isolated.`);
3038
3175
  }
@@ -3055,11 +3192,11 @@ async function runSubagent(
3055
3192
  statusPayload.lastUpdate = now;
3056
3193
  markDynamicGraphGroup(stepIndex, "failed", message);
3057
3194
  writeStatusPayload();
3058
- results.push({ agent: step.parallel.agent, context: step.parallel.context, output: message, error: message, success: false, exitCode: 1 });
3195
+ results.push(omitUndefinedProperties({ agent: step.parallel.agent, context: step.parallel.context, output: message, error: message, success: false, exitCode: 1 }));
3059
3196
  break;
3060
3197
  }
3061
3198
 
3062
- const effectiveDynamicGroupAcceptance = resolveEffectiveAcceptance({
3199
+ const effectiveDynamicGroupAcceptance = resolveEffectiveAcceptance(omitUndefinedProperties({
3063
3200
  explicit: step.acceptanceInput,
3064
3201
  agentName: step.parallel.agent,
3065
3202
  acceptanceRole: step.acceptanceRole,
@@ -3068,7 +3205,7 @@ async function runSubagent(
3068
3205
  async: true,
3069
3206
  dynamicGroup: true,
3070
3207
  agentContract: step.agentContract,
3071
- });
3208
+ }));
3072
3209
 
3073
3210
  if (materialized.parallel.length === 0) {
3074
3211
  const now = Date.now();
@@ -3089,7 +3226,7 @@ async function runSubagent(
3089
3226
  }
3090
3227
  previousOutput = "Dynamic fanout produced 0 results.";
3091
3228
  const groupAcceptance = effectiveDynamicGroupAcceptance.explicit && !timedOut && !stopped
3092
- ? await evaluateAcceptance({
3229
+ ? await evaluateAcceptance(omitUndefinedProperties({
3093
3230
  acceptance: effectiveDynamicGroupAcceptance,
3094
3231
  output: "",
3095
3232
  report: aggregateAcceptanceReport({
@@ -3100,7 +3237,7 @@ async function runSubagent(
3100
3237
  signal: combinedAbortSignal([timeoutAbortController.signal, stopAbortController.signal]),
3101
3238
  abortMessage: stopAbortController.signal.aborted ? stopMessage : timeoutMessage ?? "Subagent timed out.",
3102
3239
  reportOptional: isAgentContractV1(step.agentContract),
3103
- })
3240
+ }))
3104
3241
  : undefined;
3105
3242
  const groupStopped = stopped || stopAbortController.signal.aborted;
3106
3243
  const groupTimedOut = !groupStopped && (timedOut || timeoutAbortController.signal.aborted);
@@ -3111,18 +3248,18 @@ async function runSubagent(
3111
3248
  const errorMessage = groupStopped ? stopMessage : groupTimedOut ? timeoutMessage ?? "Subagent timed out." : groupAcceptanceFailure!;
3112
3249
  statusPayload.state = groupStopped ? "stopped" : "failed";
3113
3250
  statusPayload.error = errorMessage;
3114
- statusPayload.stopped = groupStopped ? true : statusPayload.stopped;
3251
+ setOptionalProperty(statusPayload, "stopped", groupStopped ? true : statusPayload.stopped);
3115
3252
  if (placeholder) {
3116
3253
  placeholder.status = groupStopped ? "stopped" : "failed";
3117
3254
  placeholder.error = errorMessage;
3118
3255
  placeholder.exitCode = 1;
3119
- placeholder.timedOut = groupTimedOut ? true : undefined;
3120
- placeholder.stopped = groupStopped ? true : undefined;
3256
+ setOptionalProperty(placeholder, "timedOut", groupTimedOut ? true : undefined);
3257
+ setOptionalProperty(placeholder, "stopped", groupStopped ? true : undefined);
3121
3258
  }
3122
3259
  markDynamicGraphGroup(stepIndex, groupStopped ? "stopped" : "failed", errorMessage, effectiveGroupAcceptance);
3123
3260
  statusPayload.lastUpdate = Date.now();
3124
3261
  writeStatusPayload();
3125
- results.push({ agent: step.parallel.agent, context: step.parallel.context, output: errorMessage, error: errorMessage, success: false, exitCode: 1, timedOut: groupTimedOut ? true : undefined, stopped: groupStopped ? true : undefined, acceptance: effectiveGroupAcceptance });
3262
+ results.push(omitUndefinedProperties({ agent: step.parallel.agent, context: step.parallel.context, output: errorMessage, error: errorMessage, success: false, exitCode: 1, timedOut: groupTimedOut ? true : undefined, stopped: groupStopped ? true : undefined, acceptance: effectiveGroupAcceptance }));
3126
3263
  break;
3127
3264
  }
3128
3265
  flatIndex++;
@@ -3141,10 +3278,10 @@ async function runSubagent(
3141
3278
  : step.parallel.outputPath;
3142
3279
  const taskText = task.task ?? step.parallel.task;
3143
3280
  const materializedTask = step.parallel.namespaceOutputPath ? injectSingleOutputInstruction(taskText, outputPath, step.parallel) : taskText;
3144
- return {
3281
+ return omitUndefinedProperties({
3145
3282
  ...step.parallel,
3146
3283
  task: materializedTask,
3147
- effectiveAcceptance: resolveEffectiveAcceptance({
3284
+ effectiveAcceptance: resolveEffectiveAcceptance(omitUndefinedProperties({
3148
3285
  explicit: step.parallel.acceptanceInput,
3149
3286
  agentName: step.parallel.agent,
3150
3287
  acceptanceRole: step.parallel.acceptanceRole,
@@ -3153,7 +3290,7 @@ async function runSubagent(
3153
3290
  async: true,
3154
3291
  dynamic: step.parallel.acceptanceInput === undefined,
3155
3292
  agentContract: step.parallel.agentContract ?? step.agentContract,
3156
- }),
3293
+ })),
3157
3294
  systemPrompt: step.parallel.namespaceOutputPath ? injectOutputPathSystemPrompt(step.parallel.systemPrompt ?? "", outputPath, step.parallel) : step.parallel.systemPrompt,
3158
3295
  outputPath,
3159
3296
  label: task.label ?? step.parallel.label,
@@ -3161,22 +3298,23 @@ async function runSubagent(
3161
3298
  ...(thinkingOverride ? {
3162
3299
  ...(model ? { model } : {}),
3163
3300
  ...(thinking ? { thinking } : {}),
3164
- ...(step.parallel.modelCandidates ? { modelCandidates: step.parallel.modelCandidates.map((candidate) => applyThinkingSuffix(candidate, thinkingOverride, true)) } : {}),
3301
+ ...(step.parallel.modelCandidates ? { modelCandidates: step.parallel.modelCandidates.flatMap((candidate) => {
3302
+ const resolved = applyThinkingSuffix(candidate, thinkingOverride, true);
3303
+ return resolved ? [resolved] : [];
3304
+ }) } : {}),
3165
3305
  } : {}),
3166
- structuredOutput: undefined,
3167
3306
  structuredOutputSchema: step.parallel.structuredOutputSchema ?? step.parallel.structuredOutput?.schema,
3168
- };
3307
+ });
3169
3308
  });
3170
3309
  const dynamicFlatStepCount = Math.max(statusPayload.steps.length - 1 + dynamicSteps.length, 1);
3171
3310
  const dynamicStatusSteps: RunnerStatusStep[] = dynamicSteps.map((task, itemIndex) => {
3172
- const transcriptPath = resolveAsyncStepTranscriptPath({ artifactsDir, artifactConfig, runId: id, agent: task.agent, flatIndex: groupStartFlatIndex + itemIndex, flatStepCount: dynamicFlatStepCount });
3173
- return {
3311
+ const transcriptPath = resolveAsyncStepTranscriptPath(omitUndefinedProperties({ artifactsDir, artifactConfig, runId: id, agent: task.agent, flatIndex: groupStartFlatIndex + itemIndex, flatStepCount: dynamicFlatStepCount }));
3312
+ return omitUndefinedProperties({
3174
3313
  agent: task.agent,
3175
3314
  ...(statusStepDescription(task.task) ? { description: statusStepDescription(task.task) } : {}),
3176
3315
  ...(task.context ? { context: task.context } : {}),
3177
- phase: task.phase ?? step.phase,
3178
- label: task.label,
3179
- outputName: undefined,
3316
+ ...(task.phase ?? step.phase ? { phase: task.phase ?? step.phase } : {}),
3317
+ ...(task.label ? { label: task.label } : {}),
3180
3318
  structured: Boolean(task.structuredOutputSchema),
3181
3319
  ...(task.agentContract ? { agentContract: task.agentContract } : {}),
3182
3320
  ...(task.launchResolvedExtensions ? { launchResolvedExtensions: task.launchResolvedExtensions } : {}),
@@ -3184,13 +3322,13 @@ async function runSubagent(
3184
3322
  status: "pending",
3185
3323
  ...(task.sessionFile ? { sessionFile: task.sessionFile } : {}),
3186
3324
  ...(transcriptPath ? { transcriptPath } : {}),
3187
- skills: task.skills,
3188
- model: task.model,
3189
- thinking: task.thinking,
3190
- attemptedModels: task.modelCandidates && task.modelCandidates.length > 0 ? task.modelCandidates : task.model ? [task.model] : undefined,
3325
+ ...(task.skills ? { skills: task.skills } : {}),
3326
+ ...(task.model ? { model: task.model } : {}),
3327
+ ...(task.thinking ? { thinking: task.thinking } : {}),
3328
+ ...(task.modelCandidates && task.modelCandidates.length > 0 ? { attemptedModels: task.modelCandidates } : task.model ? { attemptedModels: [task.model] } : {}),
3191
3329
  recentTools: [],
3192
3330
  recentOutput: [],
3193
- };
3331
+ });
3194
3332
  });
3195
3333
  statusPayload.steps.splice(groupStartFlatIndex, 1, ...dynamicStatusSteps);
3196
3334
  if (config.childIntercomTargets) {
@@ -3219,7 +3357,7 @@ async function runSubagent(
3219
3357
  shiftFlatIndexes(statusPayload.workflowGraph.nodes);
3220
3358
  const groupNode = statusPayload.workflowGraph.nodes.find((node) => node.id === `step-${stepIndex}`);
3221
3359
  if (groupNode) {
3222
- groupNode.children = materialized.items.map((item, itemIndex) => ({
3360
+ groupNode.children = materialized.items.map((item, itemIndex) => omitUndefinedProperties({
3223
3361
  id: `step-${stepIndex}-item-${item.idKey}`,
3224
3362
  kind: "agent",
3225
3363
  agent: step.parallel.agent,
@@ -3238,54 +3376,54 @@ async function runSubagent(
3238
3376
  const concurrency = step.concurrency ?? MAX_PARALLEL_CONCURRENCY;
3239
3377
  const failFast = step.failFast ?? false;
3240
3378
  let aborted = false;
3241
- const parallelResults = await mapConcurrent(dynamicSteps, concurrency, async (task, taskIdx) => {
3379
+ const parallelResults = await mapConcurrent(dynamicSteps, concurrency, async (task, taskIdx): Promise<StepResult> => {
3242
3380
  const fi = groupStartFlatIndex + taskIdx;
3243
3381
  refreshUsageBudget();
3244
3382
  if (statusPayload.usageBudget?.exhausted) {
3245
3383
  const skippedAt = Date.now();
3246
3384
  const message = usageBudgetExceededMessage(statusPayload.usageBudget);
3247
- statusPayload.steps[fi].status = "failed";
3248
- statusPayload.steps[fi].error = message;
3249
- statusPayload.steps[fi].startedAt = skippedAt;
3250
- statusPayload.steps[fi].endedAt = skippedAt;
3251
- statusPayload.steps[fi].durationMs = 0;
3252
- statusPayload.steps[fi].exitCode = 1;
3385
+ requiredStatusStep(statusPayload, fi).status = "failed";
3386
+ requiredStatusStep(statusPayload, fi).error = message;
3387
+ requiredStatusStep(statusPayload, fi).startedAt = skippedAt;
3388
+ requiredStatusStep(statusPayload, fi).endedAt = skippedAt;
3389
+ requiredStatusStep(statusPayload, fi).durationMs = 0;
3390
+ requiredStatusStep(statusPayload, fi).exitCode = 1;
3253
3391
  statusPayload.lastUpdate = skippedAt;
3254
3392
  usageBudgetExceeded = true;
3255
3393
  writeStatusPayload();
3256
3394
  appendJsonl(eventsPath, JSON.stringify({ type: "subagent.step.failed", ts: skippedAt, runId: id, stepIndex: fi, agent: task.agent, exitCode: 1, durationMs: 0 }));
3257
- return { agent: task.agent, context: task.context, output: message, error: message, exitCode: 1 as number | null, skipped: true };
3395
+ return omitUndefinedProperties({ agent: task.agent, context: task.context, output: message, error: message, exitCode: 1 as number | null, skipped: true });
3258
3396
  }
3259
3397
  if (timedOut) return timedOutStepResult(task.agent, task.context);
3260
3398
  if (stopped) return stoppedStepResult(task.agent, task.context);
3261
3399
  if (interrupted) return pausedStepResult(task.agent, task.context);
3262
3400
  if (aborted && failFast) {
3263
3401
  const skippedAt = Date.now();
3264
- statusPayload.steps[fi].status = "failed";
3265
- statusPayload.steps[fi].error = "Skipped due to fail-fast";
3266
- statusPayload.steps[fi].startedAt = skippedAt;
3267
- statusPayload.steps[fi].endedAt = skippedAt;
3268
- statusPayload.steps[fi].durationMs = 0;
3269
- statusPayload.steps[fi].exitCode = -1;
3402
+ requiredStatusStep(statusPayload, fi).status = "failed";
3403
+ requiredStatusStep(statusPayload, fi).error = "Skipped due to fail-fast";
3404
+ requiredStatusStep(statusPayload, fi).startedAt = skippedAt;
3405
+ requiredStatusStep(statusPayload, fi).endedAt = skippedAt;
3406
+ requiredStatusStep(statusPayload, fi).durationMs = 0;
3407
+ requiredStatusStep(statusPayload, fi).exitCode = -1;
3270
3408
  statusPayload.lastUpdate = skippedAt;
3271
3409
  writeStatusPayload();
3272
- return { agent: task.agent, context: task.context, output: "(skipped — fail-fast)", exitCode: -1 as number | null, skipped: true };
3410
+ return omitUndefinedProperties({ agent: task.agent, context: task.context, output: "(skipped — fail-fast)", exitCode: -1 as number | null, skipped: true });
3273
3411
  }
3274
3412
  const taskStartTime = Date.now();
3275
3413
  statusPayload.currentStep = fi;
3276
- statusPayload.steps[fi].status = "running";
3277
- statusPayload.steps[fi].error = undefined;
3278
- statusPayload.steps[fi].activityState = undefined;
3279
- resetStepLiveDetail(statusPayload.steps[fi]);
3280
- statusPayload.steps[fi].startedAt = taskStartTime;
3281
- statusPayload.steps[fi].lastActivityAt = taskStartTime;
3414
+ requiredStatusStep(statusPayload, fi).status = "running";
3415
+ delete requiredStatusStep(statusPayload, fi).error;
3416
+ delete requiredStatusStep(statusPayload, fi).activityState;
3417
+ resetStepLiveDetail(requiredStatusStep(statusPayload, fi));
3418
+ requiredStatusStep(statusPayload, fi).startedAt = taskStartTime;
3419
+ requiredStatusStep(statusPayload, fi).lastActivityAt = taskStartTime;
3282
3420
  statusPayload.outputFile = path.join(asyncDir, `output-${fi}.log`);
3283
3421
  statusPayload.lastActivityAt = taskStartTime;
3284
3422
  statusPayload.lastUpdate = taskStartTime;
3285
3423
  writeStatusPayload();
3286
3424
  appendJsonl(eventsPath, JSON.stringify({ type: "subagent.step.started", ts: taskStartTime, runId: id, stepIndex: fi, agent: task.agent }));
3287
3425
  flushPendingStepSteers(fi);
3288
- const singleResult = await runSingleStep(task, {
3426
+ const singleResult = await runSingleStep(task, compactOptional<SingleStepContext>({
3289
3427
  previousOutput, placeholder, cwd, sessionEnabled,
3290
3428
  outputs,
3291
3429
  sessionDir: config.sessionDir ? path.join(config.sessionDir, `dynamic-${stepIndex}-${taskIdx}`) : undefined,
@@ -3313,32 +3451,33 @@ async function runSubagent(
3313
3451
  onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking),
3314
3452
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
3315
3453
  onWriterProcess,
3454
+ onExternalProcess: (process) => updateExternalProcess(fi, process),
3316
3455
  skipAcceptance: () => timedOut || stopped,
3317
- });
3456
+ }));
3318
3457
  const taskEndTime = Date.now();
3319
3458
  const childInterrupted = singleResult.interrupted === true;
3320
3459
  const childStopped = singleResult.stopped === true;
3321
- statusPayload.steps[fi].status = stopped || childStopped ? "stopped" : timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
3322
- statusPayload.steps[fi].endedAt = taskEndTime;
3323
- statusPayload.steps[fi].durationMs = taskEndTime - taskStartTime;
3324
- statusPayload.steps[fi].exitCode = stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
3325
- statusPayload.steps[fi].timedOut = timedOut || singleResult.timedOut ? true : undefined;
3326
- statusPayload.steps[fi].stopped = stopped || childStopped ? true : undefined;
3327
- statusPayload.steps[fi].turnBudget = singleResult.turnBudget;
3328
- statusPayload.steps[fi].turnBudgetExceeded = singleResult.turnBudgetExceeded;
3329
- statusPayload.steps[fi].wrapUpRequested = singleResult.wrapUpRequested;
3330
- statusPayload.steps[fi].toolBudget = singleResult.toolBudget;
3331
- statusPayload.steps[fi].toolBudgetBlocked = singleResult.toolBudgetBlocked;
3460
+ requiredStatusStep(statusPayload, fi).status = stopped || childStopped ? "stopped" : timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
3461
+ requiredStatusStep(statusPayload, fi).endedAt = taskEndTime;
3462
+ requiredStatusStep(statusPayload, fi).durationMs = taskEndTime - taskStartTime;
3463
+ requiredStatusStep(statusPayload, fi).exitCode = stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
3464
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "timedOut", timedOut || singleResult.timedOut ? true : undefined);
3465
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "stopped", stopped || childStopped ? true : undefined);
3466
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "turnBudget", singleResult.turnBudget);
3467
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "turnBudgetExceeded", singleResult.turnBudgetExceeded);
3468
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "wrapUpRequested", singleResult.wrapUpRequested);
3469
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "toolBudget", singleResult.toolBudget);
3470
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "toolBudgetBlocked", singleResult.toolBudgetBlocked);
3332
3471
  if (singleResult.toolBudget) statusPayload.toolBudget = singleResult.toolBudget;
3333
3472
  if (singleResult.toolBudgetBlocked) statusPayload.toolBudgetBlocked = true;
3334
3473
  if (singleResult.turnBudget) statusPayload.turnBudget = singleResult.turnBudget;
3335
3474
  if (singleResult.turnBudgetExceeded) statusPayload.turnBudgetExceeded = true;
3336
3475
  if (singleResult.wrapUpRequested) statusPayload.wrapUpRequested = true;
3337
- statusPayload.steps[fi].model = singleResult.model;
3338
- statusPayload.steps[fi].thinking = resolveEffectiveThinking(singleResult.model, statusPayload.steps[fi].thinking);
3339
- statusPayload.steps[fi].attemptedModels = singleResult.attemptedModels;
3340
- statusPayload.steps[fi].modelAttempts = singleResult.modelAttempts;
3341
- statusPayload.steps[fi].totalCost = singleResult.totalCost;
3476
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "model", singleResult.model);
3477
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "thinking", resolveEffectiveThinking(singleResult.model, requiredStatusStep(statusPayload, fi).thinking));
3478
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "attemptedModels", singleResult.attemptedModels);
3479
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "modelAttempts", singleResult.modelAttempts);
3480
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "totalCost", singleResult.totalCost);
3342
3481
  if (singleResult.totalCost) {
3343
3482
  pendingParallelUsageCost = {
3344
3483
  inputTokens: pendingParallelUsageCost.inputTokens + singleResult.totalCost.inputTokens,
@@ -3347,23 +3486,23 @@ async function runSubagent(
3347
3486
  };
3348
3487
  refreshUsageBudget();
3349
3488
  }
3350
- statusPayload.steps[fi].error = stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
3351
- statusPayload.steps[fi].transcriptPath = singleResult.transcriptPath ?? statusPayload.steps[fi].transcriptPath;
3352
- statusPayload.steps[fi].transcriptError = singleResult.transcriptError;
3353
- statusPayload.steps[fi].agentContract = singleResult.agentContract;
3354
- statusPayload.steps[fi].launchContractDigest = singleResult.launchContractDigest;
3355
- statusPayload.steps[fi].launchResolvedExtensions = singleResult.launchResolvedExtensions;
3356
- statusPayload.steps[fi].runtimeAcknowledgedExtensions = singleResult.runtimeAcknowledgedExtensions;
3357
- statusPayload.steps[fi].effects = singleResult.effects;
3358
- statusPayload.steps[fi].execution = singleResult.execution;
3359
- statusPayload.steps[fi].review = singleResult.review;
3360
- statusPayload.steps[fi].structuredOutput = singleResult.structuredOutput;
3361
- statusPayload.steps[fi].structuredOutputPath = singleResult.structuredOutputPath;
3362
- statusPayload.steps[fi].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
3363
- statusPayload.steps[fi].acceptance = singleResult.acceptance;
3364
- statusPayload.steps[fi].watchdog = singleResult.watchdog;
3365
- statusPayload.steps[fi].capabilityCeiling = singleResult.capabilityCeiling;
3366
- statusPayload.steps[fi].capabilityAudit = singleResult.capabilityAudit;
3489
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "error", stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error);
3490
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "transcriptPath", singleResult.transcriptPath ?? requiredStatusStep(statusPayload, fi).transcriptPath);
3491
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "transcriptError", singleResult.transcriptError);
3492
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "agentContract", singleResult.agentContract);
3493
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "launchContractDigest", singleResult.launchContractDigest);
3494
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "launchResolvedExtensions", singleResult.launchResolvedExtensions);
3495
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "runtimeAcknowledgedExtensions", singleResult.runtimeAcknowledgedExtensions);
3496
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "effects", singleResult.effects);
3497
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "execution", singleResult.execution);
3498
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "review", singleResult.review);
3499
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "structuredOutput", singleResult.structuredOutput);
3500
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "structuredOutputPath", singleResult.structuredOutputPath);
3501
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "structuredOutputSchemaPath", singleResult.structuredOutputSchemaPath);
3502
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "acceptance", singleResult.acceptance);
3503
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "watchdog", singleResult.watchdog);
3504
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "capabilityCeiling", singleResult.capabilityCeiling);
3505
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "capabilityAudit", singleResult.capabilityAudit);
3367
3506
  if (singleResult.capabilityCeiling) statusPayload.capabilityCeiling = singleResult.capabilityCeiling;
3368
3507
  if (singleResult.capabilityAudit) statusPayload.capabilityAudit = singleResult.capabilityAudit;
3369
3508
  statusPayload.lastUpdate = taskEndTime;
@@ -3380,7 +3519,7 @@ async function runSubagent(
3380
3519
 
3381
3520
  flatIndex += dynamicSteps.length;
3382
3521
  for (const pr of parallelResults) {
3383
- results.push({
3522
+ results.push(omitUndefinedProperties({
3384
3523
  agent: pr.agent,
3385
3524
  context: pr.context,
3386
3525
  agentContract: pr.agentContract,
@@ -3421,7 +3560,7 @@ async function runSubagent(
3421
3560
  watchdog: pr.watchdog,
3422
3561
  capabilityCeiling: pr.capabilityCeiling,
3423
3562
  capabilityAudit: pr.capabilityAudit,
3424
- });
3563
+ }));
3425
3564
  }
3426
3565
  pendingParallelUsageCost = { inputTokens: 0, outputTokens: 0, costUsd: 0 };
3427
3566
  refreshUsageBudget();
@@ -3432,9 +3571,9 @@ async function runSubagent(
3432
3571
  .filter(({ result, task }) => isAgentContractV1(task?.agentContract ?? step.agentContract) && task?.gateOn === "acceptance" && result.acceptance?.status === "rejected");
3433
3572
  if (acceptanceFailures.length > 0) {
3434
3573
  const message = acceptanceFailures
3435
- .map(({ result, originalIndex }) => `Dynamic item ${originalIndex + 1} (${result.agent}, key ${materialized.items[originalIndex]?.key ?? originalIndex}) acceptance rejected: ${acceptanceFailureMessage(result.acceptance) ?? "acceptance rejected"}`)
3574
+ .map(({ result, originalIndex }) => `Dynamic item ${originalIndex + 1} (${result.agent}, key ${materialized.items[originalIndex]?.key ?? originalIndex}) acceptance rejected: ${(result.acceptance ? acceptanceFailureMessage(result.acceptance) : undefined) ?? "acceptance rejected"}`)
3436
3575
  .join("\n");
3437
- results.push({ agent: step.parallel.agent, context: step.parallel.context, output: message, error: message, success: false, exitCode: 1, structuredOutput: collection });
3576
+ results.push(omitUndefinedProperties({ agent: step.parallel.agent, context: step.parallel.context, output: message, error: message, success: false, exitCode: 1, structuredOutput: collection }));
3438
3577
  statusPayload.error = message;
3439
3578
  markDynamicGraphGroup(stepIndex, "failed", message);
3440
3579
  }
@@ -3449,7 +3588,7 @@ async function runSubagent(
3449
3588
  };
3450
3589
  statusPayload.outputs = outputs;
3451
3590
  const groupAcceptance = !timedOut && !stopped
3452
- ? await evaluateAcceptance({
3591
+ ? await evaluateAcceptance(omitUndefinedProperties({
3453
3592
  acceptance: effectiveDynamicGroupAcceptance,
3454
3593
  output: "",
3455
3594
  report: aggregateAcceptanceReport({
@@ -3460,7 +3599,7 @@ async function runSubagent(
3460
3599
  signal: combinedAbortSignal([timeoutAbortController.signal, stopAbortController.signal]),
3461
3600
  abortMessage: stopAbortController.signal.aborted ? stopMessage : timeoutMessage ?? "Subagent timed out.",
3462
3601
  reportOptional: isAgentContractV1(step.agentContract),
3463
- })
3602
+ }))
3464
3603
  : undefined;
3465
3604
  const groupStopped = stopped || stopAbortController.signal.aborted;
3466
3605
  const groupTimedOut = !groupStopped && (timedOut || timeoutAbortController.signal.aborted);
@@ -3469,7 +3608,7 @@ async function runSubagent(
3469
3608
  const groupError = groupStopped ? stopMessage : groupTimedOut ? timeoutMessage ?? "Subagent timed out." : groupAcceptanceFailure;
3470
3609
  markDynamicGraphGroup(stepIndex, groupError ? groupStopped ? "stopped" : "failed" : "completed", groupError, effectiveGroupAcceptance);
3471
3610
  if (groupError) {
3472
- results.push({
3611
+ results.push(omitUndefinedProperties({
3473
3612
  agent: step.parallel.agent,
3474
3613
  output: groupError,
3475
3614
  error: groupError,
@@ -3479,19 +3618,19 @@ async function runSubagent(
3479
3618
  stopped: groupStopped ? true : undefined,
3480
3619
  structuredOutput: collection,
3481
3620
  acceptance: effectiveGroupAcceptance,
3482
- });
3621
+ }));
3483
3622
  statusPayload.error = groupError;
3484
- statusPayload.stopped = groupStopped ? true : statusPayload.stopped;
3623
+ setOptionalProperty(statusPayload, "stopped", groupStopped ? true : statusPayload.stopped);
3485
3624
  }
3486
3625
  } catch (error) {
3487
3626
  const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
3488
- results.push({ agent: step.parallel.agent, context: step.parallel.context, output: message, error: message, success: false, exitCode: 1, structuredOutput: collection });
3627
+ results.push(omitUndefinedProperties({ agent: step.parallel.agent, context: step.parallel.context, output: message, error: message, success: false, exitCode: 1, structuredOutput: collection }));
3489
3628
  statusPayload.error = message;
3490
3629
  markDynamicGraphGroup(stepIndex, "failed", message);
3491
3630
  }
3492
3631
  }
3493
3632
  previousOutput = aggregateParallelOutputs(
3494
- parallelResults.map((r, i) => ({
3633
+ parallelResults.map((r, i) => omitUndefinedProperties({
3495
3634
  agent: r.agent,
3496
3635
  taskIndex: i,
3497
3636
  output: r.output,
@@ -3543,13 +3682,13 @@ async function runSubagent(
3543
3682
  break;
3544
3683
  }
3545
3684
  try {
3546
- worktreeSetup = createWorktrees(cwd, `${id}-s${stepIndex}`, group.parallel.length, {
3685
+ worktreeSetup = createWorktrees(cwd, `${id}-s${stepIndex}`, group.parallel.length, omitUndefinedProperties({
3547
3686
  agents: group.parallel.map((task) => task.agent),
3548
3687
  setupHook: config.worktreeSetupHook
3549
- ? { hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs }
3688
+ ? omitUndefinedProperties({ hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs })
3550
3689
  : undefined,
3551
3690
  baseDir: config.worktreeBaseDir,
3552
- });
3691
+ }));
3553
3692
  } catch (error) {
3554
3693
  const setupError = error instanceof Error ? error.message : String(error);
3555
3694
  const failedAt = Date.now();
@@ -3572,6 +3711,18 @@ async function runSubagent(
3572
3711
  }
3573
3712
 
3574
3713
  try {
3714
+ if (worktreeSetup) {
3715
+ writePendingParallelHandoff({
3716
+ manifestPath: parallelHandoffPath(asyncDir),
3717
+ runId: id,
3718
+ mode: (config.resultMode ?? statusPayload.mode) === "parallel" ? "parallel" : "chain",
3719
+ source: "async",
3720
+ cwd,
3721
+ stepIndex,
3722
+ flatStartIndex: groupStartFlatIndex,
3723
+ setup: worktreeSetup,
3724
+ });
3725
+ }
3575
3726
  if (group.worktree) ensureParallelProgressFile(cwd, group);
3576
3727
  const groupStartTime = Date.now();
3577
3728
  markParallelGroupRunning({
@@ -3588,57 +3739,57 @@ async function runSubagent(
3588
3739
  const parallelResults = await mapConcurrent(
3589
3740
  group.parallel,
3590
3741
  concurrency,
3591
- async (task, taskIdx) => {
3742
+ async (task, taskIdx): Promise<StepResult> => {
3592
3743
  const fi = groupStartFlatIndex + taskIdx;
3593
3744
  refreshUsageBudget();
3594
3745
  if (statusPayload.usageBudget?.exhausted) {
3595
3746
  const skippedAt = Date.now();
3596
3747
  const message = usageBudgetExceededMessage(statusPayload.usageBudget);
3597
- statusPayload.steps[fi].status = "failed";
3598
- statusPayload.steps[fi].error = message;
3599
- statusPayload.steps[fi].startedAt = skippedAt;
3600
- statusPayload.steps[fi].endedAt = skippedAt;
3601
- statusPayload.steps[fi].durationMs = 0;
3602
- statusPayload.steps[fi].exitCode = 1;
3603
- statusPayload.steps[fi].activityState = undefined;
3748
+ requiredStatusStep(statusPayload, fi).status = "failed";
3749
+ requiredStatusStep(statusPayload, fi).error = message;
3750
+ requiredStatusStep(statusPayload, fi).startedAt = skippedAt;
3751
+ requiredStatusStep(statusPayload, fi).endedAt = skippedAt;
3752
+ requiredStatusStep(statusPayload, fi).durationMs = 0;
3753
+ requiredStatusStep(statusPayload, fi).exitCode = 1;
3754
+ delete requiredStatusStep(statusPayload, fi).activityState;
3604
3755
  statusPayload.lastUpdate = skippedAt;
3605
3756
  usageBudgetExceeded = true;
3606
3757
  writeStatusPayload();
3607
3758
  appendJsonl(eventsPath, JSON.stringify({
3608
3759
  type: "subagent.step.failed", ts: skippedAt, runId: id, stepIndex: fi, agent: task.agent, exitCode: 1, durationMs: 0,
3609
3760
  }));
3610
- return { agent: task.agent, context: task.context, output: message, error: message, exitCode: 1 as number | null, skipped: true };
3761
+ return omitUndefinedProperties({ agent: task.agent, context: task.context, output: message, error: message, exitCode: 1 as number | null, skipped: true });
3611
3762
  }
3612
3763
  if (timedOut) return timedOutStepResult(task.agent, task.context);
3613
3764
  if (stopped) return stoppedStepResult(task.agent, task.context);
3614
3765
  if (interrupted) return pausedStepResult(task.agent, task.context);
3615
3766
  if (aborted && failFast) {
3616
3767
  const skippedAt = Date.now();
3617
- statusPayload.steps[fi].status = "failed";
3618
- statusPayload.steps[fi].error = "Skipped due to fail-fast";
3619
- statusPayload.steps[fi].startedAt = skippedAt;
3620
- statusPayload.steps[fi].endedAt = skippedAt;
3621
- statusPayload.steps[fi].durationMs = 0;
3622
- statusPayload.steps[fi].exitCode = -1;
3623
- statusPayload.steps[fi].activityState = undefined;
3768
+ requiredStatusStep(statusPayload, fi).status = "failed";
3769
+ requiredStatusStep(statusPayload, fi).error = "Skipped due to fail-fast";
3770
+ requiredStatusStep(statusPayload, fi).startedAt = skippedAt;
3771
+ requiredStatusStep(statusPayload, fi).endedAt = skippedAt;
3772
+ requiredStatusStep(statusPayload, fi).durationMs = 0;
3773
+ requiredStatusStep(statusPayload, fi).exitCode = -1;
3774
+ delete requiredStatusStep(statusPayload, fi).activityState;
3624
3775
  statusPayload.lastUpdate = skippedAt;
3625
3776
  writeStatusPayload();
3626
3777
  appendJsonl(eventsPath, JSON.stringify({
3627
3778
  type: "subagent.step.failed", ts: skippedAt, runId: id, stepIndex: fi, agent: task.agent, exitCode: -1, durationMs: 0,
3628
3779
  }));
3629
- return { agent: task.agent, context: task.context, output: "(skipped — fail-fast)", exitCode: -1 as number | null, skipped: true };
3780
+ return omitUndefinedProperties({ agent: task.agent, context: task.context, output: "(skipped — fail-fast)", exitCode: -1 as number | null, skipped: true });
3630
3781
  }
3631
3782
 
3632
3783
  const taskStartTime = Date.now();
3633
3784
  statusPayload.currentStep = fi;
3634
- statusPayload.steps[fi].status = "running";
3635
- statusPayload.steps[fi].error = undefined;
3636
- statusPayload.steps[fi].activityState = undefined;
3637
- resetStepLiveDetail(statusPayload.steps[fi]);
3638
- statusPayload.steps[fi].startedAt = taskStartTime;
3639
- statusPayload.steps[fi].endedAt = undefined;
3640
- statusPayload.steps[fi].durationMs = undefined;
3641
- statusPayload.steps[fi].lastActivityAt = taskStartTime;
3785
+ requiredStatusStep(statusPayload, fi).status = "running";
3786
+ delete requiredStatusStep(statusPayload, fi).error;
3787
+ delete requiredStatusStep(statusPayload, fi).activityState;
3788
+ resetStepLiveDetail(requiredStatusStep(statusPayload, fi));
3789
+ requiredStatusStep(statusPayload, fi).startedAt = taskStartTime;
3790
+ delete requiredStatusStep(statusPayload, fi).endedAt;
3791
+ delete requiredStatusStep(statusPayload, fi).durationMs;
3792
+ requiredStatusStep(statusPayload, fi).lastActivityAt = taskStartTime;
3642
3793
  statusPayload.outputFile = path.join(asyncDir, `output-${fi}.log`);
3643
3794
  statusPayload.lastActivityAt = taskStartTime;
3644
3795
  statusPayload.lastUpdate = taskStartTime;
@@ -3654,7 +3805,7 @@ async function runSubagent(
3654
3805
  const { taskForRun, taskCwd } = prepareParallelTaskRun(task, cwd, worktreeSetup, taskIdx);
3655
3806
  flushPendingStepSteers(fi);
3656
3807
 
3657
- const singleResult = await runSingleStep(taskForRun, {
3808
+ const singleResult = await runSingleStep(taskForRun, compactOptional<SingleStepContext>({
3658
3809
  previousOutput, placeholder, cwd: taskCwd, sessionEnabled,
3659
3810
  outputs,
3660
3811
  sessionDir: taskSessionDir,
@@ -3682,8 +3833,9 @@ async function runSubagent(
3682
3833
  onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking),
3683
3834
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
3684
3835
  onWriterProcess,
3836
+ onExternalProcess: (process) => updateExternalProcess(fi, process),
3685
3837
  skipAcceptance: () => timedOut || stopped,
3686
- });
3838
+ }));
3687
3839
  if (task.sessionFile) {
3688
3840
  latestSessionFile = task.sessionFile;
3689
3841
  }
@@ -3693,27 +3845,27 @@ async function runSubagent(
3693
3845
  const childInterrupted = singleResult.interrupted === true;
3694
3846
  const childStopped = singleResult.stopped === true;
3695
3847
 
3696
- statusPayload.steps[fi].status = stopped || childStopped ? "stopped" : timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
3697
- statusPayload.steps[fi].endedAt = taskEndTime;
3698
- statusPayload.steps[fi].durationMs = taskDuration;
3699
- statusPayload.steps[fi].exitCode = stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
3700
- statusPayload.steps[fi].timedOut = timedOut || singleResult.timedOut ? true : undefined;
3701
- statusPayload.steps[fi].stopped = stopped || childStopped ? true : undefined;
3702
- statusPayload.steps[fi].turnBudget = singleResult.turnBudget;
3703
- statusPayload.steps[fi].turnBudgetExceeded = singleResult.turnBudgetExceeded;
3704
- statusPayload.steps[fi].wrapUpRequested = singleResult.wrapUpRequested;
3705
- statusPayload.steps[fi].toolBudget = singleResult.toolBudget;
3706
- statusPayload.steps[fi].toolBudgetBlocked = singleResult.toolBudgetBlocked;
3848
+ requiredStatusStep(statusPayload, fi).status = stopped || childStopped ? "stopped" : timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
3849
+ requiredStatusStep(statusPayload, fi).endedAt = taskEndTime;
3850
+ requiredStatusStep(statusPayload, fi).durationMs = taskDuration;
3851
+ requiredStatusStep(statusPayload, fi).exitCode = stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
3852
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "timedOut", timedOut || singleResult.timedOut ? true : undefined);
3853
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "stopped", stopped || childStopped ? true : undefined);
3854
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "turnBudget", singleResult.turnBudget);
3855
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "turnBudgetExceeded", singleResult.turnBudgetExceeded);
3856
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "wrapUpRequested", singleResult.wrapUpRequested);
3857
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "toolBudget", singleResult.toolBudget);
3858
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "toolBudgetBlocked", singleResult.toolBudgetBlocked);
3707
3859
  if (singleResult.toolBudget) statusPayload.toolBudget = singleResult.toolBudget;
3708
3860
  if (singleResult.toolBudgetBlocked) statusPayload.toolBudgetBlocked = true;
3709
3861
  if (singleResult.turnBudget) statusPayload.turnBudget = singleResult.turnBudget;
3710
3862
  if (singleResult.turnBudgetExceeded) statusPayload.turnBudgetExceeded = true;
3711
3863
  if (singleResult.wrapUpRequested) statusPayload.wrapUpRequested = true;
3712
- statusPayload.steps[fi].model = singleResult.model;
3713
- statusPayload.steps[fi].thinking = resolveEffectiveThinking(singleResult.model, statusPayload.steps[fi].thinking);
3714
- statusPayload.steps[fi].attemptedModels = singleResult.attemptedModels;
3715
- statusPayload.steps[fi].modelAttempts = singleResult.modelAttempts;
3716
- statusPayload.steps[fi].totalCost = singleResult.totalCost;
3864
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "model", singleResult.model);
3865
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "thinking", resolveEffectiveThinking(singleResult.model, requiredStatusStep(statusPayload, fi).thinking));
3866
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "attemptedModels", singleResult.attemptedModels);
3867
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "modelAttempts", singleResult.modelAttempts);
3868
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "totalCost", singleResult.totalCost);
3717
3869
  if (singleResult.totalCost) {
3718
3870
  pendingParallelUsageCost = {
3719
3871
  inputTokens: pendingParallelUsageCost.inputTokens + singleResult.totalCost.inputTokens,
@@ -3722,22 +3874,22 @@ async function runSubagent(
3722
3874
  };
3723
3875
  refreshUsageBudget();
3724
3876
  }
3725
- statusPayload.steps[fi].error = stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
3726
- statusPayload.steps[fi].transcriptPath = singleResult.transcriptPath ?? statusPayload.steps[fi].transcriptPath;
3727
- statusPayload.steps[fi].transcriptError = singleResult.transcriptError;
3728
- statusPayload.steps[fi].agentContract = singleResult.agentContract;
3729
- statusPayload.steps[fi].launchResolvedExtensions = singleResult.launchResolvedExtensions;
3730
- statusPayload.steps[fi].runtimeAcknowledgedExtensions = singleResult.runtimeAcknowledgedExtensions;
3731
- statusPayload.steps[fi].effects = singleResult.effects;
3732
- statusPayload.steps[fi].execution = singleResult.execution;
3733
- statusPayload.steps[fi].review = singleResult.review;
3734
- statusPayload.steps[fi].structuredOutput = singleResult.structuredOutput;
3735
- statusPayload.steps[fi].structuredOutputPath = singleResult.structuredOutputPath;
3736
- statusPayload.steps[fi].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
3737
- statusPayload.steps[fi].acceptance = singleResult.acceptance;
3738
- statusPayload.steps[fi].watchdog = singleResult.watchdog;
3739
- statusPayload.steps[fi].capabilityCeiling = singleResult.capabilityCeiling;
3740
- statusPayload.steps[fi].capabilityAudit = singleResult.capabilityAudit;
3877
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "error", stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error);
3878
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "transcriptPath", singleResult.transcriptPath ?? requiredStatusStep(statusPayload, fi).transcriptPath);
3879
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "transcriptError", singleResult.transcriptError);
3880
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "agentContract", singleResult.agentContract);
3881
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "launchResolvedExtensions", singleResult.launchResolvedExtensions);
3882
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "runtimeAcknowledgedExtensions", singleResult.runtimeAcknowledgedExtensions);
3883
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "effects", singleResult.effects);
3884
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "execution", singleResult.execution);
3885
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "review", singleResult.review);
3886
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "structuredOutput", singleResult.structuredOutput);
3887
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "structuredOutputPath", singleResult.structuredOutputPath);
3888
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "structuredOutputSchemaPath", singleResult.structuredOutputSchemaPath);
3889
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "acceptance", singleResult.acceptance);
3890
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "watchdog", singleResult.watchdog);
3891
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "capabilityCeiling", singleResult.capabilityCeiling);
3892
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "capabilityAudit", singleResult.capabilityAudit);
3741
3893
  if (singleResult.capabilityCeiling) statusPayload.capabilityCeiling = singleResult.capabilityCeiling;
3742
3894
  if (singleResult.capabilityAudit) statusPayload.capabilityAudit = singleResult.capabilityAudit;
3743
3895
  statusPayload.lastUpdate = taskEndTime;
@@ -3750,8 +3902,8 @@ async function runSubagent(
3750
3902
  exitCode: stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode, durationMs: taskDuration,
3751
3903
  }));
3752
3904
  if (singleResult.completionGuardTriggered) {
3753
- const event = buildControlEvent({
3754
- from: statusPayload.steps[fi].activityState,
3905
+ const event = buildControlEvent(omitUndefinedProperties({
3906
+ from: requiredStatusStep(statusPayload, fi).activityState,
3755
3907
  to: "needs_attention",
3756
3908
  runId: id,
3757
3909
  agent: task.agent,
@@ -3759,7 +3911,7 @@ async function runSubagent(
3759
3911
  ts: taskEndTime,
3760
3912
  message: `${task.agent} completed without making edits for an implementation task`,
3761
3913
  reason: "completion_guard",
3762
- });
3914
+ }));
3763
3915
  appendControlEvent(event);
3764
3916
  }
3765
3917
 
@@ -3778,7 +3930,7 @@ async function runSubagent(
3778
3930
  : null;
3779
3931
  const taskTokens = sessionTokens ?? tokenUsageFromAttempts(parallelResults[t]?.modelAttempts);
3780
3932
  if (!taskTokens) continue;
3781
- statusPayload.steps[fi].tokens = taskTokens;
3933
+ requiredStatusStep(statusPayload, fi).tokens = taskTokens;
3782
3934
  previousCumulativeTokens = {
3783
3935
  input: previousCumulativeTokens.input + taskTokens.input,
3784
3936
  output: previousCumulativeTokens.output + taskTokens.output,
@@ -3790,7 +3942,7 @@ async function runSubagent(
3790
3942
  writeStatusPayload();
3791
3943
 
3792
3944
  for (const pr of parallelResults) {
3793
- results.push({
3945
+ results.push(omitUndefinedProperties({
3794
3946
  agent: pr.agent,
3795
3947
  context: pr.context,
3796
3948
  agentContract: pr.agentContract,
@@ -3828,7 +3980,7 @@ async function runSubagent(
3828
3980
  structuredOutputSchemaPath: pr.structuredOutputSchemaPath,
3829
3981
  acceptance: pr.acceptance,
3830
3982
  watchdog: pr.watchdog,
3831
- });
3983
+ }));
3832
3984
  }
3833
3985
  pendingParallelUsageCost = { inputTokens: 0, outputTokens: 0, costUsd: 0 };
3834
3986
  refreshUsageBudget();
@@ -3843,7 +3995,7 @@ async function runSubagent(
3843
3995
  statusPayload.outputs = outputs;
3844
3996
 
3845
3997
  previousOutput = aggregateParallelOutputs(
3846
- parallelResults.map((r) => ({
3998
+ parallelResults.map((r) => omitUndefinedProperties({
3847
3999
  agent: r.agent,
3848
4000
  output: r.output,
3849
4001
  exitCode: r.exitCode,
@@ -3856,35 +4008,37 @@ async function runSubagent(
3856
4008
  const captured = captureParallelWorktreeDiffs(worktreeSetup, asyncDir, stepIndex, group);
3857
4009
  if (captured.summary) previousOutput = `${previousOutput}\n\n${captured.summary}`;
3858
4010
  worktreeFinalized = true;
3859
- const cleanup = cleanupWorktrees(worktreeSetup);
4011
+ const manifestPath = parallelHandoffPath(asyncDir);
4012
+ const handoff = {
4013
+ manifestPath,
4014
+ runId: id,
4015
+ mode: (config.resultMode ?? statusPayload.mode) === "parallel" ? "parallel" as const : "chain" as const,
4016
+ source: "async" as const,
4017
+ cwd,
4018
+ stepIndex,
4019
+ flatStartIndex: groupStartFlatIndex,
4020
+ setup: worktreeSetup,
4021
+ diffs: captured.diffs,
4022
+ results: parallelResults.map((result) => ({
4023
+ agent: result.agent,
4024
+ status: result.stopped || (result.exitCode !== 0 && isUnexplainedProcessSignal(omitUndefinedProperties({
4025
+ processSignal: result.processSignal,
4026
+ interrupted: result.interrupted,
4027
+ timedOut: result.timedOut,
4028
+ stopped: result.stopped,
4029
+ turnBudgetExceeded: result.turnBudgetExceeded,
4030
+ }))) ? "stopped" as const : result.interrupted ? "paused" as const : result.exitCode === 0 ? "completed" as const : "failed" as const,
4031
+ summary: result.output || result.error || "(no output)",
4032
+ ...(result.artifactPaths?.outputPath ? { outputPath: result.artifactPaths.outputPath } : {}),
4033
+ ...(result.structuredOutput !== undefined ? { structuredOutput: result.structuredOutput } : {}),
4034
+ ...(result.structuredOutputPath ? { structuredOutputPath: result.structuredOutputPath } : {}),
4035
+ ...(result.sessionFile ? { sessionPath: result.sessionFile } : {}),
4036
+ })),
4037
+ };
3860
4038
  try {
3861
- statusPayload.parallelHandoff = writeParallelHandoffGroup({
3862
- manifestPath: parallelHandoffPath(asyncDir),
3863
- runId: id,
3864
- mode: (config.resultMode ?? statusPayload.mode) === "parallel" ? "parallel" : "chain",
3865
- source: "async",
3866
- cwd,
3867
- stepIndex,
3868
- flatStartIndex: groupStartFlatIndex,
3869
- setup: worktreeSetup,
3870
- diffs: captured.diffs,
3871
- cleanup,
3872
- results: parallelResults.map((result) => ({
3873
- agent: result.agent,
3874
- status: result.stopped || (result.exitCode !== 0 && isUnexplainedProcessSignal({
3875
- processSignal: result.processSignal,
3876
- interrupted: result.interrupted,
3877
- timedOut: result.timedOut,
3878
- stopped: result.stopped,
3879
- turnBudgetExceeded: result.turnBudgetExceeded,
3880
- })) ? "stopped" : result.interrupted ? "paused" : result.exitCode === 0 ? "completed" : "failed",
3881
- summary: result.output || result.error || "(no output)",
3882
- ...(result.artifactPaths?.outputPath ? { outputPath: result.artifactPaths.outputPath } : {}),
3883
- ...(result.structuredOutput !== undefined ? { structuredOutput: result.structuredOutput } : {}),
3884
- ...(result.structuredOutputPath ? { structuredOutputPath: result.structuredOutputPath } : {}),
3885
- ...(result.sessionFile ? { sessionPath: result.sessionFile } : {}),
3886
- })),
3887
- });
4039
+ writeParallelHandoffGroup(handoff);
4040
+ const cleanup = cleanupWorktrees(worktreeSetup, { kind: "preserve", capturedDiffs: captured.diffs, handoffManifestPath: manifestPath });
4041
+ statusPayload.parallelHandoff = writeParallelHandoffGroup({ ...handoff, cleanup });
3888
4042
  previousOutput = `${previousOutput}\n\n${formatParallelHandoffReference(statusPayload.parallelHandoff)}`;
3889
4043
  } catch (error) {
3890
4044
  previousOutput = `${previousOutput}\n\n${formatParallelHandoffError(error)}`;
@@ -3905,7 +4059,7 @@ async function runSubagent(
3905
4059
  .map((result, index) => ({ result, index, task: group.parallel[index] }))
3906
4060
  .find(({ result, task }) => isAgentContractV1(task?.agentContract) && task?.gateOn === "acceptance" && result.acceptance?.status === "rejected");
3907
4061
  if (acceptanceGateFailure) {
3908
- statusPayload.error = acceptanceFailureMessage(acceptanceGateFailure.result.acceptance) ?? "Parallel acceptance gate rejected the step.";
4062
+ statusPayload.error = (acceptanceGateFailure.result.acceptance ? acceptanceFailureMessage(acceptanceGateFailure.result.acceptance) : undefined) ?? "Parallel acceptance gate rejected the step.";
3909
4063
  writeStatusPayload();
3910
4064
  break;
3911
4065
  }
@@ -3919,13 +4073,13 @@ async function runSubagent(
3919
4073
  const seqStep = step as SubagentStep;
3920
4074
  const stepStartTime = Date.now();
3921
4075
  statusPayload.currentStep = flatIndex;
3922
- statusPayload.steps[flatIndex].status = "running";
3923
- statusPayload.steps[flatIndex].activityState = undefined;
3924
- statusPayload.activityState = undefined;
3925
- resetStepLiveDetail(statusPayload.steps[flatIndex]);
3926
- statusPayload.steps[flatIndex].skills = seqStep.skills;
3927
- statusPayload.steps[flatIndex].startedAt = stepStartTime;
3928
- statusPayload.steps[flatIndex].lastActivityAt = stepStartTime;
4076
+ requiredStatusStep(statusPayload, flatIndex).status = "running";
4077
+ delete requiredStatusStep(statusPayload, flatIndex).activityState;
4078
+ delete statusPayload.activityState;
4079
+ resetStepLiveDetail(requiredStatusStep(statusPayload, flatIndex));
4080
+ setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "skills", seqStep.skills);
4081
+ requiredStatusStep(statusPayload, flatIndex).startedAt = stepStartTime;
4082
+ requiredStatusStep(statusPayload, flatIndex).lastActivityAt = stepStartTime;
3929
4083
  statusPayload.lastActivityAt = stepStartTime;
3930
4084
  statusPayload.lastUpdate = stepStartTime;
3931
4085
  statusPayload.outputFile = path.join(asyncDir, `output-${flatIndex}.log`);
@@ -3940,7 +4094,7 @@ async function runSubagent(
3940
4094
  }));
3941
4095
 
3942
4096
  flushPendingStepSteers(flatIndex);
3943
- const singleResult = await runSingleStep(seqStep, {
4097
+ const singleResult = await runSingleStep(seqStep, compactOptional<SingleStepContext>({
3944
4098
  previousOutput, placeholder, cwd, sessionEnabled,
3945
4099
  outputs: statusPayload.mode === "single" ? undefined : outputs,
3946
4100
  sessionDir: config.sessionDir,
@@ -3968,15 +4122,16 @@ async function runSubagent(
3968
4122
  onAttemptStart: (attempt) => updateStepModel(flatIndex, attempt.model, attempt.thinking),
3969
4123
  onChildEvent: (event) => updateStepFromChildEvent(flatIndex, event),
3970
4124
  onWriterProcess,
4125
+ onExternalProcess: (process) => updateExternalProcess(flatIndex, process),
3971
4126
  skipAcceptance: () => timedOut || stopped,
3972
- });
4127
+ }));
3973
4128
  if (seqStep.sessionFile) {
3974
4129
  latestSessionFile = seqStep.sessionFile;
3975
4130
  }
3976
4131
 
3977
4132
  previousOutput = singleResult.output;
3978
4133
  const childStopped = singleResult.stopped === true;
3979
- results.push({
4134
+ results.push(omitUndefinedProperties({
3980
4135
  agent: singleResult.agent,
3981
4136
  context: singleResult.context,
3982
4137
  agentContract: singleResult.agentContract,
@@ -4016,7 +4171,9 @@ async function runSubagent(
4016
4171
  wrapUpRequested: singleResult.wrapUpRequested,
4017
4172
  toolBudget: singleResult.toolBudget,
4018
4173
  toolBudgetBlocked: singleResult.toolBudgetBlocked,
4019
- });
4174
+ runner: singleResult.runner,
4175
+ externalProcess: singleResult.externalProcess,
4176
+ }));
4020
4177
  if (seqStep.outputName) {
4021
4178
  outputs[seqStep.outputName] = outputEntryFromAsyncResult({
4022
4179
  agent: singleResult.agent,
@@ -4049,47 +4206,47 @@ async function runSubagent(
4049
4206
 
4050
4207
  const stepEndTime = Date.now();
4051
4208
  const childInterrupted = singleResult.interrupted === true;
4052
- statusPayload.steps[flatIndex].status = stopped || childStopped ? "stopped" : timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
4053
- statusPayload.steps[flatIndex].endedAt = stepEndTime;
4054
- statusPayload.steps[flatIndex].durationMs = stepEndTime - stepStartTime;
4055
- statusPayload.steps[flatIndex].exitCode = stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
4056
- statusPayload.steps[flatIndex].timedOut = timedOut || singleResult.timedOut ? true : undefined;
4057
- statusPayload.steps[flatIndex].stopped = stopped || childStopped ? true : undefined;
4058
- statusPayload.steps[flatIndex].turnBudget = singleResult.turnBudget;
4059
- statusPayload.steps[flatIndex].turnBudgetExceeded = singleResult.turnBudgetExceeded;
4060
- statusPayload.steps[flatIndex].wrapUpRequested = singleResult.wrapUpRequested;
4061
- statusPayload.steps[flatIndex].toolBudget = singleResult.toolBudget;
4062
- statusPayload.steps[flatIndex].toolBudgetBlocked = singleResult.toolBudgetBlocked;
4209
+ requiredStatusStep(statusPayload, flatIndex).status = stopped || childStopped ? "stopped" : timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
4210
+ requiredStatusStep(statusPayload, flatIndex).endedAt = stepEndTime;
4211
+ requiredStatusStep(statusPayload, flatIndex).durationMs = stepEndTime - stepStartTime;
4212
+ requiredStatusStep(statusPayload, flatIndex).exitCode = stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
4213
+ setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "timedOut", timedOut || singleResult.timedOut ? true : undefined);
4214
+ setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "stopped", stopped || childStopped ? true : undefined);
4215
+ setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "turnBudget", singleResult.turnBudget);
4216
+ setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "turnBudgetExceeded", singleResult.turnBudgetExceeded);
4217
+ setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "wrapUpRequested", singleResult.wrapUpRequested);
4218
+ setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "toolBudget", singleResult.toolBudget);
4219
+ setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "toolBudgetBlocked", singleResult.toolBudgetBlocked);
4063
4220
  if (singleResult.toolBudget) statusPayload.toolBudget = singleResult.toolBudget;
4064
4221
  if (singleResult.toolBudgetBlocked) statusPayload.toolBudgetBlocked = true;
4065
4222
  if (singleResult.turnBudget) statusPayload.turnBudget = singleResult.turnBudget;
4066
4223
  if (singleResult.turnBudgetExceeded) statusPayload.turnBudgetExceeded = true;
4067
4224
  if (singleResult.wrapUpRequested) statusPayload.wrapUpRequested = true;
4068
- statusPayload.steps[flatIndex].model = singleResult.model;
4069
- statusPayload.steps[flatIndex].thinking = resolveEffectiveThinking(singleResult.model, statusPayload.steps[flatIndex].thinking);
4070
- statusPayload.steps[flatIndex].attemptedModels = singleResult.attemptedModels;
4071
- statusPayload.steps[flatIndex].modelAttempts = singleResult.modelAttempts;
4072
- statusPayload.steps[flatIndex].totalCost = singleResult.totalCost;
4073
- statusPayload.steps[flatIndex].error = stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
4074
- statusPayload.steps[flatIndex].transcriptPath = singleResult.transcriptPath ?? statusPayload.steps[flatIndex].transcriptPath;
4075
- statusPayload.steps[flatIndex].transcriptError = singleResult.transcriptError;
4076
- statusPayload.steps[flatIndex].agentContract = singleResult.agentContract;
4077
- statusPayload.steps[flatIndex].launchResolvedExtensions = singleResult.launchResolvedExtensions;
4078
- statusPayload.steps[flatIndex].runtimeAcknowledgedExtensions = singleResult.runtimeAcknowledgedExtensions;
4079
- statusPayload.steps[flatIndex].effects = singleResult.effects;
4080
- statusPayload.steps[flatIndex].execution = singleResult.execution;
4081
- statusPayload.steps[flatIndex].review = singleResult.review;
4082
- statusPayload.steps[flatIndex].structuredOutput = singleResult.structuredOutput;
4083
- statusPayload.steps[flatIndex].structuredOutputPath = singleResult.structuredOutputPath;
4084
- statusPayload.steps[flatIndex].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
4085
- statusPayload.steps[flatIndex].acceptance = singleResult.acceptance;
4086
- statusPayload.steps[flatIndex].watchdog = singleResult.watchdog;
4087
- statusPayload.steps[flatIndex].capabilityCeiling = singleResult.capabilityCeiling;
4088
- statusPayload.steps[flatIndex].capabilityAudit = singleResult.capabilityAudit;
4225
+ setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "model", singleResult.model);
4226
+ setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "thinking", resolveEffectiveThinking(singleResult.model, requiredStatusStep(statusPayload, flatIndex).thinking));
4227
+ setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "attemptedModels", singleResult.attemptedModels);
4228
+ setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "modelAttempts", singleResult.modelAttempts);
4229
+ setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "totalCost", singleResult.totalCost);
4230
+ setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "error", stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error);
4231
+ setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "transcriptPath", singleResult.transcriptPath ?? requiredStatusStep(statusPayload, flatIndex).transcriptPath);
4232
+ setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "transcriptError", singleResult.transcriptError);
4233
+ setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "agentContract", singleResult.agentContract);
4234
+ setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "launchResolvedExtensions", singleResult.launchResolvedExtensions);
4235
+ setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "runtimeAcknowledgedExtensions", singleResult.runtimeAcknowledgedExtensions);
4236
+ setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "effects", singleResult.effects);
4237
+ setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "execution", singleResult.execution);
4238
+ setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "review", singleResult.review);
4239
+ setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "structuredOutput", singleResult.structuredOutput);
4240
+ setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "structuredOutputPath", singleResult.structuredOutputPath);
4241
+ setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "structuredOutputSchemaPath", singleResult.structuredOutputSchemaPath);
4242
+ setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "acceptance", singleResult.acceptance);
4243
+ setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "watchdog", singleResult.watchdog);
4244
+ setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "capabilityCeiling", singleResult.capabilityCeiling);
4245
+ setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "capabilityAudit", singleResult.capabilityAudit);
4089
4246
  if (singleResult.capabilityCeiling) statusPayload.capabilityCeiling = singleResult.capabilityCeiling;
4090
4247
  if (singleResult.capabilityAudit) statusPayload.capabilityAudit = singleResult.capabilityAudit;
4091
4248
  if (stepTokens) {
4092
- statusPayload.steps[flatIndex].tokens = stepTokens;
4249
+ requiredStatusStep(statusPayload, flatIndex).tokens = stepTokens;
4093
4250
  statusPayload.totalTokens = { ...previousCumulativeTokens };
4094
4251
  }
4095
4252
  statusPayload.lastUpdate = stepEndTime;
@@ -4107,8 +4264,8 @@ async function runSubagent(
4107
4264
  tokens: stepTokens,
4108
4265
  }));
4109
4266
  if (singleResult.completionGuardTriggered) {
4110
- const event = buildControlEvent({
4111
- from: statusPayload.steps[flatIndex].activityState,
4267
+ const event = buildControlEvent(omitUndefinedProperties({
4268
+ from: requiredStatusStep(statusPayload, flatIndex).activityState,
4112
4269
  to: "needs_attention",
4113
4270
  runId: id,
4114
4271
  agent: seqStep.agent,
@@ -4116,7 +4273,7 @@ async function runSubagent(
4116
4273
  ts: stepEndTime,
4117
4274
  message: `${seqStep.agent} completed without making edits for an implementation task`,
4118
4275
  reason: "completion_guard",
4119
- });
4276
+ }));
4120
4277
  appendControlEvent(event);
4121
4278
  }
4122
4279
 
@@ -4197,13 +4354,13 @@ async function runSubagent(
4197
4354
  clearTimeout(timeoutTimer);
4198
4355
  timeoutTimer = undefined;
4199
4356
  }
4200
- const signalTerminated = !stopped && !timedOut && !turnBudgetExceeded && !interrupted && results.some((result) => result.exitCode !== 0 && isUnexplainedProcessSignal({
4357
+ const signalTerminated = !stopped && !timedOut && !turnBudgetExceeded && !interrupted && results.some((result) => result.exitCode !== 0 && isUnexplainedProcessSignal(omitUndefinedProperties({
4201
4358
  processSignal: result.processSignal,
4202
4359
  interrupted: result.interrupted,
4203
4360
  timedOut: result.timedOut,
4204
4361
  stopped: result.stopped,
4205
4362
  turnBudgetExceeded: result.turnBudgetExceeded,
4206
- }));
4363
+ })));
4207
4364
  statusPayload.state = checkpointRejected ? "rejected" : stopped || signalTerminated ? "stopped" : timedOut || turnBudgetExceeded || usageBudgetExceeded || statusPayload.error ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed";
4208
4365
  closeSteerInbox(asyncDir, statusPayload.state);
4209
4366
  disposeControlInbox();
@@ -4222,12 +4379,12 @@ async function runSubagent(
4222
4379
  if (changed) emitTerminalSteeringNotice(request.id, `Steering failed for run ${id}: child terminated before delivery.`);
4223
4380
  }
4224
4381
  const runEndedAt = Date.now();
4225
- statusPayload.activityState = undefined;
4382
+ delete statusPayload.activityState;
4226
4383
  if (stopped) {
4227
4384
  statusPayload.stopped = true;
4228
4385
  statusPayload.error = stopMessage;
4229
4386
  } else if (signalTerminated && !statusPayload.error) {
4230
- statusPayload.error = results.find((result) => result.processSignal)?.error;
4387
+ setOptionalProperty(statusPayload, "error", results.find((result) => result.processSignal)?.error);
4231
4388
  }
4232
4389
  if (timedOut) {
4233
4390
  statusPayload.timedOut = true;
@@ -4242,13 +4399,13 @@ async function runSubagent(
4242
4399
  }
4243
4400
  statusPayload.endedAt = runEndedAt;
4244
4401
  statusPayload.lastUpdate = runEndedAt;
4245
- statusPayload.sessionFile = effectiveSessionFile;
4402
+ setOptionalProperty(statusPayload, "sessionFile", effectiveSessionFile);
4246
4403
  if (singleRuntimeAcknowledgedExtensions) statusPayload.runtimeAcknowledgedExtensions = singleRuntimeAcknowledgedExtensions;
4247
- statusPayload.totalCost = finalTotalCost;
4248
- statusPayload.usageBudget = usageBudgetState(config.usageBudget, currentUsageTotals());
4249
- statusPayload.shareUrl = shareUrl;
4250
- statusPayload.gistUrl = gistUrl;
4251
- statusPayload.shareError = shareError;
4404
+ setOptionalProperty(statusPayload, "totalCost", finalTotalCost);
4405
+ setOptionalProperty(statusPayload, "usageBudget", usageBudgetState(config.usageBudget, currentUsageTotals()));
4406
+ setOptionalProperty(statusPayload, "shareUrl", shareUrl);
4407
+ setOptionalProperty(statusPayload, "gistUrl", gistUrl);
4408
+ setOptionalProperty(statusPayload, "shareError", shareError);
4252
4409
  if (statusPayload.state === "failed" && !statusPayload.error) {
4253
4410
  const failedStep = statusPayload.steps.find((s) => s.status === "failed");
4254
4411
  if (failedStep?.agent) {
@@ -4270,13 +4427,13 @@ async function runSubagent(
4270
4427
  usageBudget: statusPayload.usageBudget,
4271
4428
  }),
4272
4429
  );
4273
- writeRunLog(logPath, {
4430
+ writeRunLog(logPath, omitUndefinedProperties({
4274
4431
  id,
4275
4432
  mode: statusPayload.mode,
4276
4433
  cwd,
4277
4434
  startedAt: overallStartTime,
4278
4435
  endedAt: runEndedAt,
4279
- steps: statusPayload.steps.map((step) => ({
4436
+ steps: statusPayload.steps.map((step) => omitUndefinedProperties({
4280
4437
  agent: step.agent,
4281
4438
  status: step.status,
4282
4439
  durationMs: step.durationMs,
@@ -4287,7 +4444,7 @@ async function runSubagent(
4287
4444
  sessionFile: effectiveSessionFile,
4288
4445
  shareUrl,
4289
4446
  shareError,
4290
- });
4447
+ }));
4291
4448
 
4292
4449
  try {
4293
4450
  writeAtomicJson(resultPath, {
@@ -4308,7 +4465,7 @@ async function runSubagent(
4308
4465
  ...(statusPayload.usageBudget ? { usageBudget: statusPayload.usageBudget } : {}),
4309
4466
  ...(statusPayload.checkpoint ? { checkpoint: statusPayload.checkpoint } : {}),
4310
4467
  ...(stopped ? { stopped: true, error: stopMessage } : timedOut ? { timedOut: true, error: timeoutMessage ?? "Subagent timed out." } : turnBudgetExceeded ? { error: statusPayload.error ?? "Subagent exceeded turn budget." } : usageBudgetExceeded ? { error: statusPayload.error ?? "Usage budget exhausted." } : {}),
4311
- results: results.map((r) => ({
4468
+ results: results.map((r) => omitUndefinedProperties({
4312
4469
  agent: r.agent,
4313
4470
  context: r.context,
4314
4471
  output: r.output,
@@ -4333,6 +4490,8 @@ async function runSubagent(
4333
4490
  modelAttempts: r.modelAttempts,
4334
4491
  totalCost: r.totalCost,
4335
4492
  artifactPaths: r.artifactPaths,
4493
+ outputSaveError: r.outputSaveError,
4494
+ metadataSaveError: r.metadataSaveError,
4336
4495
  truncated: r.truncated,
4337
4496
  transcriptPath: r.transcriptPath,
4338
4497
  transcriptError: r.transcriptError,
@@ -4340,6 +4499,8 @@ async function runSubagent(
4340
4499
  launchContractDigest: r.launchContractDigest,
4341
4500
  launchResolvedExtensions: r.launchResolvedExtensions,
4342
4501
  runtimeAcknowledgedExtensions: r.runtimeAcknowledgedExtensions,
4502
+ runner: r.runner,
4503
+ externalProcess: r.externalProcess,
4343
4504
  execution: r.execution,
4344
4505
  review: r.review,
4345
4506
  effects: r.effects,
@@ -4356,6 +4517,8 @@ async function runSubagent(
4356
4517
  parallelHandoff: statusPayload.parallelHandoff,
4357
4518
  capabilityCeiling: statusPayload.capabilityCeiling,
4358
4519
  capabilityAudit: statusPayload.capabilityAudit,
4520
+ ...(config.parentWorkflowRunId ? { parentWorkflowRunId: config.parentWorkflowRunId } : {}),
4521
+ ...(config.workflowKey ? { workflowKey: config.workflowKey } : {}),
4359
4522
  exitCode: checkpointRejected || stopped || timedOut || turnBudgetExceeded || usageBudgetExceeded ? 1 : interrupted || results.every((r) => r.success) ? 0 : 1,
4360
4523
  timestamp: runEndedAt,
4361
4524
  durationMs: runEndedAt - overallStartTime,