pi-subagents 0.64.0 → 0.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +33 -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 +18 -16
  6. package/docs/configuration.md +7 -15
  7. package/docs/extension-api.md +15 -6
  8. package/docs/missions.md +2 -0
  9. package/docs/observability.md +10 -12
  10. package/docs/tool-reference.md +7 -6
  11. package/docs/watchdog.md +1 -1
  12. package/docs/workflows.md +5 -3
  13. package/package.json +3 -4
  14. package/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
  15. package/skills/pi-subagents/references/execution-controls.md +2 -2
  16. package/src/agents/agent-management.ts +47 -15
  17. package/src/api/capability-ceiling.ts +0 -1
  18. package/src/api/{pi-args.ts → child-tool-plan.ts} +1 -1
  19. package/src/api/preflight.ts +2 -3
  20. package/src/extension/doctor.ts +2 -10
  21. package/src/extension/fanout-child.ts +9 -11
  22. package/src/extension/index.ts +27 -5
  23. package/src/extension/public-execution.ts +14 -0
  24. package/src/extension/rpc.ts +3 -2
  25. package/src/extension/schemas.ts +2 -1
  26. package/src/extension/tool-description.ts +9 -8
  27. package/src/intercom/native-supervisor-channel.ts +138 -60
  28. package/src/intercom/supervisor-ui.ts +243 -0
  29. package/src/runs/background/async-execution.ts +43 -24
  30. package/src/runs/background/async-job-tracker.ts +11 -0
  31. package/src/runs/background/async-resume.ts +11 -2
  32. package/src/runs/background/control-channel.ts +2 -204
  33. package/src/runs/background/process-terminal.ts +1 -1
  34. package/src/runs/background/run-child-session.ts +613 -0
  35. package/src/runs/background/run-status.ts +0 -1
  36. package/src/runs/background/runner-aliases.ts +125 -0
  37. package/src/runs/background/runner-child-sessions.ts +31 -0
  38. package/src/runs/background/scheduled-runs.ts +18 -4
  39. package/src/runs/background/subagent-runner.ts +184 -901
  40. package/src/runs/foreground/async-steering-action.ts +1 -17
  41. package/src/runs/foreground/execution.ts +189 -377
  42. package/src/runs/foreground/foreground-control.ts +4 -0
  43. package/src/runs/foreground/subagent-executor.ts +100 -57
  44. package/src/runs/foreground/workflow-foreground-steering.ts +24 -98
  45. package/src/runs/shared/abort-recovery.ts +3 -3
  46. package/src/runs/shared/capability-ceiling.ts +1 -2
  47. package/src/runs/shared/child-hooks.ts +25 -0
  48. package/src/runs/shared/child-identity.ts +13 -2
  49. package/src/runs/shared/child-launch.ts +314 -0
  50. package/src/runs/shared/child-lifecycle.ts +25 -0
  51. package/src/runs/shared/child-runtime-config.ts +126 -0
  52. package/src/runs/shared/child-session.ts +342 -0
  53. package/src/runs/shared/child-tool-plan.ts +530 -0
  54. package/src/runs/shared/claude-code-adapter.ts +5 -1
  55. package/src/runs/shared/completion-guard.ts +1 -1
  56. package/src/runs/shared/external-cli-preflight.ts +16 -0
  57. package/src/runs/shared/mcp-direct-tool-allowlist.ts +5 -4
  58. package/src/runs/shared/model-exclusions.ts +82 -14
  59. package/src/runs/shared/model-fallback.ts +47 -4
  60. package/src/runs/shared/nested-events.ts +29 -45
  61. package/src/runs/shared/nested-path.ts +0 -14
  62. package/src/runs/shared/orca-progress-tabs.ts +12 -7
  63. package/src/runs/shared/parallel-utils.ts +0 -2
  64. package/src/runs/shared/permissions.ts +0 -13
  65. package/src/runs/shared/process-signal.ts +4 -1
  66. package/src/runs/shared/run-fanout-budget.ts +0 -13
  67. package/src/runs/shared/runtime-acknowledged-extensions.ts +0 -27
  68. package/src/runs/shared/structured-output.ts +17 -4
  69. package/src/runs/shared/subagent-control.ts +2 -0
  70. package/src/runs/shared/subagent-prompt-runtime.ts +87 -384
  71. package/src/runs/shared/tool-availability.ts +18 -62
  72. package/src/runs/shared/tool-budget.ts +0 -14
  73. package/src/runs/shared/worktree-cleanup-plan.ts +25 -6
  74. package/src/runs/shared/worktree.ts +117 -30
  75. package/src/shared/child-session-name.ts +1 -1
  76. package/src/shared/jsonl-writer.ts +11 -0
  77. package/src/shared/thinking-ceiling.ts +0 -6
  78. package/src/shared/types.ts +55 -28
  79. package/src/shared/utils.ts +3 -4
  80. package/src/slash/slash-commands.ts +0 -6
  81. package/src/tui/fleet.ts +0 -1
  82. package/src/tui/render.ts +233 -31
  83. package/src/watchdog/child-status.ts +0 -1
  84. package/src/watchdog/register-child.ts +12 -12
  85. package/src/workflows/scripted-workflow.ts +48 -1
  86. package/src/runs/shared/child-protocol.ts +0 -415
  87. package/src/runs/shared/pi-args.ts +0 -1059
  88. package/src/runs/shared/subagent-startup-retry.ts +0 -116
  89. package/src/shared/post-exit-stdio-guard.ts +0 -85
@@ -1,5 +1,4 @@
1
- import { spawn, spawnSync } from "node:child_process";
2
- import { randomUUID } from "node:crypto";
1
+ import { spawnSync } from "node:child_process";
3
2
  import * as fs from "node:fs";
4
3
  import * as path from "node:path";
5
4
  import { pathToFileURL } from "node:url";
@@ -11,9 +10,9 @@ import { createCapacityResilientJsonWriter } from "../../shared/capacity-resilie
11
10
  import { isStorageCapacityError } from "../../shared/file-system-retry.ts";
12
11
  import { updateActiveRunIndex } from "./active-run-index.ts";
13
12
  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";
13
+ import { closeSteerInbox, consumeInterruptRequest, consumeSteerRequests, deliverInterruptRequest, deliverStopRequest, deliverTimeoutRequest, watchAsyncControlInbox, type SteerRequest, type StopRequest } from "./control-channel.ts";
15
14
  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";
15
+ import { PI_CODING_AGENT_PACKAGE, resolveInstalledPiPackageRoot } from "../shared/pi-spawn.ts";
17
16
  import { preflightLaunchCwd } from "../shared/launch-cwd.ts";
18
17
  import { captureSingleOutputSnapshot, extractChildWrittenOutput, finalizeSingleOutput, formatSavedOutputReference, injectOutputPathSystemPrompt, injectSingleOutputInstruction, resolveSingleOutput, type SingleOutputSnapshot } from "../shared/single-output.ts";
19
18
  import {
@@ -32,7 +31,6 @@ import {
32
31
  type RuntimeAcknowledgedChildExtensionsV1,
33
32
  type ModelAttempt,
34
33
  type PiWriterProcessInstanceExitV1,
35
- type ProcessTreeTerminalV1,
36
34
  type NestedRouteInfo,
37
35
  type NestedRunSummary,
38
36
  type ResolvedControlConfig,
@@ -52,7 +50,6 @@ import {
52
50
  type MaxOutputConfig,
53
51
  SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
54
52
  truncateOutput,
55
- getSubagentDepthEnv,
56
53
  } from "../../shared/types.ts";
57
54
  import {
58
55
  DEFAULT_CONTROL_CONFIG,
@@ -75,31 +72,25 @@ import {
75
72
  DEFAULT_GLOBAL_CONCURRENCY_LIMIT,
76
73
  Semaphore,
77
74
  } from "../shared/parallel-utils.ts";
78
- import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, deriveForkPromptCacheKey, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan, type SubagentTaskDelivery } from "../shared/pi-args.ts";
75
+ import { applyThinkingSuffix, deriveForkPromptCacheKey, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/child-tool-plan.ts";
76
+ import { buildInProcessChildLaunch, type InheritedChildRuntime } from "../shared/child-launch.ts";
77
+ import type { ChildSessionFactory } from "../shared/child-session.ts";
78
+ import { runChildSession, type ChildEvent, type RunChildSessionResult, type StepSteerHandler } from "./run-child-session.ts";
79
+ import { loadRunnerChildSessionFactory } from "./runner-child-sessions.ts";
80
+ import { SUBAGENT_CHILD_ENV } from "../shared/child-runtime-config.ts";
79
81
  import { deriveChildSessionName } from "../../shared/child-session-name.ts";
80
82
  import { alignForkedSessionCwd } from "../../shared/fork-session-cwd.ts";
81
- import { readRuntimeAcknowledgedExtensions } from "../shared/runtime-acknowledged-extensions.ts";
82
83
  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";
84
+ import { clearStructuredOutputCaptures, createStructuredOutputFileCapture, createStructuredOutputRuntime, MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput, readStructuredOutputAcceptanceReport } from "../shared/structured-output.ts";
85
+ import { formatMidToolExitError, isOrdinaryToolForMidToolExit, isUnexplainedProcessSignal } from "../shared/process-signal.ts";
86
+ import { formatChildToolDiagnostic } from "../shared/tool-availability.ts";
86
87
  import { buildTimeoutRecoverySummary, collectTrackedMutationEvidence, snapshotTrackedMutations } from "../shared/mutation-evidence.ts";
87
88
  import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection } from "../shared/dynamic-fanout.ts";
88
89
  import { claimRunFanoutBatch, getRunFanoutBudgetSnapshot } from "../shared/run-fanout-budget.ts";
89
90
  import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDir, writeNestedEvent } from "../shared/nested-events.ts";
90
91
  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
92
  import { markProcessTerminalCandidateLeaseRelease, writeProcessTerminalCandidate, type ProcessTerminalCandidate } from "./process-terminal.ts";
100
- import { createOwnedProcessTreeController, type OwnedProcessTreeController } from "./owned-process-tree.ts";
101
93
  import { createSteeringStatus, recordSteeringRequest, steeringStatus, terminalSteeringNoticeState, updateSteeringTarget } from "./steering.ts";
102
- import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
103
94
  import { PROMPT_REDACTED, detectSubagentError, extractTextFromContent, extractToolArgsPreview, formatEmptyTerminalAssistantResponseError, getFinalOutput, hasEmptyTerminalAssistantResponse, readStatus } from "../../shared/utils.ts";
104
95
  import { evaluateCompletionMutationGuard, expectsImplementationMutation, hasMutationToolCapability, validateImplementationToolContract } from "../shared/completion-guard.ts";
105
96
  import { planCompletionEvidence, projectSettlementDiagnostic } from "../shared/completion-evidence.ts";
@@ -128,7 +119,7 @@ import {
128
119
  type WorktreeSetup,
129
120
  } from "../shared/worktree.ts";
130
121
  import { findModelInfo, resolveEffectiveThinking } from "../../shared/model-info.ts";
131
- import { assertThinkingWithinCeiling, decodeThinkingCeiling, SUBAGENT_THINKING_CEILING_ENV } from "../../shared/thinking-ceiling.ts";
122
+ import { assertThinkingWithinCeiling } from "../../shared/thinking-ceiling.ts";
132
123
  import { launchBindingDigest } from "../../shared/launch-contract.ts";
133
124
  import { writeInitialProgressFile } from "../../shared/settings.ts";
134
125
  import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
@@ -143,7 +134,6 @@ import { effectiveToolTimeoutMs, formatToolTimeoutMessage, toolTimeoutCallKey }
143
134
  import { usageBudgetExceededMessage, usageBudgetState } from "../shared/usage-budget.ts";
144
135
  import { formatParallelHandoffError, formatParallelHandoffReference, parallelHandoffPath, writeParallelHandoffGroup, writePendingParallelHandoff } from "../shared/parallel-handoff.ts";
145
136
  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
137
  import { acquireSessionLease, type SessionLeaseRequest } from "../shared/session-lease.ts";
148
138
  import { buildExternalCliPrompt, runExternalCli } from "../shared/external-cli-runner.ts";
149
139
  import { resolveClaudeCodeLaunch } from "../shared/claude-code-adapter.ts";
@@ -152,20 +142,22 @@ import { resolveCursorAgentLaunch } from "../shared/cursor-agent-adapter.ts";
152
142
  import { resolveExternalCliRunnerStatus } from "../shared/external-cli-contract.ts";
153
143
  import { runExternalJob } from "../shared/external-job-runner.ts";
154
144
  import { createOrcaProgressTab, type OrcaProgressTab } from "../shared/orca-progress-tabs.ts";
155
- import { decodeSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
145
+ import type { ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
156
146
  import {
157
- CHILD_WATCHDOG_CONFIG_ENV,
158
147
  acceptChildWatchdogEvent,
159
148
  applyChildWatchdogMessage,
160
- childWatchdogIsActive,
161
- decodeChildWatchdogConfig,
162
149
  isChildWatchdogStatusEvent,
163
150
  resolveChildWatchdogConfig,
164
- type ChildWatchdogStateSnapshot,
151
+ type ChildWatchdogStatusEvent,
165
152
  } from "../../watchdog/child-status.ts";
166
153
 
167
154
  const INTERCOM_DETACH_RECEIPT = "Detached for intercom coordination before task completion.";
168
155
 
156
+ // This process hosts child sessions. An ambient copy of pi-subagents loaded
157
+ // into one of them must register nothing; the variable marks the process as a
158
+ // child host.
159
+ process.env[SUBAGENT_CHILD_ENV] = "1";
160
+
169
161
  interface SubagentRunConfig {
170
162
  id: string;
171
163
  steps: RunnerStep[];
@@ -183,10 +175,14 @@ interface SubagentRunConfig {
183
175
  sessionId?: string | null;
184
176
  completionOwnerId?: string;
185
177
  piPackageRoot?: string;
186
- piArgv1?: string;
178
+ /** Test seam: module the runner imports its `ChildSessionFactory` from. */
179
+ childSessionFactoryModule?: string;
180
+ /** The launching executor's own child runtime when it was itself an in-process child. */
181
+ inheritedChildRuntime?: InheritedChildRuntime;
187
182
  worktreeSetupHook?: string;
188
183
  worktreeSetupHookTimeoutMs?: number;
189
184
  worktreeBaseDir?: string;
185
+ baseRef?: string;
190
186
  worktreeProvider?: import("../../shared/types.ts").WorktreeProvider;
191
187
  worktreeBranchPrefix?: string;
192
188
  controlConfig?: ResolvedControlConfig;
@@ -232,7 +228,6 @@ interface StepResult {
232
228
  output: string;
233
229
  outputState?: SubagentOutputState;
234
230
  error?: string;
235
- protocolError?: ProtocolOutputLimit;
236
231
  success?: boolean;
237
232
  exitCode: number | null;
238
233
  usage?: Usage;
@@ -271,8 +266,6 @@ interface StepResult {
271
266
  structuredOutputSchemaPath?: string;
272
267
  acceptance?: import("../../shared/types.ts").AcceptanceLedger;
273
268
  watchdog?: import("../../shared/types.ts").ChildWatchdogProgress;
274
- writerProcesses?: PiWriterProcessInstanceExitV1[];
275
- writerAttemptCount?: number;
276
269
  runner?: ExternalCliRunnerStatus | ExternalJobRunnerStatus;
277
270
  externalProcess?: ExternalProcessStatus;
278
271
  externalJob?: ExternalJobStatus;
@@ -373,10 +366,6 @@ function appendDiagnosticJsonl(filePath: string, line: string, droppedEventType?
373
366
  state.diagnosticsTruncated = true;
374
367
  }
375
368
 
376
- function shouldPersistChildEvent(event: Record<string, unknown>): boolean {
377
- return event.type !== "message_update";
378
- }
379
-
380
369
  function isBlockingSupervisorTool(toolName: string | undefined, args: unknown): boolean {
381
370
  if (!args || typeof args !== "object" || Array.isArray(args)) return false;
382
371
  if (toolName === "contact_supervisor") {
@@ -459,15 +448,6 @@ function appendRecentStepOutput(step: RunnerStatusStep, lines: string[]): void {
459
448
  }
460
449
  }
461
450
 
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
451
  type UndefinedOmitted<T extends object> = {
472
452
  [K in keyof T]: Exclude<T[K], undefined>;
473
453
  };
@@ -510,73 +490,6 @@ function resetStepLiveDetail(step: RunnerStatusStep): void {
510
490
  step.recentOutput = [];
511
491
  }
512
492
 
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
493
  const MAX_CHILD_FAILURE_DIAGNOSTIC_CHARS = 8_192;
581
494
 
582
495
  function formatRequiredOutputError(requiredOutput: {
@@ -611,568 +524,6 @@ function formatChildFailureDiagnostic(input: {
611
524
  return `${baseError.slice(0, Math.max(0, errorLimit))}${context ? `\n${context}` : ""}`;
612
525
  }
613
526
 
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
527
  function resolvePiPackageRootFallback(): string {
1177
528
  const root = resolveInstalledPiPackageRoot();
1178
529
  if (root) return root;
@@ -1294,15 +645,17 @@ interface SingleStepContext {
1294
645
  flatIndex: number;
1295
646
  flatStepCount: number;
1296
647
  outputFile: string;
1297
- steerInboxDir?: string;
1298
- steerCapabilityPath?: string;
1299
- steerAckDir?: string;
1300
648
  transcriptPath?: string;
1301
649
  piPackageRoot?: string;
1302
- piArgv1?: string;
650
+ /** Factory the runner creates this step's child session through. */
651
+ childSessions: ChildSessionFactory;
652
+ /** The launching executor's own child runtime; nested route, depth, and ceilings come from here. */
653
+ inheritedChildRuntime?: InheritedChildRuntime;
1303
654
  registerInterrupt?: (interrupt: (() => void) | undefined) => void;
1304
655
  registerTimeout?: (interrupt: (() => void) | undefined) => void;
1305
656
  registerStop?: (stop: (() => void) | undefined) => void;
657
+ /** Receives the live child's steer handler while its session runs. */
658
+ registerSteer?: (steer: StepSteerHandler | undefined) => void;
1306
659
  timeoutSignal?: AbortSignal;
1307
660
  stopSignal?: AbortSignal;
1308
661
  timeoutMessage?: string;
@@ -1318,7 +671,6 @@ interface SingleStepContext {
1318
671
  runFanoutBudget?: RunFanoutBudgetDescriptor;
1319
672
  onAttemptStart?: (attempt: { model?: string; thinking?: string; contextLimit?: number }) => void;
1320
673
  onChildEvent?: (event: ChildEvent) => void;
1321
- onWriterProcess?: (writer: { state: "none" | "spawning" } | { state: "running"; pid: number }) => void;
1322
674
  onExternalProcess?: (process: ExternalProcessStatus) => void;
1323
675
  onExternalJob?: (status: ExternalJobStatus) => void;
1324
676
  skipAcceptance?: () => boolean;
@@ -1424,13 +776,11 @@ async function runSingleStepInner(
1424
776
  model: step.model,
1425
777
  modelCandidates: step.modelCandidates,
1426
778
  mcpDirectTools: step.mcpDirectTools,
1427
- mcpConfig: step.mcpConfig,
1428
- runtimeServerNames: step.runtimeServerNames,
1429
779
  cwd: step.cwd ?? ctx.cwd,
1430
780
  requireReadTool: Boolean(step.skills?.length),
1431
781
  structuredOutput: Boolean(effectiveStructuredOutput),
1432
782
  capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
1433
- inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
783
+ inheritedCapabilityCeiling: ctx.inheritedChildRuntime?.capabilityCeiling,
1434
784
  permissionRules: step.permissionRules,
1435
785
  }));
1436
786
  const contractTools = resolvedTaskToolPlan.explicitToolAllowlist ? resolvedTaskToolPlan.effectiveToolAllowlist : undefined;
@@ -1650,12 +1000,10 @@ async function runSingleStepInner(
1650
1000
  let capabilityAudit: import("../shared/capability-ceiling.ts").SubagentCapabilityAudit | undefined;
1651
1001
  let launchResolvedExtensions = step.launchResolvedExtensions;
1652
1002
  const modelAttempts: ModelAttempt[] = [];
1653
- const writerProcesses: PiWriterProcessInstanceExitV1[] = [];
1654
- let writerAttemptCount = 0;
1655
1003
  const attemptNotes: string[] = [];
1656
1004
  let finalRequiredOutputMissing: boolean | undefined;
1657
1005
  const eventsPath = path.join(path.dirname(ctx.outputFile), "events.jsonl");
1658
- let finalResult: RunPiStreamingResult | undefined;
1006
+ let finalResult: RunChildSessionResult | undefined;
1659
1007
  let finalOutputSnapshot: SingleOutputSnapshot | undefined;
1660
1008
  let structuredAcceptanceReport: unknown;
1661
1009
  let structuredAcceptanceReportError: string | undefined;
@@ -1667,10 +1015,6 @@ async function runSingleStepInner(
1667
1015
  let finalMutationEvidence = collectTrackedMutationEvidence(mutationSnapshot, step.cwd ?? ctx.cwd);
1668
1016
 
1669
1017
  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
1018
  let contextOverflow = false;
1675
1019
  let launchWarningsEmitted = false;
1676
1020
  let abortRecoveryAttempted = false;
@@ -1682,7 +1026,7 @@ async function runSingleStepInner(
1682
1026
  const candidate = candidates[modelIndex];
1683
1027
  const expectedModelForVerification = candidate && !(step.skipPrimaryModelVerification && modelIndex === 0) ? candidate : undefined;
1684
1028
  try {
1685
- assertThinkingWithinCeiling({ model: candidate, configThinking: step.thinking, ceiling: step.thinkingCeiling ?? decodeThinkingCeiling(process.env[SUBAGENT_THINKING_CEILING_ENV]), agent: step.agent, runId: ctx.id });
1029
+ assertThinkingWithinCeiling({ model: candidate, configThinking: step.thinking, ceiling: step.thinkingCeiling, agent: step.agent, runId: ctx.id });
1686
1030
  } catch (error) {
1687
1031
  const message = error instanceof Error ? error.message : String(error);
1688
1032
  return omitUndefinedProperties({ agent: step.agent, output: message, error: message, exitCode: 1, context: step.context, thinkingCeiling: step.thinkingCeiling });
@@ -1709,12 +1053,10 @@ async function runSingleStepInner(
1709
1053
  childIndex: ctx.flatIndex,
1710
1054
  })
1711
1055
  : undefined;
1712
- const { args, env, tempDir, toolDiagnosticPath, runtimeAcknowledgedExtensionsPath, capabilityAudit: attemptCapabilityAudit, warnings } = buildPiArgs(omitUndefinedProperties({
1056
+ let watchdogSink: ((event: ChildWatchdogStatusEvent) => void) | undefined;
1057
+ const launch = buildInProcessChildLaunch(omitUndefinedProperties({
1713
1058
  parentSessionId: step.parentSessionId,
1714
1059
  forkCacheKey: step.context === "fork" ? deriveForkPromptCacheKey(step.parentSessionId) : undefined,
1715
- baseArgs: ["--mode", "json", "-p"],
1716
- task: attemptTask,
1717
- taskDelivery: taskDeliveryOverride,
1718
1060
  sessionEnabled,
1719
1061
  sessionDir,
1720
1062
  sessionFile: step.sessionFile,
@@ -1733,28 +1075,20 @@ async function runSingleStepInner(
1733
1075
  systemPrompt: step.systemPrompt ?? "",
1734
1076
  systemPromptMode: step.systemPromptMode,
1735
1077
  mcpDirectTools: step.mcpDirectTools,
1736
- mcpConfig: step.mcpConfig,
1737
- runtimeServerNames: step.runtimeServerNames,
1078
+ extensionBindings,
1738
1079
  capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
1739
1080
  cwd: step.cwd ?? ctx.cwd,
1740
- promptFileStem: step.agent,
1741
1081
  intercomSessionName: ctx.childIntercomTarget,
1742
1082
  sessionName: childSessionName,
1743
1083
  orchestratorIntercomTarget: ctx.orchestratorIntercomTarget,
1744
1084
  runId: ctx.id,
1745
1085
  childAgentName: step.agent,
1746
1086
  childIndex: ctx.flatIndex,
1747
- parentEventSink: ctx.nestedRoute?.eventSink,
1748
- parentControlInbox: ctx.nestedRoute?.controlInbox,
1749
- parentRootRunId: ctx.nestedRoute?.rootRunId,
1750
- parentCapabilityToken: ctx.nestedRoute?.capabilityToken,
1087
+ nestedRoute: ctx.nestedRoute,
1751
1088
  runFanoutBudget: ctx.runFanoutBudget ? {
1752
1089
  ...ctx.runFanoutBudget,
1753
1090
  ...(step.runFanoutPath ? { parentPath: `${ctx.runFanoutBudget.parentPath ? `${ctx.runFanoutBudget.parentPath}/` : ""}${step.runFanoutPath}` } : {}),
1754
1091
  } : undefined,
1755
- steerInboxDir: ctx.steerInboxDir,
1756
- steerCapabilityPath: ctx.steerCapabilityPath,
1757
- steerAckDir: ctx.steerAckDir,
1758
1092
  structuredOutput: effectiveStructuredOutput,
1759
1093
  toolBudget: step.toolBudget,
1760
1094
  permissionRules: step.permissionRules,
@@ -1762,11 +1096,19 @@ async function runSingleStepInner(
1762
1096
  ? path.join(ctx.artifactsDir, "permission-audit", `${ctx.id}-${ctx.flatIndex}.jsonl`)
1763
1097
  : undefined,
1764
1098
  childWatchdog,
1099
+ watchdogStatus: (event) => watchdogSink?.(event),
1765
1100
  waitToolEnabled: step.waitToolEnabled,
1766
1101
  waitToolDefaultTimeoutMs: step.waitToolDefaultTimeoutMs,
1767
1102
  thinkingCeiling: step.thinkingCeiling,
1768
- extensionBindings,
1103
+ maxSubagentDepth: step.maxSubagentDepth,
1104
+ inherited: ctx.inheritedChildRuntime,
1105
+ host: "runner",
1769
1106
  }));
1107
+ if (effectiveStructuredOutput && launch.config.structuredOutput) {
1108
+ // The runner reads the value back from the runtime's files after the run.
1109
+ launch.config.structuredOutput.capture = createStructuredOutputFileCapture(effectiveStructuredOutput);
1110
+ }
1111
+ const { warnings, capabilityAudit: attemptCapabilityAudit } = launch;
1770
1112
  if (!launchWarningsEmitted && warnings.length > 0) {
1771
1113
  for (const warning of warnings) console.warn(`[pi-subagents] ${warning}`);
1772
1114
  launchWarningsEmitted = true;
@@ -1782,13 +1124,11 @@ async function runSingleStepInner(
1782
1124
  model: step.model,
1783
1125
  modelCandidates: step.modelCandidates,
1784
1126
  mcpDirectTools: step.mcpDirectTools,
1785
- mcpConfig: step.mcpConfig,
1786
- runtimeServerNames: step.runtimeServerNames,
1787
1127
  cwd: step.cwd ?? ctx.cwd,
1788
1128
  requireReadTool: Boolean(step.skills?.length),
1789
1129
  structuredOutput: Boolean(effectiveStructuredOutput),
1790
1130
  capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
1791
- inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
1131
+ inheritedCapabilityCeiling: ctx.inheritedChildRuntime?.capabilityCeiling,
1792
1132
  permissionRules: step.permissionRules,
1793
1133
  }));
1794
1134
  launchResolvedExtensions = projectLaunchResolvedChildExtensions(toolPlan);
@@ -1817,59 +1157,49 @@ async function runSingleStepInner(
1817
1157
  }));
1818
1158
  }
1819
1159
  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,
1160
+ // Each attempt rewrites the step output log; synchronous appends keep a
1161
+ // retried attempt from interleaving with the previous attempt's flush.
1162
+ fs.writeFileSync(ctx.outputFile, "", "utf-8");
1163
+ const run = await runChildSession(omitUndefinedProperties({
1164
+ factory: ctx.childSessions,
1165
+ launch,
1166
+ prompt: `Task: ${attemptTask}`,
1167
+ childWatchdog,
1168
+ childEventContext: { runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
1169
+ appendChildEvent: (event) => appendDiagnosticJsonl(eventsPath, JSON.stringify(event), typeof event.type === "string" ? event.type : undefined),
1170
+ writeOutputLine: (line) => {
1171
+ try {
1172
+ fs.appendFileSync(ctx.outputFile, `${line}\n`, "utf-8");
1173
+ } catch {
1174
+ // The output log is observability only.
1175
+ }
1176
+ ctx.orcaProgressTab?.append(`${line}\n`);
1177
+ },
1178
+ registerInterrupt: ctx.registerInterrupt,
1179
+ registerTimeout: ctx.registerTimeout,
1180
+ registerStop: ctx.registerStop,
1181
+ registerSteer: ctx.registerSteer,
1182
+ registerWatchdogStatus: (sink) => { watchdogSink = sink; },
1183
+ timeoutMessage: ctx.timeoutMessage,
1184
+ stopMessage: ctx.stopMessage,
1185
+ onChildEvent: ctx.onChildEvent,
1832
1186
  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,
1187
+ toolTimeoutMs: ctx.toolTimeoutMs,
1188
+ runDeadlineAt: ctx.deadlineAt,
1841
1189
  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);
1190
+ modelVerificationRegistry: step.modelVerificationRegistry,
1191
+ mutationTools: step.mutationTools,
1192
+ }));
1193
+ const toolDiagnostic = run.exitCode === 0 && !run.error ? launch.capture.toolDiagnostic() : undefined;
1194
+ const toolAvailabilityError = toolDiagnostic ? formatChildToolDiagnostic(toolDiagnostic) : undefined;
1195
+ const runtimeAcknowledgedExtensions = launch.capture.runtimeAcknowledgedExtensions();
1861
1196
  const midToolExitError = run.currentTool
1862
1197
  && isOrdinaryToolForMidToolExit(run.currentTool)
1863
1198
  && !run.interrupted
1864
1199
  && !run.timedOut
1865
1200
  && !run.stopped
1866
- && !run.protocolError
1867
1201
  && !toolAvailabilityError
1868
- ? formatMidToolExitError({
1869
- toolName: run.currentTool,
1870
- exitCode: run.exitCode,
1871
- processSignal: run.processSignal,
1872
- })
1202
+ ? formatMidToolExitError({ toolName: run.currentTool })
1873
1203
  : undefined;
1874
1204
 
1875
1205
  let structuredOutput: unknown;
@@ -1956,19 +1286,10 @@ async function runSingleStepInner(
1956
1286
  : run.error && run.exitCode === 0
1957
1287
  ? 1
1958
1288
  : 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
1289
  const underlyingError = toolAvailabilityError
1966
1290
  ?? midToolExitError
1967
1291
  ?? structuredError
1968
1292
  ?? 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
1293
  ?? emptyOutputError
1973
1294
  ?? (missingRequiredOutputAfterMutation ? missingRequiredOutputError : undefined)
1974
1295
  ?? (hiddenError?.hasError
@@ -1976,13 +1297,7 @@ async function runSingleStepInner(
1976
1297
  ? `${hiddenError.errorType} failed (exit ${effectiveExitCode}): ${hiddenError.details}`
1977
1298
  : `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
1978
1299
  : undefined);
1979
- const error = formatSubagentExtensionConflictError(
1980
- underlyingError ?? missingRequiredOutputError ?? completionEvidence.legacyFailureError,
1981
- {
1982
- agent: step.agent,
1983
- ambientExtensionsEnabled: launchResolvedExtensions?.disableAmbientExtensions === false,
1984
- },
1985
- );
1300
+ const error = underlyingError ?? missingRequiredOutputError ?? completionEvidence.legacyFailureError;
1986
1301
  const attempt: ModelAttempt = omitUndefinedProperties({
1987
1302
  model: candidate ?? run.model ?? step.model ?? "default",
1988
1303
  success: effectiveExitCode === 0 && !error,
@@ -1991,7 +1306,7 @@ async function runSingleStepInner(
1991
1306
  usage: run.usage,
1992
1307
  });
1993
1308
  modelAttempts.push(attempt);
1994
- if (!recoveringAbort && candidate && startupAttemptIndex === 0) attemptedModels.push(candidate);
1309
+ if (!recoveringAbort && candidate) attemptedModels.push(candidate);
1995
1310
  completionGuardTriggeredFinal = completionEvidence.guardTriggered && !underlyingError && !missingRequiredOutputError;
1996
1311
  finalOutputSnapshot = outputSnapshot;
1997
1312
  if (step.toolBudget) {
@@ -2008,11 +1323,10 @@ async function runSingleStepInner(
2008
1323
  afterCompactionSettlement: run.afterCompactionSettlement === true,
2009
1324
  });
2010
1325
  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;
1326
+ 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
1327
  const abortRecovery = !attempt.success ? planAbortRecovery({
2013
1328
  messages: run.messages,
2014
1329
  error,
2015
- processSignal: run.processSignal,
2016
1330
  sessionAvailable: Boolean(step.sessionFile && fs.existsSync(step.sessionFile)),
2017
1331
  alreadyResumed: abortRecoveryAttempted,
2018
1332
  stopped: run.stopped || ctx.stopSignal?.aborted || ctx.skipAcceptance?.(),
@@ -2043,50 +1357,6 @@ async function runSingleStepInner(
2043
1357
  }
2044
1358
  if (completionEvidence.guardTriggered) break modelAttemptsLoop;
2045
1359
 
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
1360
  const retryableModelFailure = isRetryableModelFailureAttempt({ error, messages: run.messages, toolCount: run.toolCount });
2091
1361
  if (retryableModelFailure) recordRetryableModelFailure(candidate ?? run.model ?? step.model, error);
2092
1362
  if (isContextOverflow(error)) {
@@ -2097,7 +1367,6 @@ async function runSingleStepInner(
2097
1367
  if (!retryableModelFailure || modelIndex === candidates.length - 1) break modelAttemptsLoop;
2098
1368
  attemptNotes.push(formatModelAttemptNote(attempt, candidates[modelIndex + 1]));
2099
1369
  modelIndex += 1;
2100
- startupAttemptIndex = 0;
2101
1370
  }
2102
1371
 
2103
1372
  const rawOutput = finalResult?.finalOutput ?? "";
@@ -2125,7 +1394,7 @@ async function runSingleStepInner(
2125
1394
  ? extractChildWrittenOutput(finalResult?.messages, step.outputPath, step.cwd ?? ctx.cwd)
2126
1395
  : undefined;
2127
1396
  const outputState: SubagentOutputState = finalResult?.outputState === "present"
2128
- || (finalResult as (RunPiStreamingResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput !== undefined
1397
+ || (finalResult as (RunChildSessionResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput !== undefined
2129
1398
  || Boolean(childWrittenOutput?.trim())
2130
1399
  ? "present"
2131
1400
  : resolvedOutput.savedPath
@@ -2227,7 +1496,7 @@ async function runSingleStepInner(
2227
1496
  ...(capabilityAudit ? { capabilityCeiling: capabilityAudit.ceiling, capabilityAudit } : {}),
2228
1497
  launchContractDigest: actualLaunchContractDigest,
2229
1498
  launchResolvedExtensions,
2230
- ...((finalResult as (RunPiStreamingResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }) | undefined)?.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: (finalResult as RunPiStreamingResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }).runtimeAcknowledgedExtensions } : {}),
1499
+ ...((finalResult as (RunChildSessionResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }) | undefined)?.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: (finalResult as RunChildSessionResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }).runtimeAcknowledgedExtensions } : {}),
2231
1500
  ...(transcriptWriter ? { transcriptPath: artifactPaths.transcriptPath } : {}),
2232
1501
  transcriptError: transcriptWriter?.getError(),
2233
1502
  skills: step.skills,
@@ -2246,7 +1515,6 @@ async function runSingleStepInner(
2246
1515
  outputState,
2247
1516
  exitCode: effectiveFinalExitCode,
2248
1517
  error: effectiveFinalError,
2249
- protocolError: finalResult?.protocolError,
2250
1518
  sessionFile: step.sessionFile,
2251
1519
  intercomTarget: ctx.childIntercomTarget,
2252
1520
  model: finalResult?.model,
@@ -2264,22 +1532,19 @@ async function runSingleStepInner(
2264
1532
  interrupted: timedOutAfterAcceptance || stoppedAfterAcceptance ? false : finalResult?.interrupted,
2265
1533
  timedOut: timedOutAfterAcceptance ? true : finalResult?.timedOut,
2266
1534
  stopped: stoppedAfterAcceptance ? true : finalResult?.stopped,
2267
- processSignal: finalResult?.processSignal,
2268
1535
  timeoutRecovery,
2269
1536
  toolBudget,
2270
1537
  toolBudgetBlocked: toolBudgetBlocked || undefined,
2271
1538
  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,
1539
+ ...((finalResult as (RunChildSessionResult & { effects?: import("../../shared/types.ts").EffectsProjection }) | undefined)?.effects ? { effects: (finalResult as RunChildSessionResult & { effects?: import("../../shared/types.ts").EffectsProjection }).effects } : {}),
1540
+ structuredOutput: timedOutAfterAcceptance || stoppedAfterAcceptance ? undefined : (finalResult as (RunChildSessionResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput,
2274
1541
  structuredOutputPath: timedOutAfterAcceptance || stoppedAfterAcceptance ? undefined : effectiveStructuredOutput?.outputPath,
2275
1542
  structuredOutputSchemaPath: timedOutAfterAcceptance || stoppedAfterAcceptance ? undefined : effectiveStructuredOutput?.schemaPath,
2276
1543
  acceptance: effectiveAcceptance,
2277
1544
  watchdog: finalResult?.watchdog,
2278
1545
  ...(capabilityAudit ? { capabilityCeiling: capabilityAudit.ceiling, capabilityAudit } : {}),
2279
1546
  launchResolvedExtensions,
2280
- ...((finalResult as (RunPiStreamingResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }) | undefined)?.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: (finalResult as RunPiStreamingResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }).runtimeAcknowledgedExtensions } : {}),
2281
- writerProcesses,
2282
- writerAttemptCount,
1547
+ ...((finalResult as (RunChildSessionResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }) | undefined)?.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: (finalResult as RunChildSessionResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }).runtimeAcknowledgedExtensions } : {}),
2283
1548
  });
2284
1549
  return isAgentContractV1(step.agentContract) ? attachContractProjections(result as unknown as import("../../shared/types.ts").SingleResult) as unknown as typeof result : result;
2285
1550
  }
@@ -2573,7 +1838,7 @@ async function runSingleStepWithTimeout(
2573
1838
 
2574
1839
  async function runSubagent(
2575
1840
  config: SubagentRunConfig,
2576
- onWriterProcess?: (writer: { state: "none" | "spawning" } | { state: "running"; pid: number }) => void,
1841
+ childSessions: ChildSessionFactory,
2577
1842
  ): Promise<void> {
2578
1843
  const { id, steps, resultPath, cwd, placeholder, taskIndex, totalTasks, maxOutput, artifactsDir, artifactConfig } =
2579
1844
  config;
@@ -2593,9 +1858,11 @@ async function runSubagent(
2593
1858
  const activeChildInterrupts = new Map<number, () => void>();
2594
1859
  const activeChildTimeouts = new Map<number, () => void>();
2595
1860
  const activeChildStops = new Map<number, () => void>();
1861
+ const activeChildSteers = new Map<number, StepSteerHandler>();
1862
+ /** Steers routed to a running step before its session was created. */
1863
+ const queuedStepSteers = new Map<number, SteerRequest[]>();
2596
1864
  const childStopRequests = new Map<number, { childId: string; requestedAt: number }>();
2597
1865
  const pendingStepSteers: SteerRequest[] = [];
2598
- const steeringCapabilities = new Map<number, SteerCapability>();
2599
1866
  let interrupted = false;
2600
1867
  let currentActivityState: ActivityState | undefined;
2601
1868
  let activityTimer: NodeJS.Timeout | undefined;
@@ -3053,6 +2320,16 @@ async function runSubagent(
3053
2320
  activeChildStops.set(flatIndex, stop);
3054
2321
  if (stopped || childStopRequests.has(flatIndex)) stop();
3055
2322
  };
2323
+ const registerStepSteer = (flatIndex: number, steer: StepSteerHandler | undefined): void => {
2324
+ if (!steer) {
2325
+ activeChildSteers.delete(flatIndex);
2326
+ return;
2327
+ }
2328
+ activeChildSteers.set(flatIndex, steer);
2329
+ const queued = queuedStepSteers.get(flatIndex);
2330
+ queuedStepSteers.delete(flatIndex);
2331
+ for (const request of queued ?? []) steerLiveChild(flatIndex, request);
2332
+ };
3056
2333
  const interruptActiveChildren = (): void => {
3057
2334
  for (const interrupt of [...activeChildInterrupts.values()]) interrupt();
3058
2335
  };
@@ -3505,22 +2782,15 @@ async function runSubagent(
3505
2782
  if (!step) return { index, state: "failed" as const, reason: "child index out of range" };
3506
2783
  if (step.status === "pending") return { index, state: "scheduled" as const };
3507
2784
  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
2785
  return { index, state: "routed" as const };
3510
2786
  });
3511
2787
  recordSteeringLifecycle(request, targetStates);
3512
2788
  emitSteeringEvent("subagent.steer.requested", request, undefined, { targets: targetStates });
3513
2789
  for (const target of targetStates) {
3514
2790
  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
- }
2791
+ updateSteeringLifecycleTarget(request.id, target.index, "routed", now);
2792
+ emitSteeringEvent("subagent.steer.routed", request, target.index);
2793
+ steerLiveChild(target.index, request);
3524
2794
  } else if (target.state === "failed") {
3525
2795
  markSteeringAttention(target.index);
3526
2796
  emitSteeringEvent("subagent.steer.failed", request, target.index, { reason: target.reason });
@@ -3532,27 +2802,42 @@ async function runSubagent(
3532
2802
  statusPayload.lastUpdate = now;
3533
2803
  writeStatusPayload();
3534
2804
  };
3535
- const consumeSteerAck = (ack: SteerAck): void => {
2805
+ /** Record the outcome of handing a routed steer to the live child session. */
2806
+ const applySteerDelivery = (requestId: string, index: number, delivery: { state: "delivered" | "queued" | "failed"; message: string }): void => {
3536
2807
  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));
2808
+ const request = lifecycle.recent.find((candidate) => candidate.id === requestId);
2809
+ if (!request || !request.targets.some((target) => target.index === index)) return;
2810
+ const late = fs.existsSync(steeringMarkerPath(requestId));
3540
2811
  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 });
2812
+ if (delivery.state === "delivered") {
2813
+ updateSteeringLifecycleTarget(requestId, index, late ? "late" : "delivered", now, omitUndefinedProperties({ reason: late ? "acknowledged after recovery commit" : undefined }));
2814
+ emitSteeringEvent("subagent.steer.delivered", { type: "steer", id: requestId, ts: now, message: delivery.message }, index, { late, deliveryStatus: "delivered", message: delivery.message });
2815
+ } else if (delivery.state === "queued") {
2816
+ updateSteeringLifecycleTarget(requestId, index, "queued", now);
2817
+ emitSteeringEvent("subagent.steer.queued", { type: "steer", id: requestId, ts: now, message: delivery.message }, index, { deliveryStatus: "queued", message: delivery.message });
3547
2818
  } 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 });
2819
+ markSteeringAttention(index);
2820
+ updateSteeringLifecycleTarget(requestId, index, "failed", now, { reason: delivery.message });
2821
+ emitSteeringEvent("subagent.steer.failed", { type: "steer", id: requestId, ts: now, message: delivery.message }, index, { reason: delivery.message });
3551
2822
  }
3552
- emitTerminalSteeringNotice(ack.requestId, `Steering failed for run ${id}: ${ack.message}`);
2823
+ emitTerminalSteeringNotice(requestId, `Steering failed for run ${id}: ${delivery.message}`);
3553
2824
  statusPayload.lastUpdate = now;
3554
2825
  writeStatusPayload();
3555
2826
  };
2827
+ /** Hand a routed steer to the step's live session, or hold it until the session exists. */
2828
+ const steerLiveChild = (index: number, request: SteerRequest): void => {
2829
+ const steer = activeChildSteers.get(index);
2830
+ if (!steer) {
2831
+ const queued = queuedStepSteers.get(index) ?? [];
2832
+ queued.push(request);
2833
+ queuedStepSteers.set(index, queued);
2834
+ return;
2835
+ }
2836
+ void steer(request).then(
2837
+ (delivery) => applySteerDelivery(request.id, index, delivery),
2838
+ (error) => applySteerDelivery(request.id, index, { state: "failed", message: error instanceof Error ? error.message : String(error) }),
2839
+ );
2840
+ };
3556
2841
  const flushPendingStepSteers = (flatIndex: number): void => {
3557
2842
  const remaining: SteerRequest[] = [];
3558
2843
  for (const request of pendingStepSteers.splice(0)) {
@@ -3636,6 +2921,7 @@ async function runSubagent(
3636
2921
  tokens: step.tokens?.total,
3637
2922
  toolCount: step.toolCount,
3638
2923
  currentTool: step.currentTool,
2924
+ toolCallId: event.toolCallId,
3639
2925
  currentToolDurationMs: 0,
3640
2926
  currentPath: step.currentPath,
3641
2927
  })));
@@ -3910,23 +3196,6 @@ async function runSubagent(
3910
3196
  pendingStepSteers.push(request);
3911
3197
  }
3912
3198
  },
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
3199
  });
3931
3200
  if (config.deadlineAt !== undefined) {
3932
3201
  const remainingMs = Math.max(0, config.deadlineAt - Date.now());
@@ -3983,7 +3252,7 @@ async function runSubagent(
3983
3252
  : [undefined]
3984
3253
  : model ? [model] : [undefined];
3985
3254
  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 });
3255
+ assertThinkingWithinCeiling({ model: candidate, configThinking, ceiling: step.parallel.thinkingCeiling, agent: step.parallel.agent, runId: id });
3987
3256
  }
3988
3257
  }
3989
3258
  if (materialized.collectedOnEmpty) await validateDynamicCollection(step.collect.outputSchema, materialized.collectedOnEmpty);
@@ -4253,11 +3522,9 @@ async function runSubagent(
4253
3522
  artifactsDir, artifactConfig, id,
4254
3523
  flatIndex: fi, flatStepCount: Math.max(statusPayload.steps.length, 1),
4255
3524
  outputFile: path.join(asyncDir, `output-${fi}.log`),
4256
- steerInboxDir: stepSteerInboxDir(asyncDir, fi),
4257
- steerCapabilityPath: steerCapabilityPath(asyncDir, fi),
4258
- steerAckDir: steerAcksDir(asyncDir, fi),
4259
3525
  piPackageRoot: config.piPackageRoot,
4260
- piArgv1: config.piArgv1,
3526
+ childSessions,
3527
+ inheritedChildRuntime: config.inheritedChildRuntime,
4261
3528
  childIntercomTarget: config.childIntercomTargets?.[fi],
4262
3529
  orchestratorIntercomTarget: config.controlIntercomTarget,
4263
3530
  nestedRoute: config.nestedRoute,
@@ -4266,6 +3533,7 @@ async function runSubagent(
4266
3533
  registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
4267
3534
  registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
4268
3535
  registerStop: (stop) => registerStepStop(fi, stop),
3536
+ registerSteer: (steer) => registerStepSteer(fi, steer),
4269
3537
  timeoutSignal: timeoutAbortController.signal,
4270
3538
  stopSignal: stopAbortController.signal,
4271
3539
  trackedMutationEvidenceForCompletionGuard: false,
@@ -4274,7 +3542,6 @@ async function runSubagent(
4274
3542
  toolTimeoutMs: task.toolTimeoutMs ?? config.toolTimeoutMs,
4275
3543
  onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking, attempt.contextLimit),
4276
3544
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
4277
- onWriterProcess,
4278
3545
  onExternalProcess: (process) => updateExternalProcess(fi, process),
4279
3546
  onExternalJob: (externalJob) => updateExternalJob(fi, externalJob),
4280
3547
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(fi),
@@ -4355,7 +3622,6 @@ async function runSubagent(
4355
3622
  output: pr.output,
4356
3623
  outputState: pr.outputState,
4357
3624
  error: pr.error,
4358
- protocolError: pr.protocolError,
4359
3625
  success: pr.stopped !== true && pr.interrupted !== true && pr.exitCode === 0,
4360
3626
  exitCode: pr.interrupted === true ? 0 : pr.exitCode,
4361
3627
  skipped: pr.skipped,
@@ -4515,6 +3781,7 @@ async function runSubagent(
4515
3781
  labels: group.parallel.map((task) => task.lane?.key ?? config.workflowKey ?? task.outputName ?? task.label),
4516
3782
  tasks: group.parallel.map((task) => task.task),
4517
3783
  provider: config.worktreeProvider,
3784
+ baseRef: config.baseRef,
4518
3785
  branchPrefix: config.worktreeBranchPrefix,
4519
3786
  setupHook: config.worktreeSetupHook
4520
3787
  ? omitUndefinedProperties({ hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs })
@@ -4652,11 +3919,9 @@ async function runSubagent(
4652
3919
  artifactsDir, artifactConfig, id,
4653
3920
  flatIndex: fi, flatStepCount: Math.max(statusPayload.steps.length, 1),
4654
3921
  outputFile: path.join(asyncDir, `output-${fi}.log`),
4655
- steerInboxDir: stepSteerInboxDir(asyncDir, fi),
4656
- steerCapabilityPath: steerCapabilityPath(asyncDir, fi),
4657
- steerAckDir: steerAcksDir(asyncDir, fi),
4658
3922
  piPackageRoot: config.piPackageRoot,
4659
- piArgv1: config.piArgv1,
3923
+ childSessions,
3924
+ inheritedChildRuntime: config.inheritedChildRuntime,
4660
3925
  childIntercomTarget: config.childIntercomTargets?.[fi],
4661
3926
  orchestratorIntercomTarget: config.controlIntercomTarget,
4662
3927
  nestedRoute: config.nestedRoute,
@@ -4665,6 +3930,7 @@ async function runSubagent(
4665
3930
  registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
4666
3931
  registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
4667
3932
  registerStop: (stop) => registerStepStop(fi, stop),
3933
+ registerSteer: (steer) => registerStepSteer(fi, steer),
4668
3934
  timeoutSignal: timeoutAbortController.signal,
4669
3935
  stopSignal: stopAbortController.signal,
4670
3936
  trackedMutationEvidenceForCompletionGuard: Boolean(worktreeSetup),
@@ -4673,7 +3939,6 @@ async function runSubagent(
4673
3939
  toolTimeoutMs: taskForRun.toolTimeoutMs ?? config.toolTimeoutMs,
4674
3940
  onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking, attempt.contextLimit),
4675
3941
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
4676
- onWriterProcess,
4677
3942
  onExternalProcess: (process) => updateExternalProcess(fi, process),
4678
3943
  onExternalJob: (externalJob) => updateExternalJob(fi, externalJob),
4679
3944
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(fi),
@@ -4801,7 +4066,6 @@ async function runSubagent(
4801
4066
  output: pr.output,
4802
4067
  outputState: pr.outputState,
4803
4068
  error: pr.error,
4804
- protocolError: pr.protocolError,
4805
4069
  success: pr.stopped !== true && pr.interrupted !== true && pr.exitCode === 0,
4806
4070
  exitCode: pr.interrupted === true ? 0 : pr.exitCode,
4807
4071
  skipped: pr.skipped,
@@ -4952,6 +4216,7 @@ async function runSubagent(
4952
4216
  labels: [seqStep.lane?.key ?? config.workflowKey ?? seqStep.outputName ?? seqStep.label],
4953
4217
  tasks: [seqStep.task],
4954
4218
  provider: config.worktreeProvider,
4219
+ baseRef: config.baseRef,
4955
4220
  branchPrefix: config.worktreeBranchPrefix,
4956
4221
  setupHook: config.worktreeSetupHook
4957
4222
  ? omitUndefinedProperties({ hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs })
@@ -5019,11 +4284,9 @@ async function runSubagent(
5019
4284
  artifactsDir, artifactConfig, id,
5020
4285
  flatIndex, flatStepCount: Math.max(statusPayload.steps.length, 1),
5021
4286
  outputFile: path.join(asyncDir, `output-${flatIndex}.log`),
5022
- steerInboxDir: stepSteerInboxDir(asyncDir, flatIndex),
5023
- steerCapabilityPath: steerCapabilityPath(asyncDir, flatIndex),
5024
- steerAckDir: steerAcksDir(asyncDir, flatIndex),
5025
4287
  piPackageRoot: config.piPackageRoot,
5026
- piArgv1: config.piArgv1,
4288
+ childSessions,
4289
+ inheritedChildRuntime: config.inheritedChildRuntime,
5027
4290
  childIntercomTarget: config.childIntercomTargets?.[flatIndex],
5028
4291
  orchestratorIntercomTarget: config.controlIntercomTarget,
5029
4292
  nestedRoute: config.nestedRoute,
@@ -5032,6 +4295,7 @@ async function runSubagent(
5032
4295
  registerInterrupt: (interrupt) => registerStepInterrupt(flatIndex, interrupt),
5033
4296
  registerTimeout: (interrupt) => registerStepTimeout(flatIndex, interrupt),
5034
4297
  registerStop: (stop) => registerStepStop(flatIndex, stop),
4298
+ registerSteer: (steer) => registerStepSteer(flatIndex, steer),
5035
4299
  timeoutSignal: timeoutAbortController.signal,
5036
4300
  stopSignal: stopAbortController.signal,
5037
4301
  timeoutMessage,
@@ -5039,7 +4303,6 @@ async function runSubagent(
5039
4303
  toolTimeoutMs: seqStep.toolTimeoutMs ?? config.toolTimeoutMs,
5040
4304
  onAttemptStart: (attempt) => updateStepModel(flatIndex, attempt.model, attempt.thinking, attempt.contextLimit),
5041
4305
  onChildEvent: (event) => updateStepFromChildEvent(flatIndex, event),
5042
- onWriterProcess,
5043
4306
  onExternalProcess: (process) => updateExternalProcess(flatIndex, process),
5044
4307
  onExternalJob: (externalJob) => updateExternalJob(flatIndex, externalJob),
5045
4308
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(flatIndex),
@@ -5067,7 +4330,6 @@ async function runSubagent(
5067
4330
  output: stopped || childStopped ? stopMessage : timedOut ? singleResult.output || (timeoutMessage ?? "Subagent timed out.") : singleResult.output,
5068
4331
  outputState: singleResult.outputState,
5069
4332
  error: stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error,
5070
- protocolError: singleResult.protocolError,
5071
4333
  success: !stopped && !childStopped && !timedOut && singleResult.interrupted !== true && singleResult.exitCode === 0,
5072
4334
  exitCode: stopped || childStopped ? 1 : timedOut ? 1 : singleResult.interrupted === true ? 0 : singleResult.exitCode,
5073
4335
  sessionFile: singleResult.sessionFile,
@@ -5335,7 +4597,7 @@ async function runSubagent(
5335
4597
  clearTimeout(timeoutTimer);
5336
4598
  timeoutTimer = undefined;
5337
4599
  }
5338
- if (!timedOut && !stopped && !interrupted && config.timeoutMs !== undefined && results.some((result) => result.timedOut === true && result.error === timeoutMessage)) {
4600
+ if (!timedOut && !stopped && !interrupted && config.timeoutMs !== undefined && timeoutMessage !== undefined && results.some((result) => result.timedOut === true && result.error?.startsWith(timeoutMessage))) {
5339
4601
  timedOut = true;
5340
4602
  }
5341
4603
  const signalTerminated = !stopped && !timedOut && !interrupted && results.some((result) => result.exitCode !== 0 && isUnexplainedProcessSignal(omitUndefinedProperties({
@@ -5425,7 +4687,6 @@ async function runSubagent(
5425
4687
  output: r.output,
5426
4688
  outputState: r.outputState,
5427
4689
  error: r.error,
5428
- protocolError: r.protocolError,
5429
4690
  success: r.success,
5430
4691
  skipped: r.skipped || undefined,
5431
4692
  interrupted: r.interrupted || undefined,
@@ -5507,7 +4768,7 @@ async function runSubagent(
5507
4768
  statusPayload.lastUpdate = Date.now();
5508
4769
  }
5509
4770
  writeStatusPayload();
5510
- orcaProgressTab?.finish(statusPayload.state === "complete" ? "completed" : statusPayload.state === "stopped" ? "stopped" : "failed", effectiveSessionFile);
4771
+ await orcaProgressTab?.finish(statusPayload.state === "complete" ? "completed" : statusPayload.state === "stopped" ? "stopped" : "failed", effectiveSessionFile);
5511
4772
  appendJsonl(
5512
4773
  eventsPath,
5513
4774
  JSON.stringify({
@@ -5542,14 +4803,21 @@ async function runSubagent(
5542
4803
  }), (filePath, content) => runPersistence.write(filePath, { content }, (_path, payload) => {
5543
4804
  fs.writeFileSync(_path, (payload as { content: string }).content, "utf-8");
5544
4805
  }));
5545
- if (runPersistence.pendingCount() === 0) runPersistence.dispose();
5546
- if (indexPersistence.pendingCount() === 0) indexPersistence.dispose();
4806
+ // The run is committed: release the child sessions first, then wait for the
4807
+ // storage-capacity retries that may still hold the final status or result.
4808
+ await childSessions.dispose().catch((error: unknown) => console.error("Failed to dispose runner child sessions:", error));
4809
+ while (runPersistence.pendingCount() + indexPersistence.pendingCount() > 0) {
4810
+ await new Promise((resolve) => setTimeout(resolve, 200));
4811
+ }
4812
+ runPersistence.dispose();
4813
+ indexPersistence.dispose();
5547
4814
  if (config.runnerProcessInstanceId) {
4815
+ // Children run inside this process, so no step has writer processes to prove terminal.
5548
4816
  const writers: Record<string, PiWriterProcessInstanceExitV1[]> = {};
5549
4817
  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;
4818
+ for (const index of results.keys()) {
4819
+ writers[String(index)] = [];
4820
+ expectedWriters[String(index)] = 0;
5553
4821
  }
5554
4822
  const candidate: ProcessTerminalCandidate = {
5555
4823
  version: 1,
@@ -5631,7 +4899,16 @@ async function runConfiguredSubagent(config: SubagentRunConfig): Promise<void> {
5631
4899
  }
5632
4900
  }
5633
4901
  }
5634
- await runSubagent(config, lease ? (writer) => lease!.updateWriter(writer) : undefined);
4902
+ const childSessions = await loadRunnerChildSessionFactory(config);
4903
+ try {
4904
+ await runSubagent(config, childSessions);
4905
+ } finally {
4906
+ try {
4907
+ await childSessions.dispose();
4908
+ } catch (error) {
4909
+ console.error("Failed to dispose runner child sessions:", error);
4910
+ }
4911
+ }
5635
4912
  } catch (error) {
5636
4913
  if (!startupCommitted) {
5637
4914
  try {
@@ -5660,10 +4937,16 @@ async function runConfiguredSubagent(config: SubagentRunConfig): Promise<void> {
5660
4937
  }
5661
4938
 
5662
4939
  function startConfiguredSubagent(config: SubagentRunConfig): void {
5663
- runConfiguredSubagent(config).catch((runErr) => {
5664
- console.error("Subagent runner error:", runErr);
5665
- process.exit(1);
5666
- });
4940
+ // Child sessions and the extensions loaded into them may leave handles
4941
+ // behind even after shutdown; the run is fully persisted by now, so exit
4942
+ // explicitly instead of waiting for the event loop to drain.
4943
+ runConfiguredSubagent(config).then(
4944
+ () => process.exit(0),
4945
+ (runErr) => {
4946
+ console.error("Subagent runner error:", runErr);
4947
+ process.exit(1);
4948
+ },
4949
+ );
5667
4950
  }
5668
4951
 
5669
4952
  const configArg = process.argv[2];