pi-subagents 0.64.0 → 0.65.1

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 (106) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +2 -2
  3. package/agents/reviewer.md +1 -1
  4. package/agents/scout.md +1 -1
  5. package/docs/agents.md +19 -17
  6. package/docs/configuration.md +23 -15
  7. package/docs/extension-api.md +18 -6
  8. package/docs/missions.md +2 -0
  9. package/docs/observability.md +10 -12
  10. package/docs/tool-reference.md +15 -8
  11. package/docs/watchdog.md +1 -1
  12. package/docs/workflows.md +13 -3
  13. package/package.json +5 -4
  14. package/runner-server-preload.mjs +13 -0
  15. package/skills/pi-subagents/SKILL.md +2 -1
  16. package/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
  17. package/skills/pi-subagents/references/execution-controls.md +7 -3
  18. package/skills/pi-subagents/references/multi-lane-orchestration.md +2 -0
  19. package/src/agents/agent-management.ts +47 -15
  20. package/src/api/capability-ceiling.ts +0 -1
  21. package/src/api/{pi-args.ts → child-tool-plan.ts} +1 -1
  22. package/src/api/preflight.ts +7 -4
  23. package/src/extension/config.ts +4 -2
  24. package/src/extension/doctor.ts +2 -10
  25. package/src/extension/fanout-child.ts +9 -11
  26. package/src/extension/index.ts +58 -7
  27. package/src/extension/public-execution.ts +14 -0
  28. package/src/extension/rpc.ts +3 -2
  29. package/src/extension/schemas.ts +3 -2
  30. package/src/extension/tool-description.ts +13 -8
  31. package/src/integrations/pi-web-session-liveness.ts +73 -0
  32. package/src/intercom/native-supervisor-channel.ts +159 -95
  33. package/src/intercom/supervisor-ui.ts +244 -0
  34. package/src/missions/workflow-state.ts +37 -16
  35. package/src/runs/background/async-execution.ts +51 -25
  36. package/src/runs/background/async-job-tracker.ts +11 -0
  37. package/src/runs/background/async-resume.ts +14 -3
  38. package/src/runs/background/async-retention.ts +9 -0
  39. package/src/runs/background/control-channel.ts +2 -204
  40. package/src/runs/background/notify.ts +2 -0
  41. package/src/runs/background/process-terminal.ts +1 -1
  42. package/src/runs/background/retained-nested-route-tracker.ts +96 -0
  43. package/src/runs/background/run-child-session.ts +614 -0
  44. package/src/runs/background/run-status.ts +0 -1
  45. package/src/runs/background/runner-aliases.ts +152 -0
  46. package/src/runs/background/runner-child-sessions.ts +31 -0
  47. package/src/runs/background/scheduled-runs.ts +18 -4
  48. package/src/runs/background/subagent-runner.ts +203 -901
  49. package/src/runs/foreground/async-steering-action.ts +1 -17
  50. package/src/runs/foreground/execution.ts +204 -378
  51. package/src/runs/foreground/foreground-control.ts +4 -0
  52. package/src/runs/foreground/foreground-history.ts +3 -1
  53. package/src/runs/foreground/prompt-audit.ts +9 -5
  54. package/src/runs/foreground/subagent-executor.ts +161 -91
  55. package/src/runs/foreground/workflow-foreground-steering.ts +24 -98
  56. package/src/runs/shared/abort-recovery.ts +3 -3
  57. package/src/runs/shared/acceptance.ts +14 -1
  58. package/src/runs/shared/capability-ceiling.ts +1 -2
  59. package/src/runs/shared/child-hooks.ts +25 -0
  60. package/src/runs/shared/child-identity.ts +13 -2
  61. package/src/runs/shared/child-launch.ts +314 -0
  62. package/src/runs/shared/child-lifecycle.ts +25 -0
  63. package/src/runs/shared/child-runtime-config.ts +126 -0
  64. package/src/runs/shared/child-session.ts +337 -0
  65. package/src/runs/shared/child-tool-plan.ts +530 -0
  66. package/src/runs/shared/claude-code-adapter.ts +5 -1
  67. package/src/runs/shared/completion-guard.ts +1 -1
  68. package/src/runs/shared/external-cli-preflight.ts +16 -0
  69. package/src/runs/shared/llm-intent-arbiter.ts +20 -11
  70. package/src/runs/shared/mcp-direct-tool-allowlist.ts +5 -4
  71. package/src/runs/shared/model-exclusions.ts +84 -15
  72. package/src/runs/shared/model-fallback.ts +78 -6
  73. package/src/runs/shared/nested-events.ts +32 -48
  74. package/src/runs/shared/nested-path.ts +0 -14
  75. package/src/runs/shared/orca-progress-tabs.ts +12 -7
  76. package/src/runs/shared/parallel-utils.ts +1 -2
  77. package/src/runs/shared/permissions.ts +0 -13
  78. package/src/runs/shared/process-signal.ts +4 -1
  79. package/src/runs/shared/run-fanout-budget.ts +0 -13
  80. package/src/runs/shared/runtime-acknowledged-extensions.ts +0 -27
  81. package/src/runs/shared/structured-output.ts +17 -4
  82. package/src/runs/shared/subagent-control.ts +2 -0
  83. package/src/runs/shared/subagent-prompt-runtime.ts +87 -384
  84. package/src/runs/shared/tool-availability.ts +18 -62
  85. package/src/runs/shared/tool-budget.ts +0 -14
  86. package/src/runs/shared/worktree-cleanup-plan.ts +31 -9
  87. package/src/runs/shared/worktree.ts +192 -40
  88. package/src/shared/child-session-name.ts +1 -1
  89. package/src/shared/jsonl-writer.ts +11 -0
  90. package/src/shared/model-response-aliases.ts +13 -0
  91. package/src/shared/thinking-ceiling.ts +0 -6
  92. package/src/shared/types.ts +63 -28
  93. package/src/shared/utils.ts +6 -4
  94. package/src/shared/watch-strategy.ts +2 -0
  95. package/src/slash/slash-commands.ts +0 -6
  96. package/src/tui/fleet-status.ts +1 -1
  97. package/src/tui/fleet.ts +0 -1
  98. package/src/tui/render.ts +254 -41
  99. package/src/watchdog/child-status.ts +0 -1
  100. package/src/watchdog/register-child.ts +12 -12
  101. package/src/workflows/scripted-workflow.ts +80 -7
  102. package/src/workflows/workflow-checklist.ts +4 -3
  103. package/src/runs/shared/child-protocol.ts +0 -415
  104. package/src/runs/shared/pi-args.ts +0 -1059
  105. package/src/runs/shared/subagent-startup-retry.ts +0 -116
  106. package/src/shared/post-exit-stdio-guard.ts +0 -85
@@ -1,9 +1,26 @@
1
- import { spawn, spawnSync } from "node:child_process";
2
- import { randomUUID } from "node:crypto";
1
+ import { spawnSync } from "node:child_process";
2
+ import { EventEmitter } from "node:events";
3
3
  import * as fs from "node:fs";
4
+ import { createRequire } from "node:module";
4
5
  import * as path from "node:path";
5
6
  import { pathToFileURL } from "node:url";
6
7
  import type { Message } from "@earendil-works/pi-ai";
8
+
9
+ // Detached runners skip Pi's CLI proxy setup. Keep fetch on the same Undici dispatcher.
10
+ function ensureProxyAwareHttpDispatcher(): void {
11
+ try {
12
+ // SAFETY: require loads the pinned direct dependency described by these types.
13
+ const undici = createRequire(import.meta.url)("undici") as typeof import("undici");
14
+ const dispatcher = new undici.EnvHttpProxyAgent({ allowH2: false });
15
+ // Fetch rejects stream errors; the listener prevents an unhandled EventEmitter error.
16
+ EventEmitter.prototype.on.call(dispatcher, "error", () => {});
17
+ undici.setGlobalDispatcher(dispatcher);
18
+ undici.install();
19
+ } catch (error) {
20
+ console.error(`[pi-subagents] proxy-aware HTTP dispatcher not installed: ${error instanceof Error ? error.message : String(error)}`);
21
+ }
22
+ }
23
+ ensureProxyAwareHttpDispatcher();
7
24
  import { writeAtomicJson } from "../../shared/atomic-json.ts";
8
25
  import { writeAsyncResultFile, writePendingAsyncResultFile } from "./result-files.ts";
9
26
  import { createFileCoalescer } from "../../shared/file-coalescer.ts";
@@ -11,9 +28,9 @@ import { createCapacityResilientJsonWriter } from "../../shared/capacity-resilie
11
28
  import { isStorageCapacityError } from "../../shared/file-system-retry.ts";
12
29
  import { updateActiveRunIndex } from "./active-run-index.ts";
13
30
  import { createChildTranscriptWriter, type ChildTranscriptWriter } from "../../shared/child-transcript.ts";
14
- import { closeSteerInbox, consumeInterruptRequest, consumeSteerRequests, deliverInterruptRequest, deliverStopRequest, deliverTimeoutRequest, enqueueStepSteer, steerAcksDir, steerCapabilityPath, stepSteerInboxDir, watchAsyncControlInbox, type SteerAck, type SteerCapability, type SteerRequest, type StopRequest } from "./control-channel.ts";
31
+ import { closeSteerInbox, consumeInterruptRequest, consumeSteerRequests, deliverInterruptRequest, deliverStopRequest, deliverTimeoutRequest, watchAsyncControlInbox, type SteerRequest, type StopRequest } from "./control-channel.ts";
15
32
  import { appendJsonl as appendRawJsonl, formatOutputArtifactContent, getArtifactPaths, writeArtifact, writeMetadata } from "../../shared/artifacts.ts";
16
- import { PI_CODING_AGENT_PACKAGE, getPiSpawnCommand, resolveInstalledPiPackageRoot } from "../shared/pi-spawn.ts";
33
+ import { PI_CODING_AGENT_PACKAGE, resolveInstalledPiPackageRoot } from "../shared/pi-spawn.ts";
17
34
  import { preflightLaunchCwd } from "../shared/launch-cwd.ts";
18
35
  import { captureSingleOutputSnapshot, extractChildWrittenOutput, finalizeSingleOutput, formatSavedOutputReference, injectOutputPathSystemPrompt, injectSingleOutputInstruction, resolveSingleOutput, type SingleOutputSnapshot } from "../shared/single-output.ts";
19
36
  import {
@@ -32,7 +49,6 @@ import {
32
49
  type RuntimeAcknowledgedChildExtensionsV1,
33
50
  type ModelAttempt,
34
51
  type PiWriterProcessInstanceExitV1,
35
- type ProcessTreeTerminalV1,
36
52
  type NestedRouteInfo,
37
53
  type NestedRunSummary,
38
54
  type ResolvedControlConfig,
@@ -52,7 +68,6 @@ import {
52
68
  type MaxOutputConfig,
53
69
  SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
54
70
  truncateOutput,
55
- getSubagentDepthEnv,
56
71
  } from "../../shared/types.ts";
57
72
  import {
58
73
  DEFAULT_CONTROL_CONFIG,
@@ -75,31 +90,25 @@ import {
75
90
  DEFAULT_GLOBAL_CONCURRENCY_LIMIT,
76
91
  Semaphore,
77
92
  } from "../shared/parallel-utils.ts";
78
- import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, deriveForkPromptCacheKey, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan, type SubagentTaskDelivery } from "../shared/pi-args.ts";
93
+ import { applyThinkingSuffix, deriveForkPromptCacheKey, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/child-tool-plan.ts";
94
+ import { buildInProcessChildLaunch, type InheritedChildRuntime } from "../shared/child-launch.ts";
95
+ import type { ChildSessionFactory } from "../shared/child-session.ts";
96
+ import { runChildSession, type ChildEvent, type RunChildSessionResult, type StepSteerHandler } from "./run-child-session.ts";
97
+ import { loadRunnerChildSessionFactory } from "./runner-child-sessions.ts";
98
+ import { SUBAGENT_CHILD_ENV } from "../shared/child-runtime-config.ts";
79
99
  import { deriveChildSessionName } from "../../shared/child-session-name.ts";
80
100
  import { alignForkedSessionCwd } from "../../shared/fork-session-cwd.ts";
81
- import { readRuntimeAcknowledgedExtensions } from "../shared/runtime-acknowledged-extensions.ts";
82
101
  import { outputEntryFromAsyncResult, resolveOutputReferences } from "../shared/chain-outputs.ts";
83
- import { clearStructuredOutputCaptures, createStructuredOutputRuntime, MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput, readStructuredOutputAcceptanceReport } from "../shared/structured-output.ts";
84
- import { formatMidToolExitError, formatProcessSignalError, isOrdinaryToolForMidToolExit, isUnexplainedProcessSignal } from "../shared/process-signal.ts";
85
- import { readChildToolDiagnosticError } from "../shared/tool-availability.ts";
102
+ import { clearStructuredOutputCaptures, createStructuredOutputFileCapture, createStructuredOutputRuntime, MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput, readStructuredOutputAcceptanceReport } from "../shared/structured-output.ts";
103
+ import { formatMidToolExitError, isOrdinaryToolForMidToolExit, isUnexplainedProcessSignal } from "../shared/process-signal.ts";
104
+ import { formatChildToolDiagnostic } from "../shared/tool-availability.ts";
86
105
  import { buildTimeoutRecoverySummary, collectTrackedMutationEvidence, snapshotTrackedMutations } from "../shared/mutation-evidence.ts";
87
106
  import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection } from "../shared/dynamic-fanout.ts";
88
107
  import { claimRunFanoutBatch, getRunFanoutBudgetSnapshot } from "../shared/run-fanout-budget.ts";
89
108
  import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDir, writeNestedEvent } from "../shared/nested-events.ts";
90
109
  import { formatModelAttemptNote, formatSubagentModelVerificationError, isContextOverflow, isRetryableModelFailureAttempt, recordRetryableModelFailure } from "../shared/model-fallback.ts";
91
- import {
92
- SUBAGENT_STARTUP_RETRY_DELAYS_MS,
93
- formatSubagentExtensionConflictError,
94
- formatSubagentStartupRetryExhaustedError,
95
- formatSubagentStartupRetryNote,
96
- isRetryableSubagentStartupFailure,
97
- waitForSubagentStartupRetry,
98
- } from "../shared/subagent-startup-retry.ts";
99
110
  import { markProcessTerminalCandidateLeaseRelease, writeProcessTerminalCandidate, type ProcessTerminalCandidate } from "./process-terminal.ts";
100
- import { createOwnedProcessTreeController, type OwnedProcessTreeController } from "./owned-process-tree.ts";
101
111
  import { createSteeringStatus, recordSteeringRequest, steeringStatus, terminalSteeringNoticeState, updateSteeringTarget } from "./steering.ts";
102
- import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
103
112
  import { PROMPT_REDACTED, detectSubagentError, extractTextFromContent, extractToolArgsPreview, formatEmptyTerminalAssistantResponseError, getFinalOutput, hasEmptyTerminalAssistantResponse, readStatus } from "../../shared/utils.ts";
104
113
  import { evaluateCompletionMutationGuard, expectsImplementationMutation, hasMutationToolCapability, validateImplementationToolContract } from "../shared/completion-guard.ts";
105
114
  import { planCompletionEvidence, projectSettlementDiagnostic } from "../shared/completion-evidence.ts";
@@ -128,7 +137,7 @@ import {
128
137
  type WorktreeSetup,
129
138
  } from "../shared/worktree.ts";
130
139
  import { findModelInfo, resolveEffectiveThinking } from "../../shared/model-info.ts";
131
- import { assertThinkingWithinCeiling, decodeThinkingCeiling, SUBAGENT_THINKING_CEILING_ENV } from "../../shared/thinking-ceiling.ts";
140
+ import { assertThinkingWithinCeiling } from "../../shared/thinking-ceiling.ts";
132
141
  import { launchBindingDigest } from "../../shared/launch-contract.ts";
133
142
  import { writeInitialProgressFile } from "../../shared/settings.ts";
134
143
  import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
@@ -143,7 +152,6 @@ import { effectiveToolTimeoutMs, formatToolTimeoutMessage, toolTimeoutCallKey }
143
152
  import { usageBudgetExceededMessage, usageBudgetState } from "../shared/usage-budget.ts";
144
153
  import { formatParallelHandoffError, formatParallelHandoffReference, parallelHandoffPath, writeParallelHandoffGroup, writePendingParallelHandoff } from "../shared/parallel-handoff.ts";
145
154
  import { resolveWatchdogConfig } from "../../watchdog/settings.ts";
146
- import { createBoundedByteTail, createBoundedLineReader, formatProtocolOutputLimit, MAX_CHILD_STDERR_BYTES, PI_AGGREGATE_EVENT_PROJECTOR, projectChildLifecycle, type ChildLifecycleAction, type ChildLifecycleState, type ProtocolOutputLimit } from "../shared/child-protocol.ts";
147
155
  import { acquireSessionLease, type SessionLeaseRequest } from "../shared/session-lease.ts";
148
156
  import { buildExternalCliPrompt, runExternalCli } from "../shared/external-cli-runner.ts";
149
157
  import { resolveClaudeCodeLaunch } from "../shared/claude-code-adapter.ts";
@@ -152,20 +160,22 @@ import { resolveCursorAgentLaunch } from "../shared/cursor-agent-adapter.ts";
152
160
  import { resolveExternalCliRunnerStatus } from "../shared/external-cli-contract.ts";
153
161
  import { runExternalJob } from "../shared/external-job-runner.ts";
154
162
  import { createOrcaProgressTab, type OrcaProgressTab } from "../shared/orca-progress-tabs.ts";
155
- import { decodeSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
163
+ import type { ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
156
164
  import {
157
- CHILD_WATCHDOG_CONFIG_ENV,
158
165
  acceptChildWatchdogEvent,
159
166
  applyChildWatchdogMessage,
160
- childWatchdogIsActive,
161
- decodeChildWatchdogConfig,
162
167
  isChildWatchdogStatusEvent,
163
168
  resolveChildWatchdogConfig,
164
- type ChildWatchdogStateSnapshot,
169
+ type ChildWatchdogStatusEvent,
165
170
  } from "../../watchdog/child-status.ts";
166
171
 
167
172
  const INTERCOM_DETACH_RECEIPT = "Detached for intercom coordination before task completion.";
168
173
 
174
+ // This process hosts child sessions. An ambient copy of pi-subagents loaded
175
+ // into one of them must register nothing; the variable marks the process as a
176
+ // child host.
177
+ process.env[SUBAGENT_CHILD_ENV] = "1";
178
+
169
179
  interface SubagentRunConfig {
170
180
  id: string;
171
181
  steps: RunnerStep[];
@@ -183,10 +193,14 @@ interface SubagentRunConfig {
183
193
  sessionId?: string | null;
184
194
  completionOwnerId?: string;
185
195
  piPackageRoot?: string;
186
- piArgv1?: string;
196
+ /** Test seam: module the runner imports its `ChildSessionFactory` from. */
197
+ childSessionFactoryModule?: string;
198
+ /** The launching executor's own child runtime when it was itself an in-process child. */
199
+ inheritedChildRuntime?: InheritedChildRuntime;
187
200
  worktreeSetupHook?: string;
188
201
  worktreeSetupHookTimeoutMs?: number;
189
202
  worktreeBaseDir?: string;
203
+ baseRef?: string;
190
204
  worktreeProvider?: import("../../shared/types.ts").WorktreeProvider;
191
205
  worktreeBranchPrefix?: string;
192
206
  controlConfig?: ResolvedControlConfig;
@@ -232,7 +246,6 @@ interface StepResult {
232
246
  output: string;
233
247
  outputState?: SubagentOutputState;
234
248
  error?: string;
235
- protocolError?: ProtocolOutputLimit;
236
249
  success?: boolean;
237
250
  exitCode: number | null;
238
251
  usage?: Usage;
@@ -271,8 +284,6 @@ interface StepResult {
271
284
  structuredOutputSchemaPath?: string;
272
285
  acceptance?: import("../../shared/types.ts").AcceptanceLedger;
273
286
  watchdog?: import("../../shared/types.ts").ChildWatchdogProgress;
274
- writerProcesses?: PiWriterProcessInstanceExitV1[];
275
- writerAttemptCount?: number;
276
287
  runner?: ExternalCliRunnerStatus | ExternalJobRunnerStatus;
277
288
  externalProcess?: ExternalProcessStatus;
278
289
  externalJob?: ExternalJobStatus;
@@ -373,10 +384,6 @@ function appendDiagnosticJsonl(filePath: string, line: string, droppedEventType?
373
384
  state.diagnosticsTruncated = true;
374
385
  }
375
386
 
376
- function shouldPersistChildEvent(event: Record<string, unknown>): boolean {
377
- return event.type !== "message_update";
378
- }
379
-
380
387
  function isBlockingSupervisorTool(toolName: string | undefined, args: unknown): boolean {
381
388
  if (!args || typeof args !== "object" || Array.isArray(args)) return false;
382
389
  if (toolName === "contact_supervisor") {
@@ -459,15 +466,6 @@ function appendRecentStepOutput(step: RunnerStatusStep, lines: string[]): void {
459
466
  }
460
467
  }
461
468
 
462
- function assistantStartsToolCall(message: Message): boolean {
463
- return Array.isArray(message.content)
464
- && message.content.some((part) => (part as { type?: string }).type === "toolCall");
465
- }
466
-
467
- function isTerminalAssistantStop(message: Message): boolean {
468
- return (message as { stopReason?: string }).stopReason === "stop" && !assistantStartsToolCall(message);
469
- }
470
-
471
469
  type UndefinedOmitted<T extends object> = {
472
470
  [K in keyof T]: Exclude<T[K], undefined>;
473
471
  };
@@ -510,73 +508,6 @@ function resetStepLiveDetail(step: RunnerStatusStep): void {
510
508
  step.recentOutput = [];
511
509
  }
512
510
 
513
- interface ChildEventContext {
514
- eventsPath: string;
515
- runId: string;
516
- stepIndex: number;
517
- agent: string;
518
- }
519
-
520
- interface ChildUsage {
521
- input?: number;
522
- inputTokens?: number;
523
- output?: number;
524
- outputTokens?: number;
525
- cacheRead?: number;
526
- cacheReadTokens?: number;
527
- cacheWrite?: number;
528
- cost?: { total?: number };
529
- }
530
-
531
- type ChildMessage = Message & {
532
- model?: string;
533
- errorMessage?: string;
534
- usage?: ChildUsage;
535
- };
536
-
537
- interface ChildEvent {
538
- type?: string;
539
- message?: ChildMessage;
540
- toolName?: string;
541
- args?: Record<string, unknown>;
542
- willRetry?: unknown;
543
- }
544
-
545
- interface RunPiStreamingResult {
546
- stderr: string;
547
- exitCode: number | null;
548
- messages: Message[];
549
- usage: Usage;
550
- toolCount: number;
551
- durationMs: number;
552
- model?: string;
553
- error?: string;
554
- protocolError?: ProtocolOutputLimit;
555
- finalOutput: string;
556
- outputState: SubagentOutputState;
557
- interrupted?: boolean;
558
- timedOut?: boolean;
559
- stopped?: boolean;
560
- toolBudget?: ToolBudgetState;
561
- toolBudgetBlocked?: boolean;
562
- observedMutationAttempt?: boolean;
563
- structuredOutputToolInvoked?: boolean;
564
- structuredOutputMessageStartIndex?: number;
565
- structuredOutput?: unknown;
566
- watchdog?: ChildWatchdogStateSnapshot;
567
- runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
568
- processInstanceId: string;
569
- processCloseObservedAt?: number;
570
- processSignal?: string | null;
571
- processTree: ProcessTreeTerminalV1;
572
- currentTool?: string;
573
- currentToolArgs?: string;
574
- currentPath?: string;
575
- afterCompactionSettlement?: boolean;
576
- abortRecoveryDiagnostic?: string;
577
- effects?: import("../../shared/types.ts").EffectsProjection;
578
- }
579
-
580
511
  const MAX_CHILD_FAILURE_DIAGNOSTIC_CHARS = 8_192;
581
512
 
582
513
  function formatRequiredOutputError(requiredOutput: {
@@ -611,568 +542,6 @@ function formatChildFailureDiagnostic(input: {
611
542
  return `${baseError.slice(0, Math.max(0, errorLimit))}${context ? `\n${context}` : ""}`;
612
543
  }
613
544
 
614
- function runPiStreaming(
615
- args: string[],
616
- cwd: string,
617
- outputFile: string,
618
- env?: Record<string, string | undefined>,
619
- piPackageRoot?: string,
620
- piArgv1?: string,
621
- maxSubagentDepth?: number,
622
- childEventContext?: ChildEventContext,
623
- registerInterrupt?: (interrupt: (() => void) | undefined) => void,
624
- onChildEvent?: (event: ChildEvent) => void,
625
- transcriptWriter?: ChildTranscriptWriter,
626
- registerTimeout?: (interrupt: (() => void) | undefined) => void,
627
- timeoutMessage?: string,
628
- registerStop?: (stop: (() => void) | undefined) => void,
629
- stopMessage?: string,
630
- onWriterProcess?: (writer: { state: "none" | "spawning" } | { state: "running"; pid: number }) => void,
631
- toolTimeoutMs?: number,
632
- runDeadlineAt?: number,
633
- orcaProgressTab?: OrcaProgressTab,
634
- expectedModelForVerification?: string,
635
- modelVerificationRegistry?: Array<{ provider: string; id: string; fullId: string }>,
636
- mutationTools?: readonly string[],
637
- ): Promise<RunPiStreamingResult> {
638
- return new Promise((resolve) => {
639
- const startedAt = Date.now();
640
- const processInstanceId = randomUUID();
641
- onWriterProcess?.({ state: "spawning" });
642
- const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
643
- const spawnEnv = { ...process.env, ...(env ?? {}), ...getSubagentDepthEnv(maxSubagentDepth) };
644
- const spawnSpec = getPiSpawnCommand(args, {
645
- ...(piPackageRoot ? { piPackageRoot } : {}),
646
- ...(piArgv1 ? { argv1: piArgv1 } : {}),
647
- });
648
- const child = spawn(spawnSpec.command, spawnSpec.args, {
649
- cwd,
650
- stdio: ["ignore", "pipe", "pipe"],
651
- env: spawnEnv,
652
- windowsHide: true,
653
- detached: process.platform !== "win32",
654
- });
655
- let processTreeController: OwnedProcessTreeController | undefined;
656
- const stderrTail = createBoundedByteTail();
657
- const rawStdoutTail = createBoundedByteTail();
658
- const messages: Message[] = [];
659
- const usage = emptyUsage();
660
- let model: string | undefined;
661
- let writerRegistrationError: string | undefined;
662
- if (typeof child.pid === "number") {
663
- processTreeController = createOwnedProcessTreeController(child.pid);
664
- try {
665
- onWriterProcess?.({ state: "running", pid: child.pid });
666
- } catch (writerError) {
667
- writerRegistrationError = `Failed to record revived Pi writer ownership: ${writerError instanceof Error ? writerError.message : String(writerError)}`;
668
- trySignalChild(child, "SIGKILL");
669
- }
670
- }
671
- let error: string | undefined = writerRegistrationError;
672
- let assistantError: string | undefined;
673
- let interrupted = false;
674
- let timedOut = false;
675
- let stopped = false;
676
- let observedMutationAttempt = false;
677
- let structuredOutputToolInvoked = false;
678
- let structuredOutputMessageStartIndex: number | undefined;
679
- let currentTool: string | undefined;
680
- let currentToolArgs: string | undefined;
681
- let currentPath: string | undefined;
682
- let toolCount = 0;
683
- type ActiveToolCall = { key: string; tool: string; args?: string; path?: string };
684
- let activeToolSequence = 0;
685
- const activeToolCalls = new Map<string, ActiveToolCall>();
686
- const activeToolKeysByName = new Map<string, string[]>();
687
- const refreshCurrentTool = (): void => {
688
- const active = [...activeToolCalls.values()].at(-1);
689
- currentTool = active?.tool;
690
- currentToolArgs = active?.args;
691
- currentPath = active?.path;
692
- };
693
- const recordActiveToolCall = (event: { toolCallId?: unknown; toolName: string; args?: Record<string, unknown> }): void => {
694
- const key = toolTimeoutCallKey(event, ++activeToolSequence);
695
- const active = omitUndefinedProperties({
696
- key,
697
- tool: event.toolName,
698
- args: extractToolArgsPreview(event.args ?? {}),
699
- path: resolveCurrentPath(event.toolName, event.args),
700
- });
701
- activeToolCalls.set(key, active);
702
- const keys = activeToolKeysByName.get(active.tool) ?? [];
703
- keys.push(key);
704
- activeToolKeysByName.set(active.tool, keys);
705
- refreshCurrentTool();
706
- };
707
- const removeActiveToolCall = (event: { toolCallId?: unknown; toolName?: unknown }): void => {
708
- const key = typeof event.toolCallId === "string" && event.toolCallId.length > 0
709
- ? `id:${event.toolCallId}`
710
- : typeof event.toolName === "string"
711
- ? activeToolKeysByName.get(event.toolName)?.[0]
712
- : activeToolCalls.size === 1
713
- ? [...activeToolCalls.keys()][0]
714
- : undefined;
715
- if (!key) return;
716
- const active = activeToolCalls.get(key);
717
- if (!active) return;
718
- activeToolCalls.delete(key);
719
- const keys = activeToolKeysByName.get(active.tool)?.filter((candidate) => candidate !== key) ?? [];
720
- if (keys.length > 0) activeToolKeysByName.set(active.tool, keys);
721
- else activeToolKeysByName.delete(active.tool);
722
- refreshCurrentTool();
723
- };
724
- const childWatchdogConfig = decodeChildWatchdogConfig(env?.[CHILD_WATCHDOG_CONFIG_ENV]);
725
- let childWatchdogState: ChildWatchdogStateSnapshot | undefined;
726
- const childLifecycleState: ChildLifecycleState = { compactionRetryActive: false };
727
- let applyChildLifecycle = (_action: ChildLifecycleAction): void => {};
728
- const updateChildWatchdogState = (snapshot: ChildWatchdogStateSnapshot): void => {
729
- childWatchdogState = snapshot;
730
- };
731
-
732
- const writeOutputLine = (line: string) => {
733
- if (!line.trim()) return;
734
- outputStream.write(`${line}\n`);
735
- orcaProgressTab?.append(`${line}\n`);
736
- };
737
-
738
- const writeOutputText = (text: string) => {
739
- for (const line of text.split("\n")) {
740
- writeOutputLine(line);
741
- }
742
- };
743
-
744
- const appendChildEvent = (event: Record<string, unknown>) => {
745
- if (!childEventContext) return;
746
- if (!shouldPersistChildEvent(event)) return;
747
- appendDiagnosticJsonl(childEventContext.eventsPath, JSON.stringify({
748
- ...event,
749
- subagentSource: "child",
750
- subagentRunId: childEventContext.runId,
751
- subagentStepIndex: childEventContext.stepIndex,
752
- subagentAgent: childEventContext.agent,
753
- observedAt: Date.now(),
754
- }), typeof event.type === "string" ? event.type : undefined);
755
- };
756
-
757
- const appendChildLine = (type: "subagent.child.stdout" | "subagent.child.stderr", line: string) => {
758
- appendChildEvent({ type, line });
759
- if (type === "subagent.child.stdout") transcriptWriter?.writeStdoutLine(line);
760
- else transcriptWriter?.writeStderrLine(line);
761
- };
762
-
763
- const processStdoutLine = (line: string) => {
764
- if (!line.trim()) return;
765
- let event: ChildEvent;
766
- try {
767
- event = JSON.parse(line) as ChildEvent;
768
- } catch {
769
- rawStdoutTail.push(`${line}\n`);
770
- writeOutputLine(line);
771
- appendChildLine("subagent.child.stdout", line);
772
- return;
773
- }
774
-
775
- appendChildEvent(event as unknown as Record<string, unknown>);
776
- transcriptWriter?.writeChildEvent(event);
777
- if (event.type === "compaction_start") compactionStartedReceived = true;
778
- if (event.type === "compaction_end" && event.willRetry === true) {
779
- compactionStartedReceived = false;
780
- afterCompactionSettlement = false;
781
- }
782
- if (event.type === "agent_start" || event.type === "auto_retry_start") {
783
- compactionStartedReceived = false;
784
- afterCompactionSettlement = false;
785
- }
786
- const lifecycleAction = projectChildLifecycle(event, false, childLifecycleState);
787
- if (event.type === "agent_settled" && lifecycleAction === "start-drain") {
788
- agentSettledReceived = true;
789
- afterCompactionSettlement = compactionStartedReceived;
790
- }
791
- applyChildLifecycle(lifecycleAction);
792
-
793
- if (isChildWatchdogStatusEvent(event)) {
794
- if (!childWatchdogConfig) return;
795
- const next = acceptChildWatchdogEvent({
796
- current: childWatchdogState,
797
- event,
798
- ...(childEventContext ? {
799
- runId: childEventContext.runId,
800
- agent: childEventContext.agent,
801
- childIndex: childEventContext.stepIndex,
802
- } : {}),
803
- });
804
- if (!next) return;
805
- updateChildWatchdogState(next);
806
- onChildEvent?.(event);
807
- if (childWatchdogIsActive(next)) {
808
- if (finalDrainTimer) {
809
- clearTimeout(finalDrainTimer);
810
- finalDrainTimer = undefined;
811
- }
812
- if (finalHardKillTimer) {
813
- clearTimeout(finalHardKillTimer);
814
- finalHardKillTimer = undefined;
815
- }
816
- armWatchdogTail();
817
- } else {
818
- clearWatchdogTailTimer();
819
- if (cleanTerminalAssistantStopReceived || agentSettledReceived) startFinalDrain();
820
- }
821
- return;
822
- }
823
-
824
- onChildEvent?.(event);
825
-
826
- if (event.type === "tool_execution_end") {
827
- clearActiveToolTimeout(event);
828
- removeActiveToolCall(event);
829
- return;
830
- }
831
-
832
- if (event.type === "tool_execution_start" && event.toolName) {
833
- toolCount += 1;
834
- armToolTimeout({ toolCallId: (event as { toolCallId?: unknown }).toolCallId, toolName: event.toolName });
835
- recordActiveToolCall({ toolCallId: (event as { toolCallId?: unknown }).toolCallId, toolName: event.toolName, args: event.args });
836
- if (event.toolName === "structured_output") {
837
- structuredOutputToolInvoked = true;
838
- structuredOutputMessageStartIndex = messages.length;
839
- }
840
- observedMutationAttempt = observedMutationAttempt || isMutatingTool(event.toolName, event.args, mutationTools);
841
- const toolArgs = extractToolArgsPreview(event.args ?? {});
842
- writeOutputLine(toolArgs ? `${event.toolName}: ${toolArgs}` : event.toolName);
843
- return;
844
- }
845
-
846
- if ((event.type === "message_end" || event.type === "tool_result_end") && event.message) {
847
- if (event.type === "tool_result_end") {
848
- clearActiveToolTimeout(event);
849
- removeActiveToolCall({
850
- toolCallId: (event.message as { toolCallId?: unknown }).toolCallId ?? (event as { toolCallId?: unknown }).toolCallId,
851
- toolName: (event.message as { toolName?: unknown }).toolName ?? event.toolName,
852
- });
853
- }
854
- messages.push(event.message);
855
- const text = extractTextFromContent(event.message.content);
856
- if (text) writeOutputText(text);
857
-
858
- if (childWatchdogConfig && event.type === "message_end") {
859
- const next = applyChildWatchdogMessage(childWatchdogState, event.message);
860
- if (next) updateChildWatchdogState(next);
861
- }
862
- if (event.type !== "message_end" || event.message.role !== "assistant") return;
863
- const hasToolCall = assistantStartsToolCall(event.message);
864
- if (event.message.model) {
865
- model = event.message.model;
866
- if (expectedModelForVerification && !hasToolCall) {
867
- const modelVerificationError = formatSubagentModelVerificationError(expectedModelForVerification, event.message.model, modelVerificationRegistry);
868
- if (modelVerificationError && !error) error = modelVerificationError;
869
- }
870
- }
871
- if (event.message.errorMessage) assistantError = event.message.errorMessage;
872
- const eventUsage = event.message.usage;
873
- if (eventUsage) {
874
- usage.turns++;
875
- usage.input += eventUsage.input ?? eventUsage.inputTokens ?? 0;
876
- usage.output += eventUsage.output ?? eventUsage.outputTokens ?? 0;
877
- usage.cacheRead += eventUsage.cacheRead ?? 0;
878
- usage.cacheWrite += eventUsage.cacheWrite ?? 0;
879
- usage.cost += eventUsage.cost?.total ?? 0;
880
- }
881
- if (isTerminalAssistantStop(event.message)) {
882
- if (!event.message.errorMessage && extractTextFromContent(event.message.content).trim()) assistantError = undefined;
883
- cleanTerminalAssistantStopReceived ||= !event.message.errorMessage;
884
- clearAllToolTimeouts();
885
- activeToolCalls.clear();
886
- activeToolKeysByName.clear();
887
- refreshCurrentTool();
888
- applyChildLifecycle(projectChildLifecycle(event, true, childLifecycleState));
889
- }
890
- }
891
- };
892
-
893
- // Guard both cases that can leave the parent waiting on `close` forever:
894
- // a lingering stdio holder after `exit`, or a child that never exits.
895
- const FINAL_STOP_GRACE_MS = 1000;
896
- const HARD_KILL_MS = 3000;
897
- let childExited = false;
898
- let forcedTerminationSignal = false;
899
- let cleanTerminalAssistantStopReceived = false;
900
- let agentSettledReceived = false;
901
- let compactionStartedReceived = false;
902
- let afterCompactionSettlement = false;
903
- let finalDrainTimer: NodeJS.Timeout | undefined;
904
- let finalHardKillTimer: NodeJS.Timeout | undefined;
905
- let watchdogTailTimer: NodeJS.Timeout | undefined;
906
- let protocolHardKillTimer: NodeJS.Timeout | undefined;
907
- let protocolError: ProtocolOutputLimit | undefined;
908
- let settled = false;
909
- applyChildLifecycle = (action: ChildLifecycleAction): void => {
910
- if (action === "cancel-drain") {
911
- if (finalDrainTimer) {
912
- clearTimeout(finalDrainTimer);
913
- finalDrainTimer = undefined;
914
- }
915
- if (finalHardKillTimer) {
916
- clearTimeout(finalHardKillTimer);
917
- finalHardKillTimer = undefined;
918
- }
919
- clearWatchdogTailTimer();
920
- return;
921
- }
922
- if (action === "start-drain") startFinalDrain();
923
- };
924
- const failProtocol = (limit: ProtocolOutputLimit): void => {
925
- if (protocolError) return;
926
- protocolError = limit;
927
- error = formatProtocolOutputLimit(limit);
928
- if (!childExited) {
929
- trySignalChild(child, "SIGTERM");
930
- protocolHardKillTimer = setTimeout(() => {
931
- if (!settled) trySignalChild(child, "SIGKILL");
932
- }, 3000);
933
- protocolHardKillTimer.unref?.();
934
- }
935
- };
936
- const stdoutReader = createBoundedLineReader({
937
- oversizedLineProjector: PI_AGGREGATE_EVENT_PROJECTOR,
938
- onLine: processStdoutLine,
939
- onLimit: failProtocol,
940
- });
941
- const stderrReader = createBoundedLineReader({
942
- stream: "stderr",
943
- maxPendingLineBytes: MAX_CHILD_STDERR_BYTES,
944
- onLine: (line) => appendChildLine("subagent.child.stderr", line),
945
- onLimit: (limit) => appendChildLine("subagent.child.stderr", formatProtocolOutputLimit(limit)),
946
- });
947
- const clearStdioGuard = attachPostExitStdioGuard(child, { idleMs: 2000, hardMs: 8000 });
948
- child.stdout.on("data", (chunk: Buffer) => stdoutReader.push(chunk));
949
- child.stderr.on("data", (chunk: Buffer) => {
950
- stderrTail.push(chunk);
951
- stderrReader.push(chunk);
952
- outputStream.write(chunk);
953
- orcaProgressTab?.append(chunk.toString("utf-8"));
954
- });
955
- registerInterrupt?.(() => {
956
- if (settled || timedOut || stopped) return;
957
- interrupted = true;
958
- if (!error) error = "Interrupted. Waiting for explicit next action.";
959
- trySignalChild(child, "SIGINT");
960
- setTimeout(() => {
961
- if (!settled && !timedOut && !stopped) trySignalChild(child, "SIGTERM");
962
- }, 1000).unref?.();
963
- });
964
- const terminateForTimeout = (message: string): void => {
965
- if (settled || timedOut || stopped) return;
966
- timedOut = true;
967
- // runPiStreaming's terminal result derives the timeout error from this
968
- // message, so retain the tool-specific reason through finalization.
969
- timeoutMessage = message;
970
- interrupted = false;
971
- error = message;
972
- if (processTreeController) void processTreeController.terminate();
973
- else trySignalChild(child, "SIGTERM");
974
- };
975
- let toolTimeoutSequence = 0;
976
- const activeToolTimeouts = new Map<string, { toolName: string; timer: ReturnType<typeof setTimeout> }>();
977
- const activeToolTimeoutKeysByName = new Map<string, string[]>();
978
- const removeToolTimeoutKey = (key: string): void => {
979
- const active = activeToolTimeouts.get(key);
980
- if (!active) return;
981
- clearTimeout(active.timer);
982
- activeToolTimeouts.delete(key);
983
- const keys = activeToolTimeoutKeysByName.get(active.toolName)?.filter((candidate) => candidate !== key) ?? [];
984
- if (keys.length > 0) activeToolTimeoutKeysByName.set(active.toolName, keys);
985
- else activeToolTimeoutKeysByName.delete(active.toolName);
986
- };
987
- const clearActiveToolTimeout = (event: { toolCallId?: unknown; toolName?: unknown }): void => {
988
- const key = typeof event.toolCallId === "string" && event.toolCallId.length > 0
989
- ? `id:${event.toolCallId}`
990
- : typeof event.toolName === "string"
991
- ? activeToolTimeoutKeysByName.get(event.toolName)?.[0]
992
- : activeToolTimeouts.size === 1
993
- ? [...activeToolTimeouts.keys()][0]
994
- : undefined;
995
- if (key) removeToolTimeoutKey(key);
996
- };
997
- const clearAllToolTimeouts = (): void => {
998
- for (const key of [...activeToolTimeouts.keys()]) removeToolTimeoutKey(key);
999
- };
1000
- const armToolTimeout = (event: { toolCallId?: unknown; toolName: string }): void => {
1001
- const timeoutForTool = effectiveToolTimeoutMs(event.toolName, toolTimeoutMs);
1002
- if (timeoutForTool === undefined) return;
1003
- const runRemaining = runDeadlineAt === undefined ? undefined : Math.max(0, runDeadlineAt - Date.now());
1004
- if (runRemaining !== undefined && timeoutForTool >= runRemaining) return;
1005
- const key = toolTimeoutCallKey(event, ++toolTimeoutSequence);
1006
- const toolName = event.toolName;
1007
- const timer = setTimeout(() => {
1008
- removeToolTimeoutKey(key);
1009
- terminateForTimeout(formatToolTimeoutMessage(toolName, timeoutForTool));
1010
- }, timeoutForTool);
1011
- timer.unref?.();
1012
- activeToolTimeouts.set(key, { toolName, timer });
1013
- const keys = activeToolTimeoutKeysByName.get(toolName) ?? [];
1014
- keys.push(key);
1015
- activeToolTimeoutKeysByName.set(toolName, keys);
1016
- };
1017
- registerTimeout?.(() => terminateForTimeout(timeoutMessage ?? "Subagent timed out."));
1018
- registerStop?.(() => {
1019
- if (settled || timedOut || stopped) return;
1020
- stopped = true;
1021
- interrupted = false;
1022
- error = stopMessage ?? "Subagent stopped by user.";
1023
- if (processTreeController) void processTreeController.terminate();
1024
- else trySignalChild(child, "SIGTERM");
1025
- });
1026
- const clearDrainTimers = () => {
1027
- clearAllToolTimeouts();
1028
- if (finalDrainTimer) {
1029
- clearTimeout(finalDrainTimer);
1030
- finalDrainTimer = undefined;
1031
- }
1032
- if (finalHardKillTimer) {
1033
- clearTimeout(finalHardKillTimer);
1034
- finalHardKillTimer = undefined;
1035
- }
1036
- clearWatchdogTailTimer();
1037
- if (protocolHardKillTimer) {
1038
- clearTimeout(protocolHardKillTimer);
1039
- protocolHardKillTimer = undefined;
1040
- }
1041
- };
1042
- function startFinalDrain(): void {
1043
- if (childWatchdogIsActive(childWatchdogState)) {
1044
- armWatchdogTail();
1045
- return;
1046
- }
1047
- if (childExited || finalDrainTimer || settled) return;
1048
- finalDrainTimer = setTimeout(() => {
1049
- if (settled) return;
1050
- const termSent = trySignalChild(child, "SIGTERM");
1051
- if (!termSent) return;
1052
- forcedTerminationSignal = true;
1053
- if (!cleanTerminalAssistantStopReceived && !agentSettledReceived && !error && !assistantError) {
1054
- error = `Subagent process did not exit within ${FINAL_STOP_GRACE_MS}ms after its terminal event. Forcing termination.`;
1055
- }
1056
- finalHardKillTimer = setTimeout(() => {
1057
- if (settled) return;
1058
- forcedTerminationSignal = trySignalChild(child, "SIGKILL") || forcedTerminationSignal;
1059
- }, HARD_KILL_MS);
1060
- finalHardKillTimer.unref?.();
1061
- }, FINAL_STOP_GRACE_MS);
1062
- finalDrainTimer.unref?.();
1063
- }
1064
- function clearWatchdogTailTimer(): void {
1065
- if (watchdogTailTimer) {
1066
- clearTimeout(watchdogTailTimer);
1067
- watchdogTailTimer = undefined;
1068
- }
1069
- }
1070
- function armWatchdogTail(): void {
1071
- if ((!cleanTerminalAssistantStopReceived && !agentSettledReceived) || watchdogTailTimer || settled) return;
1072
- watchdogTailTimer = setTimeout(() => {
1073
- watchdogTailTimer = undefined;
1074
- updateChildWatchdogState({
1075
- phase: "stale",
1076
- seq: (childWatchdogState?.seq ?? 0) + 1,
1077
- lastUpdate: Date.now(),
1078
- reason: "child watchdog tail timeout",
1079
- timedOut: true,
1080
- });
1081
- startFinalDrain();
1082
- }, childWatchdogConfig?.watchdogTailTimeoutMs ?? 120_000);
1083
- watchdogTailTimer.unref?.();
1084
- }
1085
- child.on("exit", () => {
1086
- childExited = true;
1087
- clearDrainTimers();
1088
- });
1089
- child.on("close", async (exitCode, signal) => {
1090
- settled = true;
1091
- const processCloseObservedAt = Date.now();
1092
- const processTree = processTreeController
1093
- ? await processTreeController.finishAfterWriterClose()
1094
- : { state: "unknown" as const, reason: "verification-failed" as const, diagnostic: "Writer PID was unavailable." };
1095
- try {
1096
- onWriterProcess?.({ state: "none" });
1097
- } catch {
1098
- // The runner still owns and releases the lease during finalization.
1099
- }
1100
- registerInterrupt?.(undefined);
1101
- registerTimeout?.(undefined);
1102
- registerStop?.(undefined);
1103
- clearDrainTimers();
1104
- clearStdioGuard();
1105
- stdoutReader.end();
1106
- stderrReader.end();
1107
- outputStream.end();
1108
- const stderr = stderrTail.text();
1109
- const finalOutput = getFinalOutput(messages) || rawStdoutTail.text().trim();
1110
- const finalError = error ?? assistantError;
1111
- const forcedDrainAfterFinalSuccess = Boolean(forcedTerminationSignal || signal) && (cleanTerminalAssistantStopReceived || agentSettledReceived) && !finalError;
1112
- const forcedDrainAfterEmptyTerminal = forcedDrainAfterFinalSuccess && hasEmptyTerminalAssistantResponse(messages);
1113
- const forcedDrainError = forcedDrainAfterEmptyTerminal && stderr.trim()
1114
- ? stderr.trim()
1115
- : undefined;
1116
- const signalError = isUnexplainedProcessSignal({
1117
- processSignal: signal,
1118
- interrupted,
1119
- timedOut,
1120
- stopped,
1121
- forcedDrainAfterFinalSuccess: forcedDrainAfterFinalSuccess && !forcedDrainAfterEmptyTerminal,
1122
- }) ? formatProcessSignalError(signal!) : undefined;
1123
- resolve(omitUndefinedProperties({
1124
- stderr,
1125
- exitCode: timedOut || stopped ? 1 : interrupted || (forcedDrainAfterFinalSuccess && !forcedDrainAfterEmptyTerminal) ? 0 : forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
1126
- messages,
1127
- usage,
1128
- toolCount,
1129
- durationMs: Date.now() - startedAt,
1130
- model,
1131
- error: stopped ? (stopMessage ?? "Subagent stopped by user.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : interrupted || (forcedDrainAfterFinalSuccess && !forcedDrainAfterEmptyTerminal) ? undefined : finalError ?? forcedDrainError ?? signalError,
1132
- protocolError,
1133
- finalOutput: (timedOut || stopped) && !finalOutput.trim() ? (stopped ? stopMessage ?? "Subagent stopped by user." : timeoutMessage ?? "Subagent timed out.") : finalOutput,
1134
- outputState: finalOutput.trim() ? "present" : "absent",
1135
- interrupted,
1136
- timedOut,
1137
- stopped,
1138
- observedMutationAttempt,
1139
- structuredOutputToolInvoked,
1140
- structuredOutputMessageStartIndex,
1141
- watchdog: childWatchdogState,
1142
- processInstanceId,
1143
- processCloseObservedAt,
1144
- processSignal: signal,
1145
- processTree,
1146
- currentTool,
1147
- currentToolArgs,
1148
- currentPath,
1149
- afterCompactionSettlement: afterCompactionSettlement || undefined,
1150
- }));
1151
- });
1152
-
1153
- child.on("error", (spawnError) => {
1154
- settled = true;
1155
- try {
1156
- onWriterProcess?.({ state: "none" });
1157
- } catch {
1158
- // The runner still owns and releases the lease during finalization.
1159
- }
1160
- registerInterrupt?.(undefined);
1161
- registerTimeout?.(undefined);
1162
- registerStop?.(undefined);
1163
- clearDrainTimers();
1164
- clearStdioGuard();
1165
- stdoutReader.end();
1166
- stderrReader.end();
1167
- outputStream.end();
1168
- const stderr = stderrTail.text();
1169
- const finalOutput = getFinalOutput(messages) || rawStdoutTail.text().trim();
1170
- const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
1171
- 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.") : 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, observedMutationAttempt, structuredOutputToolInvoked, structuredOutputMessageStartIndex, watchdog: childWatchdogState, processInstanceId, processTree: { state: "unknown", reason: "verification-failed", diagnostic: spawnErrorMessage } }));
1172
- });
1173
- });
1174
- }
1175
-
1176
545
  function resolvePiPackageRootFallback(): string {
1177
546
  const root = resolveInstalledPiPackageRoot();
1178
547
  if (root) return root;
@@ -1294,15 +663,17 @@ interface SingleStepContext {
1294
663
  flatIndex: number;
1295
664
  flatStepCount: number;
1296
665
  outputFile: string;
1297
- steerInboxDir?: string;
1298
- steerCapabilityPath?: string;
1299
- steerAckDir?: string;
1300
666
  transcriptPath?: string;
1301
667
  piPackageRoot?: string;
1302
- piArgv1?: string;
668
+ /** Factory the runner creates this step's child session through. */
669
+ childSessions: ChildSessionFactory;
670
+ /** The launching executor's own child runtime; nested route, depth, and ceilings come from here. */
671
+ inheritedChildRuntime?: InheritedChildRuntime;
1303
672
  registerInterrupt?: (interrupt: (() => void) | undefined) => void;
1304
673
  registerTimeout?: (interrupt: (() => void) | undefined) => void;
1305
674
  registerStop?: (stop: (() => void) | undefined) => void;
675
+ /** Receives the live child's steer handler while its session runs. */
676
+ registerSteer?: (steer: StepSteerHandler | undefined) => void;
1306
677
  timeoutSignal?: AbortSignal;
1307
678
  stopSignal?: AbortSignal;
1308
679
  timeoutMessage?: string;
@@ -1318,7 +689,6 @@ interface SingleStepContext {
1318
689
  runFanoutBudget?: RunFanoutBudgetDescriptor;
1319
690
  onAttemptStart?: (attempt: { model?: string; thinking?: string; contextLimit?: number }) => void;
1320
691
  onChildEvent?: (event: ChildEvent) => void;
1321
- onWriterProcess?: (writer: { state: "none" | "spawning" } | { state: "running"; pid: number }) => void;
1322
692
  onExternalProcess?: (process: ExternalProcessStatus) => void;
1323
693
  onExternalJob?: (status: ExternalJobStatus) => void;
1324
694
  skipAcceptance?: () => boolean;
@@ -1424,13 +794,11 @@ async function runSingleStepInner(
1424
794
  model: step.model,
1425
795
  modelCandidates: step.modelCandidates,
1426
796
  mcpDirectTools: step.mcpDirectTools,
1427
- mcpConfig: step.mcpConfig,
1428
- runtimeServerNames: step.runtimeServerNames,
1429
797
  cwd: step.cwd ?? ctx.cwd,
1430
798
  requireReadTool: Boolean(step.skills?.length),
1431
799
  structuredOutput: Boolean(effectiveStructuredOutput),
1432
800
  capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
1433
- inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
801
+ inheritedCapabilityCeiling: ctx.inheritedChildRuntime?.capabilityCeiling,
1434
802
  permissionRules: step.permissionRules,
1435
803
  }));
1436
804
  const contractTools = resolvedTaskToolPlan.explicitToolAllowlist ? resolvedTaskToolPlan.effectiveToolAllowlist : undefined;
@@ -1650,12 +1018,10 @@ async function runSingleStepInner(
1650
1018
  let capabilityAudit: import("../shared/capability-ceiling.ts").SubagentCapabilityAudit | undefined;
1651
1019
  let launchResolvedExtensions = step.launchResolvedExtensions;
1652
1020
  const modelAttempts: ModelAttempt[] = [];
1653
- const writerProcesses: PiWriterProcessInstanceExitV1[] = [];
1654
- let writerAttemptCount = 0;
1655
1021
  const attemptNotes: string[] = [];
1656
1022
  let finalRequiredOutputMissing: boolean | undefined;
1657
1023
  const eventsPath = path.join(path.dirname(ctx.outputFile), "events.jsonl");
1658
- let finalResult: RunPiStreamingResult | undefined;
1024
+ let finalResult: RunChildSessionResult | undefined;
1659
1025
  let finalOutputSnapshot: SingleOutputSnapshot | undefined;
1660
1026
  let structuredAcceptanceReport: unknown;
1661
1027
  let structuredAcceptanceReportError: string | undefined;
@@ -1667,10 +1033,6 @@ async function runSingleStepInner(
1667
1033
  let finalMutationEvidence = collectTrackedMutationEvidence(mutationSnapshot, step.cwd ?? ctx.cwd);
1668
1034
 
1669
1035
  let modelIndex = 0;
1670
- let startupAttemptIndex = 0;
1671
- // Escalated to "file" after an unexplained zero-activity startup failure so
1672
- // retries keep the task text out of argv (endpoint pre-exec scans may deny it).
1673
- let taskDeliveryOverride: SubagentTaskDelivery | undefined;
1674
1036
  let contextOverflow = false;
1675
1037
  let launchWarningsEmitted = false;
1676
1038
  let abortRecoveryAttempted = false;
@@ -1682,7 +1044,7 @@ async function runSingleStepInner(
1682
1044
  const candidate = candidates[modelIndex];
1683
1045
  const expectedModelForVerification = candidate && !(step.skipPrimaryModelVerification && modelIndex === 0) ? candidate : undefined;
1684
1046
  try {
1685
- assertThinkingWithinCeiling({ model: candidate, configThinking: step.thinking, ceiling: step.thinkingCeiling ?? decodeThinkingCeiling(process.env[SUBAGENT_THINKING_CEILING_ENV]), agent: step.agent, runId: ctx.id });
1047
+ assertThinkingWithinCeiling({ model: candidate, configThinking: step.thinking, ceiling: step.thinkingCeiling, agent: step.agent, runId: ctx.id });
1686
1048
  } catch (error) {
1687
1049
  const message = error instanceof Error ? error.message : String(error);
1688
1050
  return omitUndefinedProperties({ agent: step.agent, output: message, error: message, exitCode: 1, context: step.context, thinkingCeiling: step.thinkingCeiling });
@@ -1709,12 +1071,10 @@ async function runSingleStepInner(
1709
1071
  childIndex: ctx.flatIndex,
1710
1072
  })
1711
1073
  : undefined;
1712
- const { args, env, tempDir, toolDiagnosticPath, runtimeAcknowledgedExtensionsPath, capabilityAudit: attemptCapabilityAudit, warnings } = buildPiArgs(omitUndefinedProperties({
1074
+ let watchdogSink: ((event: ChildWatchdogStatusEvent) => void) | undefined;
1075
+ const launch = buildInProcessChildLaunch(omitUndefinedProperties({
1713
1076
  parentSessionId: step.parentSessionId,
1714
1077
  forkCacheKey: step.context === "fork" ? deriveForkPromptCacheKey(step.parentSessionId) : undefined,
1715
- baseArgs: ["--mode", "json", "-p"],
1716
- task: attemptTask,
1717
- taskDelivery: taskDeliveryOverride,
1718
1078
  sessionEnabled,
1719
1079
  sessionDir,
1720
1080
  sessionFile: step.sessionFile,
@@ -1733,28 +1093,20 @@ async function runSingleStepInner(
1733
1093
  systemPrompt: step.systemPrompt ?? "",
1734
1094
  systemPromptMode: step.systemPromptMode,
1735
1095
  mcpDirectTools: step.mcpDirectTools,
1736
- mcpConfig: step.mcpConfig,
1737
- runtimeServerNames: step.runtimeServerNames,
1096
+ extensionBindings,
1738
1097
  capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
1739
1098
  cwd: step.cwd ?? ctx.cwd,
1740
- promptFileStem: step.agent,
1741
1099
  intercomSessionName: ctx.childIntercomTarget,
1742
1100
  sessionName: childSessionName,
1743
1101
  orchestratorIntercomTarget: ctx.orchestratorIntercomTarget,
1744
1102
  runId: ctx.id,
1745
1103
  childAgentName: step.agent,
1746
1104
  childIndex: ctx.flatIndex,
1747
- parentEventSink: ctx.nestedRoute?.eventSink,
1748
- parentControlInbox: ctx.nestedRoute?.controlInbox,
1749
- parentRootRunId: ctx.nestedRoute?.rootRunId,
1750
- parentCapabilityToken: ctx.nestedRoute?.capabilityToken,
1105
+ nestedRoute: ctx.nestedRoute,
1751
1106
  runFanoutBudget: ctx.runFanoutBudget ? {
1752
1107
  ...ctx.runFanoutBudget,
1753
1108
  ...(step.runFanoutPath ? { parentPath: `${ctx.runFanoutBudget.parentPath ? `${ctx.runFanoutBudget.parentPath}/` : ""}${step.runFanoutPath}` } : {}),
1754
1109
  } : undefined,
1755
- steerInboxDir: ctx.steerInboxDir,
1756
- steerCapabilityPath: ctx.steerCapabilityPath,
1757
- steerAckDir: ctx.steerAckDir,
1758
1110
  structuredOutput: effectiveStructuredOutput,
1759
1111
  toolBudget: step.toolBudget,
1760
1112
  permissionRules: step.permissionRules,
@@ -1762,11 +1114,19 @@ async function runSingleStepInner(
1762
1114
  ? path.join(ctx.artifactsDir, "permission-audit", `${ctx.id}-${ctx.flatIndex}.jsonl`)
1763
1115
  : undefined,
1764
1116
  childWatchdog,
1117
+ watchdogStatus: (event) => watchdogSink?.(event),
1765
1118
  waitToolEnabled: step.waitToolEnabled,
1766
1119
  waitToolDefaultTimeoutMs: step.waitToolDefaultTimeoutMs,
1767
1120
  thinkingCeiling: step.thinkingCeiling,
1768
- extensionBindings,
1121
+ maxSubagentDepth: step.maxSubagentDepth,
1122
+ inherited: ctx.inheritedChildRuntime,
1123
+ host: "runner",
1769
1124
  }));
1125
+ if (effectiveStructuredOutput && launch.config.structuredOutput) {
1126
+ // The runner reads the value back from the runtime's files after the run.
1127
+ launch.config.structuredOutput.capture = createStructuredOutputFileCapture(effectiveStructuredOutput);
1128
+ }
1129
+ const { warnings, capabilityAudit: attemptCapabilityAudit } = launch;
1770
1130
  if (!launchWarningsEmitted && warnings.length > 0) {
1771
1131
  for (const warning of warnings) console.warn(`[pi-subagents] ${warning}`);
1772
1132
  launchWarningsEmitted = true;
@@ -1782,13 +1142,11 @@ async function runSingleStepInner(
1782
1142
  model: step.model,
1783
1143
  modelCandidates: step.modelCandidates,
1784
1144
  mcpDirectTools: step.mcpDirectTools,
1785
- mcpConfig: step.mcpConfig,
1786
- runtimeServerNames: step.runtimeServerNames,
1787
1145
  cwd: step.cwd ?? ctx.cwd,
1788
1146
  requireReadTool: Boolean(step.skills?.length),
1789
1147
  structuredOutput: Boolean(effectiveStructuredOutput),
1790
1148
  capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
1791
- inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
1149
+ inheritedCapabilityCeiling: ctx.inheritedChildRuntime?.capabilityCeiling,
1792
1150
  permissionRules: step.permissionRules,
1793
1151
  }));
1794
1152
  launchResolvedExtensions = projectLaunchResolvedChildExtensions(toolPlan);
@@ -1817,59 +1175,50 @@ async function runSingleStepInner(
1817
1175
  }));
1818
1176
  }
1819
1177
  capabilityAudit = attemptCapabilityAudit;
1820
- writerAttemptCount += 1;
1821
- const run = await runPiStreaming(
1822
- args,
1823
- step.cwd ?? ctx.cwd,
1824
- ctx.outputFile,
1825
- env,
1826
- ctx.piPackageRoot,
1827
- ctx.piArgv1,
1828
- step.maxSubagentDepth,
1829
- { eventsPath, runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
1830
- ctx.registerInterrupt,
1831
- ctx.onChildEvent,
1178
+ // Each attempt rewrites the step output log; synchronous appends keep a
1179
+ // retried attempt from interleaving with the previous attempt's flush.
1180
+ fs.writeFileSync(ctx.outputFile, "", "utf-8");
1181
+ const run = await runChildSession(omitUndefinedProperties({
1182
+ factory: ctx.childSessions,
1183
+ launch,
1184
+ prompt: `Task: ${attemptTask}`,
1185
+ childWatchdog,
1186
+ childEventContext: { runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
1187
+ appendChildEvent: (event) => appendDiagnosticJsonl(eventsPath, JSON.stringify(event), typeof event.type === "string" ? event.type : undefined),
1188
+ writeOutputLine: (line) => {
1189
+ try {
1190
+ fs.appendFileSync(ctx.outputFile, `${line}\n`, "utf-8");
1191
+ } catch {
1192
+ // The output log is observability only.
1193
+ }
1194
+ ctx.orcaProgressTab?.append(`${line}\n`);
1195
+ },
1196
+ registerInterrupt: ctx.registerInterrupt,
1197
+ registerTimeout: ctx.registerTimeout,
1198
+ registerStop: ctx.registerStop,
1199
+ registerSteer: ctx.registerSteer,
1200
+ registerWatchdogStatus: (sink) => { watchdogSink = sink; },
1201
+ timeoutMessage: ctx.timeoutMessage,
1202
+ stopMessage: ctx.stopMessage,
1203
+ onChildEvent: ctx.onChildEvent,
1832
1204
  transcriptWriter,
1833
- ctx.registerTimeout,
1834
- ctx.timeoutMessage,
1835
- ctx.registerStop,
1836
- ctx.stopMessage,
1837
- ctx.onWriterProcess,
1838
- ctx.toolTimeoutMs,
1839
- ctx.deadlineAt,
1840
- ctx.orcaProgressTab,
1205
+ toolTimeoutMs: ctx.toolTimeoutMs,
1206
+ runDeadlineAt: ctx.deadlineAt,
1841
1207
  expectedModelForVerification,
1842
- step.modelVerificationRegistry,
1843
- step.mutationTools,
1844
- );
1845
- if (run.processCloseObservedAt !== undefined) {
1846
- writerProcesses.push({
1847
- processInstanceId: run.processInstanceId,
1848
- kind: "pi-writer",
1849
- attempt: writerAttemptCount - 1,
1850
- closeObservedAt: run.processCloseObservedAt,
1851
- exitCode: run.exitCode,
1852
- signal: run.processSignal ?? null,
1853
- processTree: run.processTree,
1854
- });
1855
- }
1856
- const toolAvailabilityError = run.exitCode === 0 && !run.error
1857
- ? readChildToolDiagnosticError(toolDiagnosticPath)
1858
- : undefined;
1859
- const runtimeAcknowledgedExtensions = readRuntimeAcknowledgedExtensions(runtimeAcknowledgedExtensionsPath);
1860
- cleanupTempDir(tempDir);
1208
+ modelVerificationRegistry: step.modelVerificationRegistry,
1209
+ modelResponseAliases: step.modelResponseAliases,
1210
+ mutationTools: step.mutationTools,
1211
+ }));
1212
+ const toolDiagnostic = run.exitCode === 0 && !run.error ? launch.capture.toolDiagnostic() : undefined;
1213
+ const toolAvailabilityError = toolDiagnostic ? formatChildToolDiagnostic(toolDiagnostic) : undefined;
1214
+ const runtimeAcknowledgedExtensions = launch.capture.runtimeAcknowledgedExtensions();
1861
1215
  const midToolExitError = run.currentTool
1862
1216
  && isOrdinaryToolForMidToolExit(run.currentTool)
1863
1217
  && !run.interrupted
1864
1218
  && !run.timedOut
1865
1219
  && !run.stopped
1866
- && !run.protocolError
1867
1220
  && !toolAvailabilityError
1868
- ? formatMidToolExitError({
1869
- toolName: run.currentTool,
1870
- exitCode: run.exitCode,
1871
- processSignal: run.processSignal,
1872
- })
1221
+ ? formatMidToolExitError({ toolName: run.currentTool })
1873
1222
  : undefined;
1874
1223
 
1875
1224
  let structuredOutput: unknown;
@@ -1956,19 +1305,10 @@ async function runSingleStepInner(
1956
1305
  : run.error && run.exitCode === 0
1957
1306
  ? 1
1958
1307
  : run.exitCode;
1959
- const signalError = run.exitCode !== 0 && isUnexplainedProcessSignal(omitUndefinedProperties({
1960
- processSignal: run.processSignal,
1961
- interrupted: run.interrupted,
1962
- timedOut: run.timedOut,
1963
- stopped: run.stopped,
1964
- })) ? formatProcessSignalError(run.processSignal!) : undefined;
1965
1308
  const underlyingError = toolAvailabilityError
1966
1309
  ?? midToolExitError
1967
1310
  ?? structuredError
1968
1311
  ?? run.error
1969
- ?? signalError
1970
- ?? (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined)
1971
- ?? ((emptyOutputError || missingRequiredOutputError) && run.stderr.trim() ? run.stderr.trim() : undefined)
1972
1312
  ?? emptyOutputError
1973
1313
  ?? (missingRequiredOutputAfterMutation ? missingRequiredOutputError : undefined)
1974
1314
  ?? (hiddenError?.hasError
@@ -1976,13 +1316,7 @@ async function runSingleStepInner(
1976
1316
  ? `${hiddenError.errorType} failed (exit ${effectiveExitCode}): ${hiddenError.details}`
1977
1317
  : `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
1978
1318
  : undefined);
1979
- const error = formatSubagentExtensionConflictError(
1980
- underlyingError ?? missingRequiredOutputError ?? completionEvidence.legacyFailureError,
1981
- {
1982
- agent: step.agent,
1983
- ambientExtensionsEnabled: launchResolvedExtensions?.disableAmbientExtensions === false,
1984
- },
1985
- );
1319
+ const error = underlyingError ?? missingRequiredOutputError ?? completionEvidence.legacyFailureError;
1986
1320
  const attempt: ModelAttempt = omitUndefinedProperties({
1987
1321
  model: candidate ?? run.model ?? step.model ?? "default",
1988
1322
  success: effectiveExitCode === 0 && !error,
@@ -1991,7 +1325,7 @@ async function runSingleStepInner(
1991
1325
  usage: run.usage,
1992
1326
  });
1993
1327
  modelAttempts.push(attempt);
1994
- if (!recoveringAbort && candidate && startupAttemptIndex === 0) attemptedModels.push(candidate);
1328
+ if (!recoveringAbort && candidate) attemptedModels.push(candidate);
1995
1329
  completionGuardTriggeredFinal = completionEvidence.guardTriggered && !underlyingError && !missingRequiredOutputError;
1996
1330
  finalOutputSnapshot = outputSnapshot;
1997
1331
  if (step.toolBudget) {
@@ -2008,11 +1342,10 @@ async function runSingleStepInner(
2008
1342
  afterCompactionSettlement: run.afterCompactionSettlement === true,
2009
1343
  });
2010
1344
  const fileMutationEffect = completionEvidence.fileMutation ?? (missingRequiredOutputAfterMutation ? { status: "observed" as const, expected: completionEvidence.mutationExpected, attempted: true, evidence: mutationEvidence } : undefined);
2011
- finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error, structuredOutput, runtimeAcknowledgedExtensions, ...(step.agentContract ? { agentContract: step.agentContract } : {}), ...(fileMutationEffect || settlementDiagnostic ? { effects: { ...(fileMutationEffect ? { fileMutation: fileMutationEffect } : {}), ...(settlementDiagnostic ? { settlementDiagnostic } : {}) } } : {}) } as RunPiStreamingResult;
1345
+ finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error, structuredOutput, runtimeAcknowledgedExtensions, ...(step.agentContract ? { agentContract: step.agentContract } : {}), ...(fileMutationEffect || settlementDiagnostic ? { effects: { ...(fileMutationEffect ? { fileMutation: fileMutationEffect } : {}), ...(settlementDiagnostic ? { settlementDiagnostic } : {}) } } : {}) } as RunChildSessionResult;
2012
1346
  const abortRecovery = !attempt.success ? planAbortRecovery({
2013
1347
  messages: run.messages,
2014
1348
  error,
2015
- processSignal: run.processSignal,
2016
1349
  sessionAvailable: Boolean(step.sessionFile && fs.existsSync(step.sessionFile)),
2017
1350
  alreadyResumed: abortRecoveryAttempted,
2018
1351
  stopped: run.stopped || ctx.stopSignal?.aborted || ctx.skipAcceptance?.(),
@@ -2043,50 +1376,6 @@ async function runSingleStepInner(
2043
1376
  }
2044
1377
  if (completionEvidence.guardTriggered) break modelAttemptsLoop;
2045
1378
 
2046
- const startupFailure = isRetryableSubagentStartupFailure(omitUndefinedProperties({
2047
- exitCode: effectiveExitCode,
2048
- error,
2049
- finalOutput: run.finalOutput,
2050
- messageCount: run.messages.length,
2051
- toolCount: run.toolCount,
2052
- usage: run.usage,
2053
- durationMs: run.durationMs,
2054
- protocolError: run.protocolError,
2055
- processSignal: run.processSignal,
2056
- observedMutationAttempt: run.observedMutationAttempt,
2057
- interrupted: run.interrupted,
2058
- timedOut: run.timedOut,
2059
- stopped: run.stopped,
2060
- }));
2061
- const retryDelayMs = SUBAGENT_STARTUP_RETRY_DELAYS_MS[startupAttemptIndex];
2062
- if (startupFailure && retryDelayMs !== undefined) {
2063
- const retryNote = formatSubagentStartupRetryNote({
2064
- model: attempt.model,
2065
- attempt: startupAttemptIndex + 1,
2066
- maxAttempts: SUBAGENT_STARTUP_RETRY_DELAYS_MS.length + 1,
2067
- delayMs: retryDelayMs,
2068
- });
2069
- const shouldRetry = await waitForSubagentStartupRetry(retryDelayMs, [ctx.timeoutSignal, ctx.stopSignal]);
2070
- if (!shouldRetry || ctx.skipAcceptance?.()) break modelAttemptsLoop;
2071
- if (!taskDeliveryOverride && run.processSignal === "SIGKILL") {
2072
- taskDeliveryOverride = "file";
2073
- attemptNotes.push("[startup-retry] retrying with file task delivery to keep the task text out of the child process argv.");
2074
- }
2075
- attempt.error = retryNote;
2076
- attemptNotes.push(retryNote);
2077
- startupAttemptIndex += 1;
2078
- continue;
2079
- }
2080
- if (startupFailure) {
2081
- const startupError = formatSubagentStartupRetryExhaustedError({
2082
- model: attempt.model,
2083
- attempts: startupAttemptIndex + 1,
2084
- });
2085
- attempt.error = startupError;
2086
- finalResult.error = startupError;
2087
- finalResult.finalOutput = startupError;
2088
- break modelAttemptsLoop;
2089
- }
2090
1379
  const retryableModelFailure = isRetryableModelFailureAttempt({ error, messages: run.messages, toolCount: run.toolCount });
2091
1380
  if (retryableModelFailure) recordRetryableModelFailure(candidate ?? run.model ?? step.model, error);
2092
1381
  if (isContextOverflow(error)) {
@@ -2097,7 +1386,6 @@ async function runSingleStepInner(
2097
1386
  if (!retryableModelFailure || modelIndex === candidates.length - 1) break modelAttemptsLoop;
2098
1387
  attemptNotes.push(formatModelAttemptNote(attempt, candidates[modelIndex + 1]));
2099
1388
  modelIndex += 1;
2100
- startupAttemptIndex = 0;
2101
1389
  }
2102
1390
 
2103
1391
  const rawOutput = finalResult?.finalOutput ?? "";
@@ -2125,7 +1413,7 @@ async function runSingleStepInner(
2125
1413
  ? extractChildWrittenOutput(finalResult?.messages, step.outputPath, step.cwd ?? ctx.cwd)
2126
1414
  : undefined;
2127
1415
  const outputState: SubagentOutputState = finalResult?.outputState === "present"
2128
- || (finalResult as (RunPiStreamingResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput !== undefined
1416
+ || (finalResult as (RunChildSessionResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput !== undefined
2129
1417
  || Boolean(childWrittenOutput?.trim())
2130
1418
  ? "present"
2131
1419
  : resolvedOutput.savedPath
@@ -2227,7 +1515,7 @@ async function runSingleStepInner(
2227
1515
  ...(capabilityAudit ? { capabilityCeiling: capabilityAudit.ceiling, capabilityAudit } : {}),
2228
1516
  launchContractDigest: actualLaunchContractDigest,
2229
1517
  launchResolvedExtensions,
2230
- ...((finalResult as (RunPiStreamingResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }) | undefined)?.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: (finalResult as RunPiStreamingResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }).runtimeAcknowledgedExtensions } : {}),
1518
+ ...((finalResult as (RunChildSessionResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }) | undefined)?.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: (finalResult as RunChildSessionResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }).runtimeAcknowledgedExtensions } : {}),
2231
1519
  ...(transcriptWriter ? { transcriptPath: artifactPaths.transcriptPath } : {}),
2232
1520
  transcriptError: transcriptWriter?.getError(),
2233
1521
  skills: step.skills,
@@ -2246,7 +1534,6 @@ async function runSingleStepInner(
2246
1534
  outputState,
2247
1535
  exitCode: effectiveFinalExitCode,
2248
1536
  error: effectiveFinalError,
2249
- protocolError: finalResult?.protocolError,
2250
1537
  sessionFile: step.sessionFile,
2251
1538
  intercomTarget: ctx.childIntercomTarget,
2252
1539
  model: finalResult?.model,
@@ -2264,22 +1551,19 @@ async function runSingleStepInner(
2264
1551
  interrupted: timedOutAfterAcceptance || stoppedAfterAcceptance ? false : finalResult?.interrupted,
2265
1552
  timedOut: timedOutAfterAcceptance ? true : finalResult?.timedOut,
2266
1553
  stopped: stoppedAfterAcceptance ? true : finalResult?.stopped,
2267
- processSignal: finalResult?.processSignal,
2268
1554
  timeoutRecovery,
2269
1555
  toolBudget,
2270
1556
  toolBudgetBlocked: toolBudgetBlocked || undefined,
2271
1557
  completionGuardTriggered: completionGuardTriggeredFinal,
2272
- ...((finalResult as (RunPiStreamingResult & { effects?: import("../../shared/types.ts").EffectsProjection }) | undefined)?.effects ? { effects: (finalResult as RunPiStreamingResult & { effects?: import("../../shared/types.ts").EffectsProjection }).effects } : {}),
2273
- structuredOutput: timedOutAfterAcceptance || stoppedAfterAcceptance ? undefined : (finalResult as (RunPiStreamingResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput,
1558
+ ...((finalResult as (RunChildSessionResult & { effects?: import("../../shared/types.ts").EffectsProjection }) | undefined)?.effects ? { effects: (finalResult as RunChildSessionResult & { effects?: import("../../shared/types.ts").EffectsProjection }).effects } : {}),
1559
+ structuredOutput: timedOutAfterAcceptance || stoppedAfterAcceptance ? undefined : (finalResult as (RunChildSessionResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput,
2274
1560
  structuredOutputPath: timedOutAfterAcceptance || stoppedAfterAcceptance ? undefined : effectiveStructuredOutput?.outputPath,
2275
1561
  structuredOutputSchemaPath: timedOutAfterAcceptance || stoppedAfterAcceptance ? undefined : effectiveStructuredOutput?.schemaPath,
2276
1562
  acceptance: effectiveAcceptance,
2277
1563
  watchdog: finalResult?.watchdog,
2278
1564
  ...(capabilityAudit ? { capabilityCeiling: capabilityAudit.ceiling, capabilityAudit } : {}),
2279
1565
  launchResolvedExtensions,
2280
- ...((finalResult as (RunPiStreamingResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }) | undefined)?.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: (finalResult as RunPiStreamingResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }).runtimeAcknowledgedExtensions } : {}),
2281
- writerProcesses,
2282
- writerAttemptCount,
1566
+ ...((finalResult as (RunChildSessionResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }) | undefined)?.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: (finalResult as RunChildSessionResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }).runtimeAcknowledgedExtensions } : {}),
2283
1567
  });
2284
1568
  return isAgentContractV1(step.agentContract) ? attachContractProjections(result as unknown as import("../../shared/types.ts").SingleResult) as unknown as typeof result : result;
2285
1569
  }
@@ -2573,7 +1857,7 @@ async function runSingleStepWithTimeout(
2573
1857
 
2574
1858
  async function runSubagent(
2575
1859
  config: SubagentRunConfig,
2576
- onWriterProcess?: (writer: { state: "none" | "spawning" } | { state: "running"; pid: number }) => void,
1860
+ childSessions: ChildSessionFactory,
2577
1861
  ): Promise<void> {
2578
1862
  const { id, steps, resultPath, cwd, placeholder, taskIndex, totalTasks, maxOutput, artifactsDir, artifactConfig } =
2579
1863
  config;
@@ -2593,9 +1877,11 @@ async function runSubagent(
2593
1877
  const activeChildInterrupts = new Map<number, () => void>();
2594
1878
  const activeChildTimeouts = new Map<number, () => void>();
2595
1879
  const activeChildStops = new Map<number, () => void>();
1880
+ const activeChildSteers = new Map<number, StepSteerHandler>();
1881
+ /** Steers routed to a running step before its session was created. */
1882
+ const queuedStepSteers = new Map<number, SteerRequest[]>();
2596
1883
  const childStopRequests = new Map<number, { childId: string; requestedAt: number }>();
2597
1884
  const pendingStepSteers: SteerRequest[] = [];
2598
- const steeringCapabilities = new Map<number, SteerCapability>();
2599
1885
  let interrupted = false;
2600
1886
  let currentActivityState: ActivityState | undefined;
2601
1887
  let activityTimer: NodeJS.Timeout | undefined;
@@ -3053,6 +2339,16 @@ async function runSubagent(
3053
2339
  activeChildStops.set(flatIndex, stop);
3054
2340
  if (stopped || childStopRequests.has(flatIndex)) stop();
3055
2341
  };
2342
+ const registerStepSteer = (flatIndex: number, steer: StepSteerHandler | undefined): void => {
2343
+ if (!steer) {
2344
+ activeChildSteers.delete(flatIndex);
2345
+ return;
2346
+ }
2347
+ activeChildSteers.set(flatIndex, steer);
2348
+ const queued = queuedStepSteers.get(flatIndex);
2349
+ queuedStepSteers.delete(flatIndex);
2350
+ for (const request of queued ?? []) steerLiveChild(flatIndex, request);
2351
+ };
3056
2352
  const interruptActiveChildren = (): void => {
3057
2353
  for (const interrupt of [...activeChildInterrupts.values()]) interrupt();
3058
2354
  };
@@ -3505,22 +2801,15 @@ async function runSubagent(
3505
2801
  if (!step) return { index, state: "failed" as const, reason: "child index out of range" };
3506
2802
  if (step.status === "pending") return { index, state: "scheduled" as const };
3507
2803
  if (step.status !== "running") return { index, state: "failed" as const, reason: `child is ${step.status}` };
3508
- if (steeringCapabilities.get(index)?.supported === false) return { index, state: "failed" as const, reason: "child Pi session does not support steering" };
3509
2804
  return { index, state: "routed" as const };
3510
2805
  });
3511
2806
  recordSteeringLifecycle(request, targetStates);
3512
2807
  emitSteeringEvent("subagent.steer.requested", request, undefined, { targets: targetStates });
3513
2808
  for (const target of targetStates) {
3514
2809
  if (target.state === "routed") {
3515
- try {
3516
- enqueueStepSteer(asyncDir, target.index, request);
3517
- updateSteeringLifecycleTarget(request.id, target.index, "routed", now);
3518
- emitSteeringEvent("subagent.steer.routed", request, target.index);
3519
- } catch (error) {
3520
- markSteeringAttention(target.index);
3521
- updateSteeringLifecycleTarget(request.id, target.index, "failed", now, { reason: error instanceof Error ? error.message : String(error) });
3522
- emitSteeringEvent("subagent.steer.failed", request, target.index, { reason: error instanceof Error ? error.message : String(error) });
3523
- }
2810
+ updateSteeringLifecycleTarget(request.id, target.index, "routed", now);
2811
+ emitSteeringEvent("subagent.steer.routed", request, target.index);
2812
+ steerLiveChild(target.index, request);
3524
2813
  } else if (target.state === "failed") {
3525
2814
  markSteeringAttention(target.index);
3526
2815
  emitSteeringEvent("subagent.steer.failed", request, target.index, { reason: target.reason });
@@ -3532,27 +2821,42 @@ async function runSubagent(
3532
2821
  statusPayload.lastUpdate = now;
3533
2822
  writeStatusPayload();
3534
2823
  };
3535
- const consumeSteerAck = (ack: SteerAck): void => {
2824
+ /** Record the outcome of handing a routed steer to the live child session. */
2825
+ const applySteerDelivery = (requestId: string, index: number, delivery: { state: "delivered" | "queued" | "failed"; message: string }): void => {
3536
2826
  const lifecycle = steeringStatus(statusPayload);
3537
- const request = lifecycle.recent.find((candidate) => candidate.id === ack.requestId);
3538
- if (!request || !request.targets.some((target) => target.index === ack.index)) return;
3539
- const late = fs.existsSync(steeringMarkerPath(ack.requestId));
2827
+ const request = lifecycle.recent.find((candidate) => candidate.id === requestId);
2828
+ if (!request || !request.targets.some((target) => target.index === index)) return;
2829
+ const late = fs.existsSync(steeringMarkerPath(requestId));
3540
2830
  const now = Date.now();
3541
- if (ack.state === "delivered") {
3542
- updateSteeringLifecycleTarget(ack.requestId, ack.index, late ? "late" : "delivered", now, omitUndefinedProperties({ reason: late ? "acknowledged after recovery commit" : undefined }));
3543
- emitSteeringEvent("subagent.steer.delivered", { type: "steer", id: ack.requestId, ts: now, message: ack.message }, ack.index, { late, deliveryStatus: "delivered", message: ack.message });
3544
- } else if (ack.state === "queued") {
3545
- updateSteeringLifecycleTarget(ack.requestId, ack.index, "queued", now);
3546
- emitSteeringEvent("subagent.steer.queued", { type: "steer", id: ack.requestId, ts: now, message: ack.message }, ack.index, { deliveryStatus: "queued", message: ack.message });
2831
+ if (delivery.state === "delivered") {
2832
+ updateSteeringLifecycleTarget(requestId, index, late ? "late" : "delivered", now, omitUndefinedProperties({ reason: late ? "acknowledged after recovery commit" : undefined }));
2833
+ emitSteeringEvent("subagent.steer.delivered", { type: "steer", id: requestId, ts: now, message: delivery.message }, index, { late, deliveryStatus: "delivered", message: delivery.message });
2834
+ } else if (delivery.state === "queued") {
2835
+ updateSteeringLifecycleTarget(requestId, index, "queued", now);
2836
+ emitSteeringEvent("subagent.steer.queued", { type: "steer", id: requestId, ts: now, message: delivery.message }, index, { deliveryStatus: "queued", message: delivery.message });
3547
2837
  } else {
3548
- markSteeringAttention(ack.index);
3549
- updateSteeringLifecycleTarget(ack.requestId, ack.index, "failed", now, { reason: ack.message });
3550
- emitSteeringEvent("subagent.steer.failed", { type: "steer", id: ack.requestId, ts: now, message: ack.message }, ack.index, { reason: ack.message });
2838
+ markSteeringAttention(index);
2839
+ updateSteeringLifecycleTarget(requestId, index, "failed", now, { reason: delivery.message });
2840
+ emitSteeringEvent("subagent.steer.failed", { type: "steer", id: requestId, ts: now, message: delivery.message }, index, { reason: delivery.message });
3551
2841
  }
3552
- emitTerminalSteeringNotice(ack.requestId, `Steering failed for run ${id}: ${ack.message}`);
2842
+ emitTerminalSteeringNotice(requestId, `Steering failed for run ${id}: ${delivery.message}`);
3553
2843
  statusPayload.lastUpdate = now;
3554
2844
  writeStatusPayload();
3555
2845
  };
2846
+ /** Hand a routed steer to the step's live session, or hold it until the session exists. */
2847
+ const steerLiveChild = (index: number, request: SteerRequest): void => {
2848
+ const steer = activeChildSteers.get(index);
2849
+ if (!steer) {
2850
+ const queued = queuedStepSteers.get(index) ?? [];
2851
+ queued.push(request);
2852
+ queuedStepSteers.set(index, queued);
2853
+ return;
2854
+ }
2855
+ void steer(request).then(
2856
+ (delivery) => applySteerDelivery(request.id, index, delivery),
2857
+ (error) => applySteerDelivery(request.id, index, { state: "failed", message: error instanceof Error ? error.message : String(error) }),
2858
+ );
2859
+ };
3556
2860
  const flushPendingStepSteers = (flatIndex: number): void => {
3557
2861
  const remaining: SteerRequest[] = [];
3558
2862
  for (const request of pendingStepSteers.splice(0)) {
@@ -3636,6 +2940,7 @@ async function runSubagent(
3636
2940
  tokens: step.tokens?.total,
3637
2941
  toolCount: step.toolCount,
3638
2942
  currentTool: step.currentTool,
2943
+ toolCallId: event.toolCallId,
3639
2944
  currentToolDurationMs: 0,
3640
2945
  currentPath: step.currentPath,
3641
2946
  })));
@@ -3910,23 +3215,6 @@ async function runSubagent(
3910
3215
  pendingStepSteers.push(request);
3911
3216
  }
3912
3217
  },
3913
- onSteerCapability: (capability) => {
3914
- steeringCapabilities.set(capability.index, capability);
3915
- if (!capability.supported) {
3916
- const now = Date.now();
3917
- const lifecycle = steeringStatus(statusPayload);
3918
- for (const request of lifecycle.recent) {
3919
- if (!request.targets.some((target) => target.index === capability.index && (target.state === "routed" || target.state === "scheduled"))) continue;
3920
- markSteeringAttention(capability.index);
3921
- updateSteeringLifecycleTarget(request.id, capability.index, "failed", now, { reason: "child Pi session does not support steering" });
3922
- emitSteeringEvent("subagent.steer.failed", { type: "steer", id: request.id, ts: request.requestedAt, message: "child Pi session does not support steering" }, capability.index, { reason: "child Pi session does not support steering" });
3923
- emitTerminalSteeringNotice(request.id, `Steering failed for run ${id}: child ${capability.index} does not support steering.`);
3924
- }
3925
- statusPayload.lastUpdate = now;
3926
- writeStatusPayload();
3927
- }
3928
- },
3929
- onSteerAck: consumeSteerAck,
3930
3218
  });
3931
3219
  if (config.deadlineAt !== undefined) {
3932
3220
  const remainingMs = Math.max(0, config.deadlineAt - Date.now());
@@ -3983,7 +3271,7 @@ async function runSubagent(
3983
3271
  : [undefined]
3984
3272
  : model ? [model] : [undefined];
3985
3273
  for (const candidate of candidates) {
3986
- assertThinkingWithinCeiling({ model: candidate, configThinking, ceiling: step.parallel.thinkingCeiling ?? decodeThinkingCeiling(process.env[SUBAGENT_THINKING_CEILING_ENV]), agent: step.parallel.agent, runId: id });
3274
+ assertThinkingWithinCeiling({ model: candidate, configThinking, ceiling: step.parallel.thinkingCeiling, agent: step.parallel.agent, runId: id });
3987
3275
  }
3988
3276
  }
3989
3277
  if (materialized.collectedOnEmpty) await validateDynamicCollection(step.collect.outputSchema, materialized.collectedOnEmpty);
@@ -4253,11 +3541,9 @@ async function runSubagent(
4253
3541
  artifactsDir, artifactConfig, id,
4254
3542
  flatIndex: fi, flatStepCount: Math.max(statusPayload.steps.length, 1),
4255
3543
  outputFile: path.join(asyncDir, `output-${fi}.log`),
4256
- steerInboxDir: stepSteerInboxDir(asyncDir, fi),
4257
- steerCapabilityPath: steerCapabilityPath(asyncDir, fi),
4258
- steerAckDir: steerAcksDir(asyncDir, fi),
4259
3544
  piPackageRoot: config.piPackageRoot,
4260
- piArgv1: config.piArgv1,
3545
+ childSessions,
3546
+ inheritedChildRuntime: config.inheritedChildRuntime,
4261
3547
  childIntercomTarget: config.childIntercomTargets?.[fi],
4262
3548
  orchestratorIntercomTarget: config.controlIntercomTarget,
4263
3549
  nestedRoute: config.nestedRoute,
@@ -4266,6 +3552,7 @@ async function runSubagent(
4266
3552
  registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
4267
3553
  registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
4268
3554
  registerStop: (stop) => registerStepStop(fi, stop),
3555
+ registerSteer: (steer) => registerStepSteer(fi, steer),
4269
3556
  timeoutSignal: timeoutAbortController.signal,
4270
3557
  stopSignal: stopAbortController.signal,
4271
3558
  trackedMutationEvidenceForCompletionGuard: false,
@@ -4274,7 +3561,6 @@ async function runSubagent(
4274
3561
  toolTimeoutMs: task.toolTimeoutMs ?? config.toolTimeoutMs,
4275
3562
  onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking, attempt.contextLimit),
4276
3563
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
4277
- onWriterProcess,
4278
3564
  onExternalProcess: (process) => updateExternalProcess(fi, process),
4279
3565
  onExternalJob: (externalJob) => updateExternalJob(fi, externalJob),
4280
3566
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(fi),
@@ -4355,7 +3641,6 @@ async function runSubagent(
4355
3641
  output: pr.output,
4356
3642
  outputState: pr.outputState,
4357
3643
  error: pr.error,
4358
- protocolError: pr.protocolError,
4359
3644
  success: pr.stopped !== true && pr.interrupted !== true && pr.exitCode === 0,
4360
3645
  exitCode: pr.interrupted === true ? 0 : pr.exitCode,
4361
3646
  skipped: pr.skipped,
@@ -4515,6 +3800,7 @@ async function runSubagent(
4515
3800
  labels: group.parallel.map((task) => task.lane?.key ?? config.workflowKey ?? task.outputName ?? task.label),
4516
3801
  tasks: group.parallel.map((task) => task.task),
4517
3802
  provider: config.worktreeProvider,
3803
+ baseRef: config.baseRef,
4518
3804
  branchPrefix: config.worktreeBranchPrefix,
4519
3805
  setupHook: config.worktreeSetupHook
4520
3806
  ? omitUndefinedProperties({ hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs })
@@ -4652,11 +3938,9 @@ async function runSubagent(
4652
3938
  artifactsDir, artifactConfig, id,
4653
3939
  flatIndex: fi, flatStepCount: Math.max(statusPayload.steps.length, 1),
4654
3940
  outputFile: path.join(asyncDir, `output-${fi}.log`),
4655
- steerInboxDir: stepSteerInboxDir(asyncDir, fi),
4656
- steerCapabilityPath: steerCapabilityPath(asyncDir, fi),
4657
- steerAckDir: steerAcksDir(asyncDir, fi),
4658
3941
  piPackageRoot: config.piPackageRoot,
4659
- piArgv1: config.piArgv1,
3942
+ childSessions,
3943
+ inheritedChildRuntime: config.inheritedChildRuntime,
4660
3944
  childIntercomTarget: config.childIntercomTargets?.[fi],
4661
3945
  orchestratorIntercomTarget: config.controlIntercomTarget,
4662
3946
  nestedRoute: config.nestedRoute,
@@ -4665,6 +3949,7 @@ async function runSubagent(
4665
3949
  registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
4666
3950
  registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
4667
3951
  registerStop: (stop) => registerStepStop(fi, stop),
3952
+ registerSteer: (steer) => registerStepSteer(fi, steer),
4668
3953
  timeoutSignal: timeoutAbortController.signal,
4669
3954
  stopSignal: stopAbortController.signal,
4670
3955
  trackedMutationEvidenceForCompletionGuard: Boolean(worktreeSetup),
@@ -4673,7 +3958,6 @@ async function runSubagent(
4673
3958
  toolTimeoutMs: taskForRun.toolTimeoutMs ?? config.toolTimeoutMs,
4674
3959
  onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking, attempt.contextLimit),
4675
3960
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
4676
- onWriterProcess,
4677
3961
  onExternalProcess: (process) => updateExternalProcess(fi, process),
4678
3962
  onExternalJob: (externalJob) => updateExternalJob(fi, externalJob),
4679
3963
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(fi),
@@ -4801,7 +4085,6 @@ async function runSubagent(
4801
4085
  output: pr.output,
4802
4086
  outputState: pr.outputState,
4803
4087
  error: pr.error,
4804
- protocolError: pr.protocolError,
4805
4088
  success: pr.stopped !== true && pr.interrupted !== true && pr.exitCode === 0,
4806
4089
  exitCode: pr.interrupted === true ? 0 : pr.exitCode,
4807
4090
  skipped: pr.skipped,
@@ -4952,6 +4235,7 @@ async function runSubagent(
4952
4235
  labels: [seqStep.lane?.key ?? config.workflowKey ?? seqStep.outputName ?? seqStep.label],
4953
4236
  tasks: [seqStep.task],
4954
4237
  provider: config.worktreeProvider,
4238
+ baseRef: config.baseRef,
4955
4239
  branchPrefix: config.worktreeBranchPrefix,
4956
4240
  setupHook: config.worktreeSetupHook
4957
4241
  ? omitUndefinedProperties({ hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs })
@@ -5019,11 +4303,9 @@ async function runSubagent(
5019
4303
  artifactsDir, artifactConfig, id,
5020
4304
  flatIndex, flatStepCount: Math.max(statusPayload.steps.length, 1),
5021
4305
  outputFile: path.join(asyncDir, `output-${flatIndex}.log`),
5022
- steerInboxDir: stepSteerInboxDir(asyncDir, flatIndex),
5023
- steerCapabilityPath: steerCapabilityPath(asyncDir, flatIndex),
5024
- steerAckDir: steerAcksDir(asyncDir, flatIndex),
5025
4306
  piPackageRoot: config.piPackageRoot,
5026
- piArgv1: config.piArgv1,
4307
+ childSessions,
4308
+ inheritedChildRuntime: config.inheritedChildRuntime,
5027
4309
  childIntercomTarget: config.childIntercomTargets?.[flatIndex],
5028
4310
  orchestratorIntercomTarget: config.controlIntercomTarget,
5029
4311
  nestedRoute: config.nestedRoute,
@@ -5032,6 +4314,7 @@ async function runSubagent(
5032
4314
  registerInterrupt: (interrupt) => registerStepInterrupt(flatIndex, interrupt),
5033
4315
  registerTimeout: (interrupt) => registerStepTimeout(flatIndex, interrupt),
5034
4316
  registerStop: (stop) => registerStepStop(flatIndex, stop),
4317
+ registerSteer: (steer) => registerStepSteer(flatIndex, steer),
5035
4318
  timeoutSignal: timeoutAbortController.signal,
5036
4319
  stopSignal: stopAbortController.signal,
5037
4320
  timeoutMessage,
@@ -5039,7 +4322,6 @@ async function runSubagent(
5039
4322
  toolTimeoutMs: seqStep.toolTimeoutMs ?? config.toolTimeoutMs,
5040
4323
  onAttemptStart: (attempt) => updateStepModel(flatIndex, attempt.model, attempt.thinking, attempt.contextLimit),
5041
4324
  onChildEvent: (event) => updateStepFromChildEvent(flatIndex, event),
5042
- onWriterProcess,
5043
4325
  onExternalProcess: (process) => updateExternalProcess(flatIndex, process),
5044
4326
  onExternalJob: (externalJob) => updateExternalJob(flatIndex, externalJob),
5045
4327
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(flatIndex),
@@ -5067,7 +4349,6 @@ async function runSubagent(
5067
4349
  output: stopped || childStopped ? stopMessage : timedOut ? singleResult.output || (timeoutMessage ?? "Subagent timed out.") : singleResult.output,
5068
4350
  outputState: singleResult.outputState,
5069
4351
  error: stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error,
5070
- protocolError: singleResult.protocolError,
5071
4352
  success: !stopped && !childStopped && !timedOut && singleResult.interrupted !== true && singleResult.exitCode === 0,
5072
4353
  exitCode: stopped || childStopped ? 1 : timedOut ? 1 : singleResult.interrupted === true ? 0 : singleResult.exitCode,
5073
4354
  sessionFile: singleResult.sessionFile,
@@ -5335,7 +4616,7 @@ async function runSubagent(
5335
4616
  clearTimeout(timeoutTimer);
5336
4617
  timeoutTimer = undefined;
5337
4618
  }
5338
- if (!timedOut && !stopped && !interrupted && config.timeoutMs !== undefined && results.some((result) => result.timedOut === true && result.error === timeoutMessage)) {
4619
+ if (!timedOut && !stopped && !interrupted && config.timeoutMs !== undefined && timeoutMessage !== undefined && results.some((result) => result.timedOut === true && result.error?.startsWith(timeoutMessage))) {
5339
4620
  timedOut = true;
5340
4621
  }
5341
4622
  const signalTerminated = !stopped && !timedOut && !interrupted && results.some((result) => result.exitCode !== 0 && isUnexplainedProcessSignal(omitUndefinedProperties({
@@ -5425,7 +4706,6 @@ async function runSubagent(
5425
4706
  output: r.output,
5426
4707
  outputState: r.outputState,
5427
4708
  error: r.error,
5428
- protocolError: r.protocolError,
5429
4709
  success: r.success,
5430
4710
  skipped: r.skipped || undefined,
5431
4711
  interrupted: r.interrupted || undefined,
@@ -5507,7 +4787,7 @@ async function runSubagent(
5507
4787
  statusPayload.lastUpdate = Date.now();
5508
4788
  }
5509
4789
  writeStatusPayload();
5510
- orcaProgressTab?.finish(statusPayload.state === "complete" ? "completed" : statusPayload.state === "stopped" ? "stopped" : "failed", effectiveSessionFile);
4790
+ await orcaProgressTab?.finish(statusPayload.state === "complete" ? "completed" : statusPayload.state === "stopped" ? "stopped" : "failed", effectiveSessionFile);
5511
4791
  appendJsonl(
5512
4792
  eventsPath,
5513
4793
  JSON.stringify({
@@ -5542,14 +4822,21 @@ async function runSubagent(
5542
4822
  }), (filePath, content) => runPersistence.write(filePath, { content }, (_path, payload) => {
5543
4823
  fs.writeFileSync(_path, (payload as { content: string }).content, "utf-8");
5544
4824
  }));
5545
- if (runPersistence.pendingCount() === 0) runPersistence.dispose();
5546
- if (indexPersistence.pendingCount() === 0) indexPersistence.dispose();
4825
+ // The run is committed: release the child sessions first, then wait for the
4826
+ // storage-capacity retries that may still hold the final status or result.
4827
+ await childSessions.dispose().catch((error: unknown) => console.error("Failed to dispose runner child sessions:", error));
4828
+ while (runPersistence.pendingCount() + indexPersistence.pendingCount() > 0) {
4829
+ await new Promise((resolve) => setTimeout(resolve, 200));
4830
+ }
4831
+ runPersistence.dispose();
4832
+ indexPersistence.dispose();
5547
4833
  if (config.runnerProcessInstanceId) {
4834
+ // Children run inside this process, so no step has writer processes to prove terminal.
5548
4835
  const writers: Record<string, PiWriterProcessInstanceExitV1[]> = {};
5549
4836
  const expectedWriters: Record<string, number> = {};
5550
- for (const [index, result] of results.entries()) {
5551
- writers[String(index)] = result.writerProcesses ?? [];
5552
- expectedWriters[String(index)] = result.writerAttemptCount ?? 0;
4837
+ for (const index of results.keys()) {
4838
+ writers[String(index)] = [];
4839
+ expectedWriters[String(index)] = 0;
5553
4840
  }
5554
4841
  const candidate: ProcessTerminalCandidate = {
5555
4842
  version: 1,
@@ -5631,7 +4918,16 @@ async function runConfiguredSubagent(config: SubagentRunConfig): Promise<void> {
5631
4918
  }
5632
4919
  }
5633
4920
  }
5634
- await runSubagent(config, lease ? (writer) => lease!.updateWriter(writer) : undefined);
4921
+ const childSessions = await loadRunnerChildSessionFactory(config);
4922
+ try {
4923
+ await runSubagent(config, childSessions);
4924
+ } finally {
4925
+ try {
4926
+ await childSessions.dispose();
4927
+ } catch (error) {
4928
+ console.error("Failed to dispose runner child sessions:", error);
4929
+ }
4930
+ }
5635
4931
  } catch (error) {
5636
4932
  if (!startupCommitted) {
5637
4933
  try {
@@ -5660,10 +4956,16 @@ async function runConfiguredSubagent(config: SubagentRunConfig): Promise<void> {
5660
4956
  }
5661
4957
 
5662
4958
  function startConfiguredSubagent(config: SubagentRunConfig): void {
5663
- runConfiguredSubagent(config).catch((runErr) => {
5664
- console.error("Subagent runner error:", runErr);
5665
- process.exit(1);
5666
- });
4959
+ // Child sessions and the extensions loaded into them may leave handles
4960
+ // behind even after shutdown; the run is fully persisted by now, so exit
4961
+ // explicitly instead of waiting for the event loop to drain.
4962
+ runConfiguredSubagent(config).then(
4963
+ () => process.exit(0),
4964
+ (runErr) => {
4965
+ console.error("Subagent runner error:", runErr);
4966
+ process.exit(1);
4967
+ },
4968
+ );
5667
4969
  }
5668
4970
 
5669
4971
  const configArg = process.argv[2];