pi-subagents 0.63.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 (107) hide show
  1. package/CHANGELOG.md +57 -1
  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 +92 -114
  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 +6 -5
  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 +46 -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/notify.ts +41 -2
  34. package/src/runs/background/process-terminal.ts +1 -1
  35. package/src/runs/background/run-child-session.ts +613 -0
  36. package/src/runs/background/run-status.ts +0 -1
  37. package/src/runs/background/runner-aliases.ts +125 -0
  38. package/src/runs/background/runner-child-sessions.ts +31 -0
  39. package/src/runs/background/scheduled-runs.ts +18 -4
  40. package/src/runs/background/subagent-runner.ts +203 -898
  41. package/src/runs/foreground/async-steering-action.ts +1 -17
  42. package/src/runs/foreground/execution.ts +196 -378
  43. package/src/runs/foreground/foreground-control.ts +4 -0
  44. package/src/runs/foreground/subagent-executor.ts +121 -58
  45. package/src/runs/foreground/workflow-foreground-steering.ts +24 -98
  46. package/src/runs/shared/abort-recovery.ts +3 -3
  47. package/src/runs/shared/acceptance.ts +10 -0
  48. package/src/runs/shared/async-status-projection.ts +11 -43
  49. package/src/runs/shared/capability-ceiling.ts +1 -2
  50. package/src/runs/shared/child-hooks.ts +25 -0
  51. package/src/runs/shared/child-identity.ts +13 -2
  52. package/src/runs/shared/child-launch.ts +314 -0
  53. package/src/runs/shared/child-lifecycle.ts +25 -0
  54. package/src/runs/shared/child-runtime-config.ts +126 -0
  55. package/src/runs/shared/child-session.ts +342 -0
  56. package/src/runs/shared/child-tool-plan.ts +530 -0
  57. package/src/runs/shared/claude-code-adapter.ts +5 -1
  58. package/src/runs/shared/completion-guard.ts +1 -1
  59. package/src/runs/shared/external-cli-preflight.ts +16 -0
  60. package/src/runs/shared/mcp-direct-tool-allowlist.ts +5 -4
  61. package/src/runs/shared/model-exclusions.ts +82 -14
  62. package/src/runs/shared/model-fallback.ts +47 -4
  63. package/src/runs/shared/nested-events.ts +29 -45
  64. package/src/runs/shared/nested-path.ts +0 -14
  65. package/src/runs/shared/orca-progress-tabs.ts +12 -7
  66. package/src/runs/shared/parallel-utils.ts +0 -2
  67. package/src/runs/shared/permissions.ts +0 -13
  68. package/src/runs/shared/process-signal.ts +4 -1
  69. package/src/runs/shared/run-fanout-budget.ts +0 -13
  70. package/src/runs/shared/runtime-acknowledged-extensions.ts +0 -27
  71. package/src/runs/shared/structured-output.ts +17 -4
  72. package/src/runs/shared/subagent-control.ts +6 -2
  73. package/src/runs/shared/subagent-prompt-runtime.ts +87 -384
  74. package/src/runs/shared/tool-availability.ts +18 -62
  75. package/src/runs/shared/tool-budget.ts +0 -14
  76. package/src/runs/shared/worktree-cleanup-plan.ts +25 -6
  77. package/src/runs/shared/worktree.ts +117 -30
  78. package/src/shared/child-session-name.ts +1 -1
  79. package/src/shared/jsonl-writer.ts +11 -0
  80. package/src/shared/thinking-ceiling.ts +0 -6
  81. package/src/shared/types.ts +64 -30
  82. package/src/shared/utils.ts +3 -4
  83. package/src/slash/slash-commands.ts +0 -6
  84. package/src/tui/fleet.ts +0 -1
  85. package/src/tui/render.ts +238 -31
  86. package/src/watchdog/child-status.ts +54 -34
  87. package/src/watchdog/diff-tool.ts +77 -0
  88. package/src/watchdog/emission-guard.ts +5 -3
  89. package/src/watchdog/guidance.ts +20 -0
  90. package/src/watchdog/register-child.ts +28 -25
  91. package/src/watchdog/register-main.ts +10 -9
  92. package/src/watchdog/render.ts +4 -5
  93. package/src/watchdog/review.ts +15 -4
  94. package/src/watchdog/rules.ts +70 -0
  95. package/src/watchdog/runtime.ts +75 -92
  96. package/src/watchdog/scope.ts +0 -11
  97. package/src/watchdog/settings.ts +48 -104
  98. package/src/watchdog/types.ts +18 -32
  99. package/src/watchdog/warning-format.ts +0 -1
  100. package/src/workflows/chat-progress.ts +3 -2
  101. package/src/workflows/scripted-workflow.ts +48 -1
  102. package/src/workflows/workflow-checklist.ts +10 -12
  103. package/src/workflows/workflow-preflight.ts +28 -1
  104. package/src/runs/shared/child-protocol.ts +0 -415
  105. package/src/runs/shared/pi-args.ts +0 -1062
  106. package/src/runs/shared/subagent-startup-retry.ts +0 -116
  107. 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,19 +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
- childWatchdogIsActive,
160
- decodeChildWatchdogConfig,
148
+ applyChildWatchdogMessage,
161
149
  isChildWatchdogStatusEvent,
162
150
  resolveChildWatchdogConfig,
163
- type ChildWatchdogStateSnapshot,
151
+ type ChildWatchdogStatusEvent,
164
152
  } from "../../watchdog/child-status.ts";
165
153
 
166
154
  const INTERCOM_DETACH_RECEIPT = "Detached for intercom coordination before task completion.";
167
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
+
168
161
  interface SubagentRunConfig {
169
162
  id: string;
170
163
  steps: RunnerStep[];
@@ -182,10 +175,14 @@ interface SubagentRunConfig {
182
175
  sessionId?: string | null;
183
176
  completionOwnerId?: string;
184
177
  piPackageRoot?: string;
185
- 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;
186
182
  worktreeSetupHook?: string;
187
183
  worktreeSetupHookTimeoutMs?: number;
188
184
  worktreeBaseDir?: string;
185
+ baseRef?: string;
189
186
  worktreeProvider?: import("../../shared/types.ts").WorktreeProvider;
190
187
  worktreeBranchPrefix?: string;
191
188
  controlConfig?: ResolvedControlConfig;
@@ -231,7 +228,6 @@ interface StepResult {
231
228
  output: string;
232
229
  outputState?: SubagentOutputState;
233
230
  error?: string;
234
- protocolError?: ProtocolOutputLimit;
235
231
  success?: boolean;
236
232
  exitCode: number | null;
237
233
  usage?: Usage;
@@ -248,6 +244,7 @@ interface StepResult {
248
244
  sessionFile?: string;
249
245
  intercomTarget?: string;
250
246
  model?: string;
247
+ thinking?: string;
251
248
  attemptedModels?: string[];
252
249
  modelAttempts?: ModelAttempt[];
253
250
  /** True when the dispatch failed because the input exceeded the model's context window. */
@@ -269,8 +266,6 @@ interface StepResult {
269
266
  structuredOutputSchemaPath?: string;
270
267
  acceptance?: import("../../shared/types.ts").AcceptanceLedger;
271
268
  watchdog?: import("../../shared/types.ts").ChildWatchdogProgress;
272
- writerProcesses?: PiWriterProcessInstanceExitV1[];
273
- writerAttemptCount?: number;
274
269
  runner?: ExternalCliRunnerStatus | ExternalJobRunnerStatus;
275
270
  externalProcess?: ExternalProcessStatus;
276
271
  externalJob?: ExternalJobStatus;
@@ -371,10 +366,6 @@ function appendDiagnosticJsonl(filePath: string, line: string, droppedEventType?
371
366
  state.diagnosticsTruncated = true;
372
367
  }
373
368
 
374
- function shouldPersistChildEvent(event: Record<string, unknown>): boolean {
375
- return event.type !== "message_update";
376
- }
377
-
378
369
  function isBlockingSupervisorTool(toolName: string | undefined, args: unknown): boolean {
379
370
  if (!args || typeof args !== "object" || Array.isArray(args)) return false;
380
371
  if (toolName === "contact_supervisor") {
@@ -457,15 +448,6 @@ function appendRecentStepOutput(step: RunnerStatusStep, lines: string[]): void {
457
448
  }
458
449
  }
459
450
 
460
- function assistantStartsToolCall(message: Message): boolean {
461
- return Array.isArray(message.content)
462
- && message.content.some((part) => (part as { type?: string }).type === "toolCall");
463
- }
464
-
465
- function isTerminalAssistantStop(message: Message): boolean {
466
- return (message as { stopReason?: string }).stopReason === "stop" && !assistantStartsToolCall(message);
467
- }
468
-
469
451
  type UndefinedOmitted<T extends object> = {
470
452
  [K in keyof T]: Exclude<T[K], undefined>;
471
453
  };
@@ -508,73 +490,6 @@ function resetStepLiveDetail(step: RunnerStatusStep): void {
508
490
  step.recentOutput = [];
509
491
  }
510
492
 
511
- interface ChildEventContext {
512
- eventsPath: string;
513
- runId: string;
514
- stepIndex: number;
515
- agent: string;
516
- }
517
-
518
- interface ChildUsage {
519
- input?: number;
520
- inputTokens?: number;
521
- output?: number;
522
- outputTokens?: number;
523
- cacheRead?: number;
524
- cacheReadTokens?: number;
525
- cacheWrite?: number;
526
- cost?: { total?: number };
527
- }
528
-
529
- type ChildMessage = Message & {
530
- model?: string;
531
- errorMessage?: string;
532
- usage?: ChildUsage;
533
- };
534
-
535
- interface ChildEvent {
536
- type?: string;
537
- message?: ChildMessage;
538
- toolName?: string;
539
- args?: Record<string, unknown>;
540
- willRetry?: unknown;
541
- }
542
-
543
- interface RunPiStreamingResult {
544
- stderr: string;
545
- exitCode: number | null;
546
- messages: Message[];
547
- usage: Usage;
548
- toolCount: number;
549
- durationMs: number;
550
- model?: string;
551
- error?: string;
552
- protocolError?: ProtocolOutputLimit;
553
- finalOutput: string;
554
- outputState: SubagentOutputState;
555
- interrupted?: boolean;
556
- timedOut?: boolean;
557
- stopped?: boolean;
558
- toolBudget?: ToolBudgetState;
559
- toolBudgetBlocked?: boolean;
560
- observedMutationAttempt?: boolean;
561
- structuredOutputToolInvoked?: boolean;
562
- structuredOutputMessageStartIndex?: number;
563
- structuredOutput?: unknown;
564
- watchdog?: ChildWatchdogStateSnapshot;
565
- runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
566
- processInstanceId: string;
567
- processCloseObservedAt?: number;
568
- processSignal?: string | null;
569
- processTree: ProcessTreeTerminalV1;
570
- currentTool?: string;
571
- currentToolArgs?: string;
572
- currentPath?: string;
573
- afterCompactionSettlement?: boolean;
574
- abortRecoveryDiagnostic?: string;
575
- effects?: import("../../shared/types.ts").EffectsProjection;
576
- }
577
-
578
493
  const MAX_CHILD_FAILURE_DIAGNOSTIC_CHARS = 8_192;
579
494
 
580
495
  function formatRequiredOutputError(requiredOutput: {
@@ -609,565 +524,6 @@ function formatChildFailureDiagnostic(input: {
609
524
  return `${baseError.slice(0, Math.max(0, errorLimit))}${context ? `\n${context}` : ""}`;
610
525
  }
611
526
 
612
- function runPiStreaming(
613
- args: string[],
614
- cwd: string,
615
- outputFile: string,
616
- env?: Record<string, string | undefined>,
617
- piPackageRoot?: string,
618
- piArgv1?: string,
619
- maxSubagentDepth?: number,
620
- childEventContext?: ChildEventContext,
621
- registerInterrupt?: (interrupt: (() => void) | undefined) => void,
622
- onChildEvent?: (event: ChildEvent) => void,
623
- transcriptWriter?: ChildTranscriptWriter,
624
- registerTimeout?: (interrupt: (() => void) | undefined) => void,
625
- timeoutMessage?: string,
626
- registerStop?: (stop: (() => void) | undefined) => void,
627
- stopMessage?: string,
628
- onWriterProcess?: (writer: { state: "none" | "spawning" } | { state: "running"; pid: number }) => void,
629
- toolTimeoutMs?: number,
630
- runDeadlineAt?: number,
631
- orcaProgressTab?: OrcaProgressTab,
632
- expectedModelForVerification?: string,
633
- modelVerificationRegistry?: Array<{ provider: string; id: string; fullId: string }>,
634
- mutationTools?: readonly string[],
635
- ): Promise<RunPiStreamingResult> {
636
- return new Promise((resolve) => {
637
- const startedAt = Date.now();
638
- const processInstanceId = randomUUID();
639
- onWriterProcess?.({ state: "spawning" });
640
- const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
641
- const spawnEnv = { ...process.env, ...(env ?? {}), ...getSubagentDepthEnv(maxSubagentDepth) };
642
- const spawnSpec = getPiSpawnCommand(args, {
643
- ...(piPackageRoot ? { piPackageRoot } : {}),
644
- ...(piArgv1 ? { argv1: piArgv1 } : {}),
645
- });
646
- const child = spawn(spawnSpec.command, spawnSpec.args, {
647
- cwd,
648
- stdio: ["ignore", "pipe", "pipe"],
649
- env: spawnEnv,
650
- windowsHide: true,
651
- detached: process.platform !== "win32",
652
- });
653
- let processTreeController: OwnedProcessTreeController | undefined;
654
- const stderrTail = createBoundedByteTail();
655
- const rawStdoutTail = createBoundedByteTail();
656
- const messages: Message[] = [];
657
- const usage = emptyUsage();
658
- let model: string | undefined;
659
- let writerRegistrationError: string | undefined;
660
- if (typeof child.pid === "number") {
661
- processTreeController = createOwnedProcessTreeController(child.pid);
662
- try {
663
- onWriterProcess?.({ state: "running", pid: child.pid });
664
- } catch (writerError) {
665
- writerRegistrationError = `Failed to record revived Pi writer ownership: ${writerError instanceof Error ? writerError.message : String(writerError)}`;
666
- trySignalChild(child, "SIGKILL");
667
- }
668
- }
669
- let error: string | undefined = writerRegistrationError;
670
- let assistantError: string | undefined;
671
- let interrupted = false;
672
- let timedOut = false;
673
- let stopped = false;
674
- let observedMutationAttempt = false;
675
- let structuredOutputToolInvoked = false;
676
- let structuredOutputMessageStartIndex: number | undefined;
677
- let currentTool: string | undefined;
678
- let currentToolArgs: string | undefined;
679
- let currentPath: string | undefined;
680
- let toolCount = 0;
681
- type ActiveToolCall = { key: string; tool: string; args?: string; path?: string };
682
- let activeToolSequence = 0;
683
- const activeToolCalls = new Map<string, ActiveToolCall>();
684
- const activeToolKeysByName = new Map<string, string[]>();
685
- const refreshCurrentTool = (): void => {
686
- const active = [...activeToolCalls.values()].at(-1);
687
- currentTool = active?.tool;
688
- currentToolArgs = active?.args;
689
- currentPath = active?.path;
690
- };
691
- const recordActiveToolCall = (event: { toolCallId?: unknown; toolName: string; args?: Record<string, unknown> }): void => {
692
- const key = toolTimeoutCallKey(event, ++activeToolSequence);
693
- const active = omitUndefinedProperties({
694
- key,
695
- tool: event.toolName,
696
- args: extractToolArgsPreview(event.args ?? {}),
697
- path: resolveCurrentPath(event.toolName, event.args),
698
- });
699
- activeToolCalls.set(key, active);
700
- const keys = activeToolKeysByName.get(active.tool) ?? [];
701
- keys.push(key);
702
- activeToolKeysByName.set(active.tool, keys);
703
- refreshCurrentTool();
704
- };
705
- const removeActiveToolCall = (event: { toolCallId?: unknown; toolName?: unknown }): void => {
706
- const key = typeof event.toolCallId === "string" && event.toolCallId.length > 0
707
- ? `id:${event.toolCallId}`
708
- : typeof event.toolName === "string"
709
- ? activeToolKeysByName.get(event.toolName)?.[0]
710
- : activeToolCalls.size === 1
711
- ? [...activeToolCalls.keys()][0]
712
- : undefined;
713
- if (!key) return;
714
- const active = activeToolCalls.get(key);
715
- if (!active) return;
716
- activeToolCalls.delete(key);
717
- const keys = activeToolKeysByName.get(active.tool)?.filter((candidate) => candidate !== key) ?? [];
718
- if (keys.length > 0) activeToolKeysByName.set(active.tool, keys);
719
- else activeToolKeysByName.delete(active.tool);
720
- refreshCurrentTool();
721
- };
722
- const childWatchdogConfig = decodeChildWatchdogConfig(env?.[CHILD_WATCHDOG_CONFIG_ENV]);
723
- let childWatchdogState: ChildWatchdogStateSnapshot | undefined;
724
- const childLifecycleState: ChildLifecycleState = { compactionRetryActive: false };
725
- let applyChildLifecycle = (_action: ChildLifecycleAction): void => {};
726
- const updateChildWatchdogState = (snapshot: ChildWatchdogStateSnapshot): void => {
727
- childWatchdogState = snapshot;
728
- };
729
-
730
- const writeOutputLine = (line: string) => {
731
- if (!line.trim()) return;
732
- outputStream.write(`${line}\n`);
733
- orcaProgressTab?.append(`${line}\n`);
734
- };
735
-
736
- const writeOutputText = (text: string) => {
737
- for (const line of text.split("\n")) {
738
- writeOutputLine(line);
739
- }
740
- };
741
-
742
- const appendChildEvent = (event: Record<string, unknown>) => {
743
- if (!childEventContext) return;
744
- if (!shouldPersistChildEvent(event)) return;
745
- appendDiagnosticJsonl(childEventContext.eventsPath, JSON.stringify({
746
- ...event,
747
- subagentSource: "child",
748
- subagentRunId: childEventContext.runId,
749
- subagentStepIndex: childEventContext.stepIndex,
750
- subagentAgent: childEventContext.agent,
751
- observedAt: Date.now(),
752
- }), typeof event.type === "string" ? event.type : undefined);
753
- };
754
-
755
- const appendChildLine = (type: "subagent.child.stdout" | "subagent.child.stderr", line: string) => {
756
- appendChildEvent({ type, line });
757
- if (type === "subagent.child.stdout") transcriptWriter?.writeStdoutLine(line);
758
- else transcriptWriter?.writeStderrLine(line);
759
- };
760
-
761
- const processStdoutLine = (line: string) => {
762
- if (!line.trim()) return;
763
- let event: ChildEvent;
764
- try {
765
- event = JSON.parse(line) as ChildEvent;
766
- } catch {
767
- rawStdoutTail.push(`${line}\n`);
768
- writeOutputLine(line);
769
- appendChildLine("subagent.child.stdout", line);
770
- return;
771
- }
772
-
773
- appendChildEvent(event as unknown as Record<string, unknown>);
774
- transcriptWriter?.writeChildEvent(event);
775
- if (event.type === "compaction_start") compactionStartedReceived = true;
776
- if (event.type === "compaction_end" && event.willRetry === true) {
777
- compactionStartedReceived = false;
778
- afterCompactionSettlement = false;
779
- }
780
- if (event.type === "agent_start" || event.type === "auto_retry_start") {
781
- compactionStartedReceived = false;
782
- afterCompactionSettlement = false;
783
- }
784
- const lifecycleAction = projectChildLifecycle(event, false, childLifecycleState);
785
- if (event.type === "agent_settled" && lifecycleAction === "start-drain") {
786
- agentSettledReceived = true;
787
- afterCompactionSettlement = compactionStartedReceived;
788
- }
789
- applyChildLifecycle(lifecycleAction);
790
-
791
- if (isChildWatchdogStatusEvent(event)) {
792
- if (!childWatchdogConfig) return;
793
- const next = acceptChildWatchdogEvent({
794
- current: childWatchdogState,
795
- event,
796
- ...(childEventContext ? {
797
- runId: childEventContext.runId,
798
- agent: childEventContext.agent,
799
- childIndex: childEventContext.stepIndex,
800
- } : {}),
801
- });
802
- if (!next) return;
803
- updateChildWatchdogState(next);
804
- onChildEvent?.(event);
805
- if (childWatchdogIsActive(next)) {
806
- if (finalDrainTimer) {
807
- clearTimeout(finalDrainTimer);
808
- finalDrainTimer = undefined;
809
- }
810
- if (finalHardKillTimer) {
811
- clearTimeout(finalHardKillTimer);
812
- finalHardKillTimer = undefined;
813
- }
814
- armWatchdogTail();
815
- } else {
816
- clearWatchdogTailTimer();
817
- if (cleanTerminalAssistantStopReceived || agentSettledReceived) startFinalDrain();
818
- }
819
- return;
820
- }
821
-
822
- onChildEvent?.(event);
823
-
824
- if (event.type === "tool_execution_end") {
825
- clearActiveToolTimeout(event);
826
- removeActiveToolCall(event);
827
- return;
828
- }
829
-
830
- if (event.type === "tool_execution_start" && event.toolName) {
831
- toolCount += 1;
832
- armToolTimeout({ toolCallId: (event as { toolCallId?: unknown }).toolCallId, toolName: event.toolName });
833
- recordActiveToolCall({ toolCallId: (event as { toolCallId?: unknown }).toolCallId, toolName: event.toolName, args: event.args });
834
- if (event.toolName === "structured_output") {
835
- structuredOutputToolInvoked = true;
836
- structuredOutputMessageStartIndex = messages.length;
837
- }
838
- observedMutationAttempt = observedMutationAttempt || isMutatingTool(event.toolName, event.args, mutationTools);
839
- const toolArgs = extractToolArgsPreview(event.args ?? {});
840
- writeOutputLine(toolArgs ? `${event.toolName}: ${toolArgs}` : event.toolName);
841
- return;
842
- }
843
-
844
- if ((event.type === "message_end" || event.type === "tool_result_end") && event.message) {
845
- if (event.type === "tool_result_end") {
846
- clearActiveToolTimeout(event);
847
- removeActiveToolCall({
848
- toolCallId: (event.message as { toolCallId?: unknown }).toolCallId ?? (event as { toolCallId?: unknown }).toolCallId,
849
- toolName: (event.message as { toolName?: unknown }).toolName ?? event.toolName,
850
- });
851
- }
852
- messages.push(event.message);
853
- const text = extractTextFromContent(event.message.content);
854
- if (text) writeOutputText(text);
855
-
856
- if (event.type !== "message_end" || event.message.role !== "assistant") return;
857
- const hasToolCall = assistantStartsToolCall(event.message);
858
- if (event.message.model) {
859
- model = event.message.model;
860
- if (expectedModelForVerification && !hasToolCall) {
861
- const modelVerificationError = formatSubagentModelVerificationError(expectedModelForVerification, event.message.model, modelVerificationRegistry);
862
- if (modelVerificationError && !error) error = modelVerificationError;
863
- }
864
- }
865
- if (event.message.errorMessage) assistantError = event.message.errorMessage;
866
- const eventUsage = event.message.usage;
867
- if (eventUsage) {
868
- usage.turns++;
869
- usage.input += eventUsage.input ?? eventUsage.inputTokens ?? 0;
870
- usage.output += eventUsage.output ?? eventUsage.outputTokens ?? 0;
871
- usage.cacheRead += eventUsage.cacheRead ?? 0;
872
- usage.cacheWrite += eventUsage.cacheWrite ?? 0;
873
- usage.cost += eventUsage.cost?.total ?? 0;
874
- }
875
- if (isTerminalAssistantStop(event.message)) {
876
- if (!event.message.errorMessage && extractTextFromContent(event.message.content).trim()) assistantError = undefined;
877
- cleanTerminalAssistantStopReceived ||= !event.message.errorMessage;
878
- clearAllToolTimeouts();
879
- activeToolCalls.clear();
880
- activeToolKeysByName.clear();
881
- refreshCurrentTool();
882
- applyChildLifecycle(projectChildLifecycle(event, true, childLifecycleState));
883
- }
884
- }
885
- };
886
-
887
- // Guard both cases that can leave the parent waiting on `close` forever:
888
- // a lingering stdio holder after `exit`, or a child that never exits.
889
- const FINAL_STOP_GRACE_MS = 1000;
890
- const HARD_KILL_MS = 3000;
891
- let childExited = false;
892
- let forcedTerminationSignal = false;
893
- let cleanTerminalAssistantStopReceived = false;
894
- let agentSettledReceived = false;
895
- let compactionStartedReceived = false;
896
- let afterCompactionSettlement = false;
897
- let finalDrainTimer: NodeJS.Timeout | undefined;
898
- let finalHardKillTimer: NodeJS.Timeout | undefined;
899
- let watchdogTailTimer: NodeJS.Timeout | undefined;
900
- let protocolHardKillTimer: NodeJS.Timeout | undefined;
901
- let protocolError: ProtocolOutputLimit | undefined;
902
- let settled = false;
903
- applyChildLifecycle = (action: ChildLifecycleAction): void => {
904
- if (action === "cancel-drain") {
905
- if (finalDrainTimer) {
906
- clearTimeout(finalDrainTimer);
907
- finalDrainTimer = undefined;
908
- }
909
- if (finalHardKillTimer) {
910
- clearTimeout(finalHardKillTimer);
911
- finalHardKillTimer = undefined;
912
- }
913
- clearWatchdogTailTimer();
914
- return;
915
- }
916
- if (action === "start-drain") startFinalDrain();
917
- };
918
- const failProtocol = (limit: ProtocolOutputLimit): void => {
919
- if (protocolError) return;
920
- protocolError = limit;
921
- error = formatProtocolOutputLimit(limit);
922
- if (!childExited) {
923
- trySignalChild(child, "SIGTERM");
924
- protocolHardKillTimer = setTimeout(() => {
925
- if (!settled) trySignalChild(child, "SIGKILL");
926
- }, 3000);
927
- protocolHardKillTimer.unref?.();
928
- }
929
- };
930
- const stdoutReader = createBoundedLineReader({
931
- oversizedLineProjector: PI_AGGREGATE_EVENT_PROJECTOR,
932
- onLine: processStdoutLine,
933
- onLimit: failProtocol,
934
- });
935
- const stderrReader = createBoundedLineReader({
936
- stream: "stderr",
937
- maxPendingLineBytes: MAX_CHILD_STDERR_BYTES,
938
- onLine: (line) => appendChildLine("subagent.child.stderr", line),
939
- onLimit: (limit) => appendChildLine("subagent.child.stderr", formatProtocolOutputLimit(limit)),
940
- });
941
- const clearStdioGuard = attachPostExitStdioGuard(child, { idleMs: 2000, hardMs: 8000 });
942
- child.stdout.on("data", (chunk: Buffer) => stdoutReader.push(chunk));
943
- child.stderr.on("data", (chunk: Buffer) => {
944
- stderrTail.push(chunk);
945
- stderrReader.push(chunk);
946
- outputStream.write(chunk);
947
- orcaProgressTab?.append(chunk.toString("utf-8"));
948
- });
949
- registerInterrupt?.(() => {
950
- if (settled || timedOut || stopped) return;
951
- interrupted = true;
952
- if (!error) error = "Interrupted. Waiting for explicit next action.";
953
- trySignalChild(child, "SIGINT");
954
- setTimeout(() => {
955
- if (!settled && !timedOut && !stopped) trySignalChild(child, "SIGTERM");
956
- }, 1000).unref?.();
957
- });
958
- const terminateForTimeout = (message: string): void => {
959
- if (settled || timedOut || stopped) return;
960
- timedOut = true;
961
- // runPiStreaming's terminal result derives the timeout error from this
962
- // message, so retain the tool-specific reason through finalization.
963
- timeoutMessage = message;
964
- interrupted = false;
965
- error = message;
966
- if (processTreeController) void processTreeController.terminate();
967
- else trySignalChild(child, "SIGTERM");
968
- };
969
- let toolTimeoutSequence = 0;
970
- const activeToolTimeouts = new Map<string, { toolName: string; timer: ReturnType<typeof setTimeout> }>();
971
- const activeToolTimeoutKeysByName = new Map<string, string[]>();
972
- const removeToolTimeoutKey = (key: string): void => {
973
- const active = activeToolTimeouts.get(key);
974
- if (!active) return;
975
- clearTimeout(active.timer);
976
- activeToolTimeouts.delete(key);
977
- const keys = activeToolTimeoutKeysByName.get(active.toolName)?.filter((candidate) => candidate !== key) ?? [];
978
- if (keys.length > 0) activeToolTimeoutKeysByName.set(active.toolName, keys);
979
- else activeToolTimeoutKeysByName.delete(active.toolName);
980
- };
981
- const clearActiveToolTimeout = (event: { toolCallId?: unknown; toolName?: unknown }): void => {
982
- const key = typeof event.toolCallId === "string" && event.toolCallId.length > 0
983
- ? `id:${event.toolCallId}`
984
- : typeof event.toolName === "string"
985
- ? activeToolTimeoutKeysByName.get(event.toolName)?.[0]
986
- : activeToolTimeouts.size === 1
987
- ? [...activeToolTimeouts.keys()][0]
988
- : undefined;
989
- if (key) removeToolTimeoutKey(key);
990
- };
991
- const clearAllToolTimeouts = (): void => {
992
- for (const key of [...activeToolTimeouts.keys()]) removeToolTimeoutKey(key);
993
- };
994
- const armToolTimeout = (event: { toolCallId?: unknown; toolName: string }): void => {
995
- const timeoutForTool = effectiveToolTimeoutMs(event.toolName, toolTimeoutMs);
996
- if (timeoutForTool === undefined) return;
997
- const runRemaining = runDeadlineAt === undefined ? undefined : Math.max(0, runDeadlineAt - Date.now());
998
- if (runRemaining !== undefined && timeoutForTool >= runRemaining) return;
999
- const key = toolTimeoutCallKey(event, ++toolTimeoutSequence);
1000
- const toolName = event.toolName;
1001
- const timer = setTimeout(() => {
1002
- removeToolTimeoutKey(key);
1003
- terminateForTimeout(formatToolTimeoutMessage(toolName, timeoutForTool));
1004
- }, timeoutForTool);
1005
- timer.unref?.();
1006
- activeToolTimeouts.set(key, { toolName, timer });
1007
- const keys = activeToolTimeoutKeysByName.get(toolName) ?? [];
1008
- keys.push(key);
1009
- activeToolTimeoutKeysByName.set(toolName, keys);
1010
- };
1011
- registerTimeout?.(() => terminateForTimeout(timeoutMessage ?? "Subagent timed out."));
1012
- registerStop?.(() => {
1013
- if (settled || timedOut || stopped) return;
1014
- stopped = true;
1015
- interrupted = false;
1016
- error = stopMessage ?? "Subagent stopped by user.";
1017
- if (processTreeController) void processTreeController.terminate();
1018
- else trySignalChild(child, "SIGTERM");
1019
- });
1020
- const clearDrainTimers = () => {
1021
- clearAllToolTimeouts();
1022
- if (finalDrainTimer) {
1023
- clearTimeout(finalDrainTimer);
1024
- finalDrainTimer = undefined;
1025
- }
1026
- if (finalHardKillTimer) {
1027
- clearTimeout(finalHardKillTimer);
1028
- finalHardKillTimer = undefined;
1029
- }
1030
- clearWatchdogTailTimer();
1031
- if (protocolHardKillTimer) {
1032
- clearTimeout(protocolHardKillTimer);
1033
- protocolHardKillTimer = undefined;
1034
- }
1035
- };
1036
- function startFinalDrain(): void {
1037
- if (childWatchdogIsActive(childWatchdogState)) {
1038
- armWatchdogTail();
1039
- return;
1040
- }
1041
- if (childExited || finalDrainTimer || settled) return;
1042
- finalDrainTimer = setTimeout(() => {
1043
- if (settled) return;
1044
- const termSent = trySignalChild(child, "SIGTERM");
1045
- if (!termSent) return;
1046
- forcedTerminationSignal = true;
1047
- if (!cleanTerminalAssistantStopReceived && !agentSettledReceived && !error && !assistantError) {
1048
- error = `Subagent process did not exit within ${FINAL_STOP_GRACE_MS}ms after its terminal event. Forcing termination.`;
1049
- }
1050
- finalHardKillTimer = setTimeout(() => {
1051
- if (settled) return;
1052
- forcedTerminationSignal = trySignalChild(child, "SIGKILL") || forcedTerminationSignal;
1053
- }, HARD_KILL_MS);
1054
- finalHardKillTimer.unref?.();
1055
- }, FINAL_STOP_GRACE_MS);
1056
- finalDrainTimer.unref?.();
1057
- }
1058
- function clearWatchdogTailTimer(): void {
1059
- if (watchdogTailTimer) {
1060
- clearTimeout(watchdogTailTimer);
1061
- watchdogTailTimer = undefined;
1062
- }
1063
- }
1064
- function armWatchdogTail(): void {
1065
- if ((!cleanTerminalAssistantStopReceived && !agentSettledReceived) || watchdogTailTimer || settled) return;
1066
- watchdogTailTimer = setTimeout(() => {
1067
- watchdogTailTimer = undefined;
1068
- updateChildWatchdogState({
1069
- phase: "stale",
1070
- seq: (childWatchdogState?.seq ?? 0) + 1,
1071
- lastUpdate: Date.now(),
1072
- followUpPending: false,
1073
- reason: "child watchdog tail timeout",
1074
- timedOut: true,
1075
- });
1076
- startFinalDrain();
1077
- }, childWatchdogConfig?.watchdogTailTimeoutMs ?? 120_000);
1078
- watchdogTailTimer.unref?.();
1079
- }
1080
- child.on("exit", () => {
1081
- childExited = true;
1082
- clearDrainTimers();
1083
- });
1084
- child.on("close", async (exitCode, signal) => {
1085
- settled = true;
1086
- const processCloseObservedAt = Date.now();
1087
- const processTree = processTreeController
1088
- ? await processTreeController.finishAfterWriterClose()
1089
- : { state: "unknown" as const, reason: "verification-failed" as const, diagnostic: "Writer PID was unavailable." };
1090
- try {
1091
- onWriterProcess?.({ state: "none" });
1092
- } catch {
1093
- // The runner still owns and releases the lease during finalization.
1094
- }
1095
- registerInterrupt?.(undefined);
1096
- registerTimeout?.(undefined);
1097
- registerStop?.(undefined);
1098
- clearDrainTimers();
1099
- clearStdioGuard();
1100
- stdoutReader.end();
1101
- stderrReader.end();
1102
- outputStream.end();
1103
- const stderr = stderrTail.text();
1104
- const finalOutput = getFinalOutput(messages) || rawStdoutTail.text().trim();
1105
- const finalError = error ?? assistantError;
1106
- const forcedDrainAfterFinalSuccess = Boolean(forcedTerminationSignal || signal) && (cleanTerminalAssistantStopReceived || agentSettledReceived) && !finalError;
1107
- const forcedDrainAfterEmptyTerminal = forcedDrainAfterFinalSuccess && hasEmptyTerminalAssistantResponse(messages);
1108
- const forcedDrainError = forcedDrainAfterEmptyTerminal && stderr.trim()
1109
- ? stderr.trim()
1110
- : undefined;
1111
- const signalError = isUnexplainedProcessSignal({
1112
- processSignal: signal,
1113
- interrupted,
1114
- timedOut,
1115
- stopped,
1116
- forcedDrainAfterFinalSuccess: forcedDrainAfterFinalSuccess && !forcedDrainAfterEmptyTerminal,
1117
- }) ? formatProcessSignalError(signal!) : undefined;
1118
- resolve(omitUndefinedProperties({
1119
- stderr,
1120
- exitCode: timedOut || stopped ? 1 : interrupted || (forcedDrainAfterFinalSuccess && !forcedDrainAfterEmptyTerminal) ? 0 : forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
1121
- messages,
1122
- usage,
1123
- toolCount,
1124
- durationMs: Date.now() - startedAt,
1125
- model,
1126
- error: stopped ? (stopMessage ?? "Subagent stopped by user.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : interrupted || (forcedDrainAfterFinalSuccess && !forcedDrainAfterEmptyTerminal) ? undefined : finalError ?? forcedDrainError ?? signalError,
1127
- protocolError,
1128
- finalOutput: (timedOut || stopped) && !finalOutput.trim() ? (stopped ? stopMessage ?? "Subagent stopped by user." : timeoutMessage ?? "Subagent timed out.") : finalOutput,
1129
- outputState: finalOutput.trim() ? "present" : "absent",
1130
- interrupted,
1131
- timedOut,
1132
- stopped,
1133
- observedMutationAttempt,
1134
- structuredOutputToolInvoked,
1135
- structuredOutputMessageStartIndex,
1136
- watchdog: childWatchdogState,
1137
- processInstanceId,
1138
- processCloseObservedAt,
1139
- processSignal: signal,
1140
- processTree,
1141
- currentTool,
1142
- currentToolArgs,
1143
- currentPath,
1144
- afterCompactionSettlement: afterCompactionSettlement || undefined,
1145
- }));
1146
- });
1147
-
1148
- child.on("error", (spawnError) => {
1149
- settled = true;
1150
- try {
1151
- onWriterProcess?.({ state: "none" });
1152
- } catch {
1153
- // The runner still owns and releases the lease during finalization.
1154
- }
1155
- registerInterrupt?.(undefined);
1156
- registerTimeout?.(undefined);
1157
- registerStop?.(undefined);
1158
- clearDrainTimers();
1159
- clearStdioGuard();
1160
- stdoutReader.end();
1161
- stderrReader.end();
1162
- outputStream.end();
1163
- const stderr = stderrTail.text();
1164
- const finalOutput = getFinalOutput(messages) || rawStdoutTail.text().trim();
1165
- const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
1166
- 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 } }));
1167
- });
1168
- });
1169
- }
1170
-
1171
527
  function resolvePiPackageRootFallback(): string {
1172
528
  const root = resolveInstalledPiPackageRoot();
1173
529
  if (root) return root;
@@ -1289,15 +645,17 @@ interface SingleStepContext {
1289
645
  flatIndex: number;
1290
646
  flatStepCount: number;
1291
647
  outputFile: string;
1292
- steerInboxDir?: string;
1293
- steerCapabilityPath?: string;
1294
- steerAckDir?: string;
1295
648
  transcriptPath?: string;
1296
649
  piPackageRoot?: string;
1297
- 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;
1298
654
  registerInterrupt?: (interrupt: (() => void) | undefined) => void;
1299
655
  registerTimeout?: (interrupt: (() => void) | undefined) => void;
1300
656
  registerStop?: (stop: (() => void) | undefined) => void;
657
+ /** Receives the live child's steer handler while its session runs. */
658
+ registerSteer?: (steer: StepSteerHandler | undefined) => void;
1301
659
  timeoutSignal?: AbortSignal;
1302
660
  stopSignal?: AbortSignal;
1303
661
  timeoutMessage?: string;
@@ -1313,7 +671,6 @@ interface SingleStepContext {
1313
671
  runFanoutBudget?: RunFanoutBudgetDescriptor;
1314
672
  onAttemptStart?: (attempt: { model?: string; thinking?: string; contextLimit?: number }) => void;
1315
673
  onChildEvent?: (event: ChildEvent) => void;
1316
- onWriterProcess?: (writer: { state: "none" | "spawning" } | { state: "running"; pid: number }) => void;
1317
674
  onExternalProcess?: (process: ExternalProcessStatus) => void;
1318
675
  onExternalJob?: (status: ExternalJobStatus) => void;
1319
676
  skipAcceptance?: () => boolean;
@@ -1419,13 +776,11 @@ async function runSingleStepInner(
1419
776
  model: step.model,
1420
777
  modelCandidates: step.modelCandidates,
1421
778
  mcpDirectTools: step.mcpDirectTools,
1422
- mcpConfig: step.mcpConfig,
1423
- runtimeServerNames: step.runtimeServerNames,
1424
779
  cwd: step.cwd ?? ctx.cwd,
1425
780
  requireReadTool: Boolean(step.skills?.length),
1426
781
  structuredOutput: Boolean(effectiveStructuredOutput),
1427
782
  capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
1428
- inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
783
+ inheritedCapabilityCeiling: ctx.inheritedChildRuntime?.capabilityCeiling,
1429
784
  permissionRules: step.permissionRules,
1430
785
  }));
1431
786
  const contractTools = resolvedTaskToolPlan.explicitToolAllowlist ? resolvedTaskToolPlan.effectiveToolAllowlist : undefined;
@@ -1645,12 +1000,10 @@ async function runSingleStepInner(
1645
1000
  let capabilityAudit: import("../shared/capability-ceiling.ts").SubagentCapabilityAudit | undefined;
1646
1001
  let launchResolvedExtensions = step.launchResolvedExtensions;
1647
1002
  const modelAttempts: ModelAttempt[] = [];
1648
- const writerProcesses: PiWriterProcessInstanceExitV1[] = [];
1649
- let writerAttemptCount = 0;
1650
1003
  const attemptNotes: string[] = [];
1651
1004
  let finalRequiredOutputMissing: boolean | undefined;
1652
1005
  const eventsPath = path.join(path.dirname(ctx.outputFile), "events.jsonl");
1653
- let finalResult: RunPiStreamingResult | undefined;
1006
+ let finalResult: RunChildSessionResult | undefined;
1654
1007
  let finalOutputSnapshot: SingleOutputSnapshot | undefined;
1655
1008
  let structuredAcceptanceReport: unknown;
1656
1009
  let structuredAcceptanceReportError: string | undefined;
@@ -1662,10 +1015,6 @@ async function runSingleStepInner(
1662
1015
  let finalMutationEvidence = collectTrackedMutationEvidence(mutationSnapshot, step.cwd ?? ctx.cwd);
1663
1016
 
1664
1017
  let modelIndex = 0;
1665
- let startupAttemptIndex = 0;
1666
- // Escalated to "file" after an unexplained zero-activity startup failure so
1667
- // retries keep the task text out of argv (endpoint pre-exec scans may deny it).
1668
- let taskDeliveryOverride: SubagentTaskDelivery | undefined;
1669
1018
  let contextOverflow = false;
1670
1019
  let launchWarningsEmitted = false;
1671
1020
  let abortRecoveryAttempted = false;
@@ -1677,7 +1026,7 @@ async function runSingleStepInner(
1677
1026
  const candidate = candidates[modelIndex];
1678
1027
  const expectedModelForVerification = candidate && !(step.skipPrimaryModelVerification && modelIndex === 0) ? candidate : undefined;
1679
1028
  try {
1680
- 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 });
1681
1030
  } catch (error) {
1682
1031
  const message = error instanceof Error ? error.message : String(error);
1683
1032
  return omitUndefinedProperties({ agent: step.agent, output: message, error: message, exitCode: 1, context: step.context, thinkingCeiling: step.thinkingCeiling });
@@ -1704,12 +1053,10 @@ async function runSingleStepInner(
1704
1053
  childIndex: ctx.flatIndex,
1705
1054
  })
1706
1055
  : undefined;
1707
- const { args, env, tempDir, toolDiagnosticPath, runtimeAcknowledgedExtensionsPath, capabilityAudit: attemptCapabilityAudit, warnings } = buildPiArgs(omitUndefinedProperties({
1056
+ let watchdogSink: ((event: ChildWatchdogStatusEvent) => void) | undefined;
1057
+ const launch = buildInProcessChildLaunch(omitUndefinedProperties({
1708
1058
  parentSessionId: step.parentSessionId,
1709
1059
  forkCacheKey: step.context === "fork" ? deriveForkPromptCacheKey(step.parentSessionId) : undefined,
1710
- baseArgs: ["--mode", "json", "-p"],
1711
- task: attemptTask,
1712
- taskDelivery: taskDeliveryOverride,
1713
1060
  sessionEnabled,
1714
1061
  sessionDir,
1715
1062
  sessionFile: step.sessionFile,
@@ -1728,28 +1075,20 @@ async function runSingleStepInner(
1728
1075
  systemPrompt: step.systemPrompt ?? "",
1729
1076
  systemPromptMode: step.systemPromptMode,
1730
1077
  mcpDirectTools: step.mcpDirectTools,
1731
- mcpConfig: step.mcpConfig,
1732
- runtimeServerNames: step.runtimeServerNames,
1078
+ extensionBindings,
1733
1079
  capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
1734
1080
  cwd: step.cwd ?? ctx.cwd,
1735
- promptFileStem: step.agent,
1736
1081
  intercomSessionName: ctx.childIntercomTarget,
1737
1082
  sessionName: childSessionName,
1738
1083
  orchestratorIntercomTarget: ctx.orchestratorIntercomTarget,
1739
1084
  runId: ctx.id,
1740
1085
  childAgentName: step.agent,
1741
1086
  childIndex: ctx.flatIndex,
1742
- parentEventSink: ctx.nestedRoute?.eventSink,
1743
- parentControlInbox: ctx.nestedRoute?.controlInbox,
1744
- parentRootRunId: ctx.nestedRoute?.rootRunId,
1745
- parentCapabilityToken: ctx.nestedRoute?.capabilityToken,
1087
+ nestedRoute: ctx.nestedRoute,
1746
1088
  runFanoutBudget: ctx.runFanoutBudget ? {
1747
1089
  ...ctx.runFanoutBudget,
1748
1090
  ...(step.runFanoutPath ? { parentPath: `${ctx.runFanoutBudget.parentPath ? `${ctx.runFanoutBudget.parentPath}/` : ""}${step.runFanoutPath}` } : {}),
1749
1091
  } : undefined,
1750
- steerInboxDir: ctx.steerInboxDir,
1751
- steerCapabilityPath: ctx.steerCapabilityPath,
1752
- steerAckDir: ctx.steerAckDir,
1753
1092
  structuredOutput: effectiveStructuredOutput,
1754
1093
  toolBudget: step.toolBudget,
1755
1094
  permissionRules: step.permissionRules,
@@ -1757,11 +1096,19 @@ async function runSingleStepInner(
1757
1096
  ? path.join(ctx.artifactsDir, "permission-audit", `${ctx.id}-${ctx.flatIndex}.jsonl`)
1758
1097
  : undefined,
1759
1098
  childWatchdog,
1099
+ watchdogStatus: (event) => watchdogSink?.(event),
1760
1100
  waitToolEnabled: step.waitToolEnabled,
1761
1101
  waitToolDefaultTimeoutMs: step.waitToolDefaultTimeoutMs,
1762
1102
  thinkingCeiling: step.thinkingCeiling,
1763
- extensionBindings,
1103
+ maxSubagentDepth: step.maxSubagentDepth,
1104
+ inherited: ctx.inheritedChildRuntime,
1105
+ host: "runner",
1764
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;
1765
1112
  if (!launchWarningsEmitted && warnings.length > 0) {
1766
1113
  for (const warning of warnings) console.warn(`[pi-subagents] ${warning}`);
1767
1114
  launchWarningsEmitted = true;
@@ -1777,13 +1124,11 @@ async function runSingleStepInner(
1777
1124
  model: step.model,
1778
1125
  modelCandidates: step.modelCandidates,
1779
1126
  mcpDirectTools: step.mcpDirectTools,
1780
- mcpConfig: step.mcpConfig,
1781
- runtimeServerNames: step.runtimeServerNames,
1782
1127
  cwd: step.cwd ?? ctx.cwd,
1783
1128
  requireReadTool: Boolean(step.skills?.length),
1784
1129
  structuredOutput: Boolean(effectiveStructuredOutput),
1785
1130
  capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
1786
- inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
1131
+ inheritedCapabilityCeiling: ctx.inheritedChildRuntime?.capabilityCeiling,
1787
1132
  permissionRules: step.permissionRules,
1788
1133
  }));
1789
1134
  launchResolvedExtensions = projectLaunchResolvedChildExtensions(toolPlan);
@@ -1812,59 +1157,49 @@ async function runSingleStepInner(
1812
1157
  }));
1813
1158
  }
1814
1159
  capabilityAudit = attemptCapabilityAudit;
1815
- writerAttemptCount += 1;
1816
- const run = await runPiStreaming(
1817
- args,
1818
- step.cwd ?? ctx.cwd,
1819
- ctx.outputFile,
1820
- env,
1821
- ctx.piPackageRoot,
1822
- ctx.piArgv1,
1823
- step.maxSubagentDepth,
1824
- { eventsPath, runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
1825
- ctx.registerInterrupt,
1826
- 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,
1827
1186
  transcriptWriter,
1828
- ctx.registerTimeout,
1829
- ctx.timeoutMessage,
1830
- ctx.registerStop,
1831
- ctx.stopMessage,
1832
- ctx.onWriterProcess,
1833
- ctx.toolTimeoutMs,
1834
- ctx.deadlineAt,
1835
- ctx.orcaProgressTab,
1187
+ toolTimeoutMs: ctx.toolTimeoutMs,
1188
+ runDeadlineAt: ctx.deadlineAt,
1836
1189
  expectedModelForVerification,
1837
- step.modelVerificationRegistry,
1838
- step.mutationTools,
1839
- );
1840
- if (run.processCloseObservedAt !== undefined) {
1841
- writerProcesses.push({
1842
- processInstanceId: run.processInstanceId,
1843
- kind: "pi-writer",
1844
- attempt: writerAttemptCount - 1,
1845
- closeObservedAt: run.processCloseObservedAt,
1846
- exitCode: run.exitCode,
1847
- signal: run.processSignal ?? null,
1848
- processTree: run.processTree,
1849
- });
1850
- }
1851
- const toolAvailabilityError = run.exitCode === 0 && !run.error
1852
- ? readChildToolDiagnosticError(toolDiagnosticPath)
1853
- : undefined;
1854
- const runtimeAcknowledgedExtensions = readRuntimeAcknowledgedExtensions(runtimeAcknowledgedExtensionsPath);
1855
- 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();
1856
1196
  const midToolExitError = run.currentTool
1857
1197
  && isOrdinaryToolForMidToolExit(run.currentTool)
1858
1198
  && !run.interrupted
1859
1199
  && !run.timedOut
1860
1200
  && !run.stopped
1861
- && !run.protocolError
1862
1201
  && !toolAvailabilityError
1863
- ? formatMidToolExitError({
1864
- toolName: run.currentTool,
1865
- exitCode: run.exitCode,
1866
- processSignal: run.processSignal,
1867
- })
1202
+ ? formatMidToolExitError({ toolName: run.currentTool })
1868
1203
  : undefined;
1869
1204
 
1870
1205
  let structuredOutput: unknown;
@@ -1951,19 +1286,10 @@ async function runSingleStepInner(
1951
1286
  : run.error && run.exitCode === 0
1952
1287
  ? 1
1953
1288
  : run.exitCode;
1954
- const signalError = run.exitCode !== 0 && isUnexplainedProcessSignal(omitUndefinedProperties({
1955
- processSignal: run.processSignal,
1956
- interrupted: run.interrupted,
1957
- timedOut: run.timedOut,
1958
- stopped: run.stopped,
1959
- })) ? formatProcessSignalError(run.processSignal!) : undefined;
1960
1289
  const underlyingError = toolAvailabilityError
1961
1290
  ?? midToolExitError
1962
1291
  ?? structuredError
1963
1292
  ?? run.error
1964
- ?? signalError
1965
- ?? (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined)
1966
- ?? ((emptyOutputError || missingRequiredOutputError) && run.stderr.trim() ? run.stderr.trim() : undefined)
1967
1293
  ?? emptyOutputError
1968
1294
  ?? (missingRequiredOutputAfterMutation ? missingRequiredOutputError : undefined)
1969
1295
  ?? (hiddenError?.hasError
@@ -1971,13 +1297,7 @@ async function runSingleStepInner(
1971
1297
  ? `${hiddenError.errorType} failed (exit ${effectiveExitCode}): ${hiddenError.details}`
1972
1298
  : `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
1973
1299
  : undefined);
1974
- const error = formatSubagentExtensionConflictError(
1975
- underlyingError ?? missingRequiredOutputError ?? completionEvidence.legacyFailureError,
1976
- {
1977
- agent: step.agent,
1978
- ambientExtensionsEnabled: launchResolvedExtensions?.disableAmbientExtensions === false,
1979
- },
1980
- );
1300
+ const error = underlyingError ?? missingRequiredOutputError ?? completionEvidence.legacyFailureError;
1981
1301
  const attempt: ModelAttempt = omitUndefinedProperties({
1982
1302
  model: candidate ?? run.model ?? step.model ?? "default",
1983
1303
  success: effectiveExitCode === 0 && !error,
@@ -1986,7 +1306,7 @@ async function runSingleStepInner(
1986
1306
  usage: run.usage,
1987
1307
  });
1988
1308
  modelAttempts.push(attempt);
1989
- if (!recoveringAbort && candidate && startupAttemptIndex === 0) attemptedModels.push(candidate);
1309
+ if (!recoveringAbort && candidate) attemptedModels.push(candidate);
1990
1310
  completionGuardTriggeredFinal = completionEvidence.guardTriggered && !underlyingError && !missingRequiredOutputError;
1991
1311
  finalOutputSnapshot = outputSnapshot;
1992
1312
  if (step.toolBudget) {
@@ -2003,11 +1323,10 @@ async function runSingleStepInner(
2003
1323
  afterCompactionSettlement: run.afterCompactionSettlement === true,
2004
1324
  });
2005
1325
  const fileMutationEffect = completionEvidence.fileMutation ?? (missingRequiredOutputAfterMutation ? { status: "observed" as const, expected: completionEvidence.mutationExpected, attempted: true, evidence: mutationEvidence } : undefined);
2006
- 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;
2007
1327
  const abortRecovery = !attempt.success ? planAbortRecovery({
2008
1328
  messages: run.messages,
2009
1329
  error,
2010
- processSignal: run.processSignal,
2011
1330
  sessionAvailable: Boolean(step.sessionFile && fs.existsSync(step.sessionFile)),
2012
1331
  alreadyResumed: abortRecoveryAttempted,
2013
1332
  stopped: run.stopped || ctx.stopSignal?.aborted || ctx.skipAcceptance?.(),
@@ -2038,50 +1357,6 @@ async function runSingleStepInner(
2038
1357
  }
2039
1358
  if (completionEvidence.guardTriggered) break modelAttemptsLoop;
2040
1359
 
2041
- const startupFailure = isRetryableSubagentStartupFailure(omitUndefinedProperties({
2042
- exitCode: effectiveExitCode,
2043
- error,
2044
- finalOutput: run.finalOutput,
2045
- messageCount: run.messages.length,
2046
- toolCount: run.toolCount,
2047
- usage: run.usage,
2048
- durationMs: run.durationMs,
2049
- protocolError: run.protocolError,
2050
- processSignal: run.processSignal,
2051
- observedMutationAttempt: run.observedMutationAttempt,
2052
- interrupted: run.interrupted,
2053
- timedOut: run.timedOut,
2054
- stopped: run.stopped,
2055
- }));
2056
- const retryDelayMs = SUBAGENT_STARTUP_RETRY_DELAYS_MS[startupAttemptIndex];
2057
- if (startupFailure && retryDelayMs !== undefined) {
2058
- const retryNote = formatSubagentStartupRetryNote({
2059
- model: attempt.model,
2060
- attempt: startupAttemptIndex + 1,
2061
- maxAttempts: SUBAGENT_STARTUP_RETRY_DELAYS_MS.length + 1,
2062
- delayMs: retryDelayMs,
2063
- });
2064
- const shouldRetry = await waitForSubagentStartupRetry(retryDelayMs, [ctx.timeoutSignal, ctx.stopSignal]);
2065
- if (!shouldRetry || ctx.skipAcceptance?.()) break modelAttemptsLoop;
2066
- if (!taskDeliveryOverride && run.processSignal === "SIGKILL") {
2067
- taskDeliveryOverride = "file";
2068
- attemptNotes.push("[startup-retry] retrying with file task delivery to keep the task text out of the child process argv.");
2069
- }
2070
- attempt.error = retryNote;
2071
- attemptNotes.push(retryNote);
2072
- startupAttemptIndex += 1;
2073
- continue;
2074
- }
2075
- if (startupFailure) {
2076
- const startupError = formatSubagentStartupRetryExhaustedError({
2077
- model: attempt.model,
2078
- attempts: startupAttemptIndex + 1,
2079
- });
2080
- attempt.error = startupError;
2081
- finalResult.error = startupError;
2082
- finalResult.finalOutput = startupError;
2083
- break modelAttemptsLoop;
2084
- }
2085
1360
  const retryableModelFailure = isRetryableModelFailureAttempt({ error, messages: run.messages, toolCount: run.toolCount });
2086
1361
  if (retryableModelFailure) recordRetryableModelFailure(candidate ?? run.model ?? step.model, error);
2087
1362
  if (isContextOverflow(error)) {
@@ -2092,7 +1367,6 @@ async function runSingleStepInner(
2092
1367
  if (!retryableModelFailure || modelIndex === candidates.length - 1) break modelAttemptsLoop;
2093
1368
  attemptNotes.push(formatModelAttemptNote(attempt, candidates[modelIndex + 1]));
2094
1369
  modelIndex += 1;
2095
- startupAttemptIndex = 0;
2096
1370
  }
2097
1371
 
2098
1372
  const rawOutput = finalResult?.finalOutput ?? "";
@@ -2120,7 +1394,7 @@ async function runSingleStepInner(
2120
1394
  ? extractChildWrittenOutput(finalResult?.messages, step.outputPath, step.cwd ?? ctx.cwd)
2121
1395
  : undefined;
2122
1396
  const outputState: SubagentOutputState = finalResult?.outputState === "present"
2123
- || (finalResult as (RunPiStreamingResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput !== undefined
1397
+ || (finalResult as (RunChildSessionResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput !== undefined
2124
1398
  || Boolean(childWrittenOutput?.trim())
2125
1399
  ? "present"
2126
1400
  : resolvedOutput.savedPath
@@ -2167,6 +1441,7 @@ async function runSingleStepInner(
2167
1441
  reportOptional: isAgentContractV1(step.agentContract),
2168
1442
  artifactsDir: ctx.artifactsDir,
2169
1443
  runId: ctx.id,
1444
+ watchdog: finalResult?.watchdog,
2170
1445
  }))
2171
1446
  : undefined;
2172
1447
  const stoppedAfterAcceptance = finalResult?.stopped === true || ctx.stopSignal?.aborted === true;
@@ -2221,7 +1496,7 @@ async function runSingleStepInner(
2221
1496
  ...(capabilityAudit ? { capabilityCeiling: capabilityAudit.ceiling, capabilityAudit } : {}),
2222
1497
  launchContractDigest: actualLaunchContractDigest,
2223
1498
  launchResolvedExtensions,
2224
- ...((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 } : {}),
2225
1500
  ...(transcriptWriter ? { transcriptPath: artifactPaths.transcriptPath } : {}),
2226
1501
  transcriptError: transcriptWriter?.getError(),
2227
1502
  skills: step.skills,
@@ -2240,10 +1515,10 @@ async function runSingleStepInner(
2240
1515
  outputState,
2241
1516
  exitCode: effectiveFinalExitCode,
2242
1517
  error: effectiveFinalError,
2243
- protocolError: finalResult?.protocolError,
2244
1518
  sessionFile: step.sessionFile,
2245
1519
  intercomTarget: ctx.childIntercomTarget,
2246
1520
  model: finalResult?.model,
1521
+ thinking: resolveEffectiveThinking(finalResult?.model, step.thinking),
2247
1522
  attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
2248
1523
  modelAttempts,
2249
1524
  contextOverflow: contextOverflow || undefined,
@@ -2257,22 +1532,19 @@ async function runSingleStepInner(
2257
1532
  interrupted: timedOutAfterAcceptance || stoppedAfterAcceptance ? false : finalResult?.interrupted,
2258
1533
  timedOut: timedOutAfterAcceptance ? true : finalResult?.timedOut,
2259
1534
  stopped: stoppedAfterAcceptance ? true : finalResult?.stopped,
2260
- processSignal: finalResult?.processSignal,
2261
1535
  timeoutRecovery,
2262
1536
  toolBudget,
2263
1537
  toolBudgetBlocked: toolBudgetBlocked || undefined,
2264
1538
  completionGuardTriggered: completionGuardTriggeredFinal,
2265
- ...((finalResult as (RunPiStreamingResult & { effects?: import("../../shared/types.ts").EffectsProjection }) | undefined)?.effects ? { effects: (finalResult as RunPiStreamingResult & { effects?: import("../../shared/types.ts").EffectsProjection }).effects } : {}),
2266
- 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,
2267
1541
  structuredOutputPath: timedOutAfterAcceptance || stoppedAfterAcceptance ? undefined : effectiveStructuredOutput?.outputPath,
2268
1542
  structuredOutputSchemaPath: timedOutAfterAcceptance || stoppedAfterAcceptance ? undefined : effectiveStructuredOutput?.schemaPath,
2269
1543
  acceptance: effectiveAcceptance,
2270
1544
  watchdog: finalResult?.watchdog,
2271
1545
  ...(capabilityAudit ? { capabilityCeiling: capabilityAudit.ceiling, capabilityAudit } : {}),
2272
1546
  launchResolvedExtensions,
2273
- ...((finalResult as (RunPiStreamingResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }) | undefined)?.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: (finalResult as RunPiStreamingResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }).runtimeAcknowledgedExtensions } : {}),
2274
- writerProcesses,
2275
- writerAttemptCount,
1547
+ ...((finalResult as (RunChildSessionResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }) | undefined)?.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: (finalResult as RunChildSessionResult & { runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1 }).runtimeAcknowledgedExtensions } : {}),
2276
1548
  });
2277
1549
  return isAgentContractV1(step.agentContract) ? attachContractProjections(result as unknown as import("../../shared/types.ts").SingleResult) as unknown as typeof result : result;
2278
1550
  }
@@ -2566,7 +1838,7 @@ async function runSingleStepWithTimeout(
2566
1838
 
2567
1839
  async function runSubagent(
2568
1840
  config: SubagentRunConfig,
2569
- onWriterProcess?: (writer: { state: "none" | "spawning" } | { state: "running"; pid: number }) => void,
1841
+ childSessions: ChildSessionFactory,
2570
1842
  ): Promise<void> {
2571
1843
  const { id, steps, resultPath, cwd, placeholder, taskIndex, totalTasks, maxOutput, artifactsDir, artifactConfig } =
2572
1844
  config;
@@ -2586,9 +1858,11 @@ async function runSubagent(
2586
1858
  const activeChildInterrupts = new Map<number, () => void>();
2587
1859
  const activeChildTimeouts = new Map<number, () => void>();
2588
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[]>();
2589
1864
  const childStopRequests = new Map<number, { childId: string; requestedAt: number }>();
2590
1865
  const pendingStepSteers: SteerRequest[] = [];
2591
- const steeringCapabilities = new Map<number, SteerCapability>();
2592
1866
  let interrupted = false;
2593
1867
  let currentActivityState: ActivityState | undefined;
2594
1868
  let activityTimer: NodeJS.Timeout | undefined;
@@ -2905,6 +2179,7 @@ async function runSubagent(
2905
2179
  success: statusResultSuccess(state, step),
2906
2180
  sessionFile: step.sessionFile,
2907
2181
  model: step.model,
2182
+ thinking: step.thinking,
2908
2183
  attemptedModels: step.attemptedModels,
2909
2184
  modelAttempts: step.modelAttempts,
2910
2185
  usage: usageFromAttempts(step.modelAttempts),
@@ -3045,6 +2320,16 @@ async function runSubagent(
3045
2320
  activeChildStops.set(flatIndex, stop);
3046
2321
  if (stopped || childStopRequests.has(flatIndex)) stop();
3047
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
+ };
3048
2333
  const interruptActiveChildren = (): void => {
3049
2334
  for (const interrupt of [...activeChildInterrupts.values()]) interrupt();
3050
2335
  };
@@ -3497,22 +2782,15 @@ async function runSubagent(
3497
2782
  if (!step) return { index, state: "failed" as const, reason: "child index out of range" };
3498
2783
  if (step.status === "pending") return { index, state: "scheduled" as const };
3499
2784
  if (step.status !== "running") return { index, state: "failed" as const, reason: `child is ${step.status}` };
3500
- if (steeringCapabilities.get(index)?.supported === false) return { index, state: "failed" as const, reason: "child Pi session does not support steering" };
3501
2785
  return { index, state: "routed" as const };
3502
2786
  });
3503
2787
  recordSteeringLifecycle(request, targetStates);
3504
2788
  emitSteeringEvent("subagent.steer.requested", request, undefined, { targets: targetStates });
3505
2789
  for (const target of targetStates) {
3506
2790
  if (target.state === "routed") {
3507
- try {
3508
- enqueueStepSteer(asyncDir, target.index, request);
3509
- updateSteeringLifecycleTarget(request.id, target.index, "routed", now);
3510
- emitSteeringEvent("subagent.steer.routed", request, target.index);
3511
- } catch (error) {
3512
- markSteeringAttention(target.index);
3513
- updateSteeringLifecycleTarget(request.id, target.index, "failed", now, { reason: error instanceof Error ? error.message : String(error) });
3514
- emitSteeringEvent("subagent.steer.failed", request, target.index, { reason: error instanceof Error ? error.message : String(error) });
3515
- }
2791
+ updateSteeringLifecycleTarget(request.id, target.index, "routed", now);
2792
+ emitSteeringEvent("subagent.steer.routed", request, target.index);
2793
+ steerLiveChild(target.index, request);
3516
2794
  } else if (target.state === "failed") {
3517
2795
  markSteeringAttention(target.index);
3518
2796
  emitSteeringEvent("subagent.steer.failed", request, target.index, { reason: target.reason });
@@ -3524,27 +2802,42 @@ async function runSubagent(
3524
2802
  statusPayload.lastUpdate = now;
3525
2803
  writeStatusPayload();
3526
2804
  };
3527
- 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 => {
3528
2807
  const lifecycle = steeringStatus(statusPayload);
3529
- const request = lifecycle.recent.find((candidate) => candidate.id === ack.requestId);
3530
- if (!request || !request.targets.some((target) => target.index === ack.index)) return;
3531
- 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));
3532
2811
  const now = Date.now();
3533
- if (ack.state === "delivered") {
3534
- updateSteeringLifecycleTarget(ack.requestId, ack.index, late ? "late" : "delivered", now, omitUndefinedProperties({ reason: late ? "acknowledged after recovery commit" : undefined }));
3535
- emitSteeringEvent("subagent.steer.delivered", { type: "steer", id: ack.requestId, ts: now, message: ack.message }, ack.index, { late, deliveryStatus: "delivered", message: ack.message });
3536
- } else if (ack.state === "queued") {
3537
- updateSteeringLifecycleTarget(ack.requestId, ack.index, "queued", now);
3538
- 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 });
3539
2818
  } else {
3540
- markSteeringAttention(ack.index);
3541
- updateSteeringLifecycleTarget(ack.requestId, ack.index, "failed", now, { reason: ack.message });
3542
- 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 });
3543
2822
  }
3544
- emitTerminalSteeringNotice(ack.requestId, `Steering failed for run ${id}: ${ack.message}`);
2823
+ emitTerminalSteeringNotice(requestId, `Steering failed for run ${id}: ${delivery.message}`);
3545
2824
  statusPayload.lastUpdate = now;
3546
2825
  writeStatusPayload();
3547
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
+ };
3548
2841
  const flushPendingStepSteers = (flatIndex: number): void => {
3549
2842
  const remaining: SteerRequest[] = [];
3550
2843
  for (const request of pendingStepSteers.splice(0)) {
@@ -3584,6 +2877,15 @@ async function runSubagent(
3584
2877
  writeStatusPayload(false);
3585
2878
  return;
3586
2879
  }
2880
+ if (event.type === "message_end") {
2881
+ const next = applyChildWatchdogMessage(step.watchdog, event.message, now);
2882
+ if (next) step.watchdog = next;
2883
+ if (next && (event.message as { role?: unknown } | undefined)?.role === "custom") {
2884
+ statusPayload.lastUpdate = now;
2885
+ writeStatusPayload(false);
2886
+ return;
2887
+ }
2888
+ }
3587
2889
  if (event.type === "tool_execution_start" && event.toolName) {
3588
2890
  const mutates = isMutatingTool(event.toolName, event.args, flatSteps[flatIndex]?.mutationTools);
3589
2891
  const currentPath = resolveCurrentPath(event.toolName, event.args);
@@ -3619,6 +2921,7 @@ async function runSubagent(
3619
2921
  tokens: step.tokens?.total,
3620
2922
  toolCount: step.toolCount,
3621
2923
  currentTool: step.currentTool,
2924
+ toolCallId: event.toolCallId,
3622
2925
  currentToolDurationMs: 0,
3623
2926
  currentPath: step.currentPath,
3624
2927
  })));
@@ -3728,6 +3031,7 @@ async function runSubagent(
3728
3031
  config: controlConfig,
3729
3032
  startedAt: step.startedAt ?? overallStartTime,
3730
3033
  lastActivityAt,
3034
+ turnCount: step.turnCount,
3731
3035
  currentTool: step.currentTool,
3732
3036
  thinking: step.thinking,
3733
3037
  now,
@@ -3892,23 +3196,6 @@ async function runSubagent(
3892
3196
  pendingStepSteers.push(request);
3893
3197
  }
3894
3198
  },
3895
- onSteerCapability: (capability) => {
3896
- steeringCapabilities.set(capability.index, capability);
3897
- if (!capability.supported) {
3898
- const now = Date.now();
3899
- const lifecycle = steeringStatus(statusPayload);
3900
- for (const request of lifecycle.recent) {
3901
- if (!request.targets.some((target) => target.index === capability.index && (target.state === "routed" || target.state === "scheduled"))) continue;
3902
- markSteeringAttention(capability.index);
3903
- updateSteeringLifecycleTarget(request.id, capability.index, "failed", now, { reason: "child Pi session does not support steering" });
3904
- 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" });
3905
- emitTerminalSteeringNotice(request.id, `Steering failed for run ${id}: child ${capability.index} does not support steering.`);
3906
- }
3907
- statusPayload.lastUpdate = now;
3908
- writeStatusPayload();
3909
- }
3910
- },
3911
- onSteerAck: consumeSteerAck,
3912
3199
  });
3913
3200
  if (config.deadlineAt !== undefined) {
3914
3201
  const remainingMs = Math.max(0, config.deadlineAt - Date.now());
@@ -3965,7 +3252,7 @@ async function runSubagent(
3965
3252
  : [undefined]
3966
3253
  : model ? [model] : [undefined];
3967
3254
  for (const candidate of candidates) {
3968
- 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 });
3969
3256
  }
3970
3257
  }
3971
3258
  if (materialized.collectedOnEmpty) await validateDynamicCollection(step.collect.outputSchema, materialized.collectedOnEmpty);
@@ -4235,11 +3522,9 @@ async function runSubagent(
4235
3522
  artifactsDir, artifactConfig, id,
4236
3523
  flatIndex: fi, flatStepCount: Math.max(statusPayload.steps.length, 1),
4237
3524
  outputFile: path.join(asyncDir, `output-${fi}.log`),
4238
- steerInboxDir: stepSteerInboxDir(asyncDir, fi),
4239
- steerCapabilityPath: steerCapabilityPath(asyncDir, fi),
4240
- steerAckDir: steerAcksDir(asyncDir, fi),
4241
3525
  piPackageRoot: config.piPackageRoot,
4242
- piArgv1: config.piArgv1,
3526
+ childSessions,
3527
+ inheritedChildRuntime: config.inheritedChildRuntime,
4243
3528
  childIntercomTarget: config.childIntercomTargets?.[fi],
4244
3529
  orchestratorIntercomTarget: config.controlIntercomTarget,
4245
3530
  nestedRoute: config.nestedRoute,
@@ -4248,6 +3533,7 @@ async function runSubagent(
4248
3533
  registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
4249
3534
  registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
4250
3535
  registerStop: (stop) => registerStepStop(fi, stop),
3536
+ registerSteer: (steer) => registerStepSteer(fi, steer),
4251
3537
  timeoutSignal: timeoutAbortController.signal,
4252
3538
  stopSignal: stopAbortController.signal,
4253
3539
  trackedMutationEvidenceForCompletionGuard: false,
@@ -4256,7 +3542,6 @@ async function runSubagent(
4256
3542
  toolTimeoutMs: task.toolTimeoutMs ?? config.toolTimeoutMs,
4257
3543
  onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking, attempt.contextLimit),
4258
3544
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
4259
- onWriterProcess,
4260
3545
  onExternalProcess: (process) => updateExternalProcess(fi, process),
4261
3546
  onExternalJob: (externalJob) => updateExternalJob(fi, externalJob),
4262
3547
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(fi),
@@ -4337,7 +3622,6 @@ async function runSubagent(
4337
3622
  output: pr.output,
4338
3623
  outputState: pr.outputState,
4339
3624
  error: pr.error,
4340
- protocolError: pr.protocolError,
4341
3625
  success: pr.stopped !== true && pr.interrupted !== true && pr.exitCode === 0,
4342
3626
  exitCode: pr.interrupted === true ? 0 : pr.exitCode,
4343
3627
  skipped: pr.skipped,
@@ -4349,6 +3633,7 @@ async function runSubagent(
4349
3633
  sessionFile: pr.sessionFile,
4350
3634
  intercomTarget: pr.intercomTarget,
4351
3635
  model: pr.model,
3636
+ thinking: pr.thinking,
4352
3637
  attemptedModels: pr.attemptedModels,
4353
3638
  modelAttempts: pr.modelAttempts,
4354
3639
  contextOverflow: pr.contextOverflow,
@@ -4496,6 +3781,7 @@ async function runSubagent(
4496
3781
  labels: group.parallel.map((task) => task.lane?.key ?? config.workflowKey ?? task.outputName ?? task.label),
4497
3782
  tasks: group.parallel.map((task) => task.task),
4498
3783
  provider: config.worktreeProvider,
3784
+ baseRef: config.baseRef,
4499
3785
  branchPrefix: config.worktreeBranchPrefix,
4500
3786
  setupHook: config.worktreeSetupHook
4501
3787
  ? omitUndefinedProperties({ hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs })
@@ -4633,11 +3919,9 @@ async function runSubagent(
4633
3919
  artifactsDir, artifactConfig, id,
4634
3920
  flatIndex: fi, flatStepCount: Math.max(statusPayload.steps.length, 1),
4635
3921
  outputFile: path.join(asyncDir, `output-${fi}.log`),
4636
- steerInboxDir: stepSteerInboxDir(asyncDir, fi),
4637
- steerCapabilityPath: steerCapabilityPath(asyncDir, fi),
4638
- steerAckDir: steerAcksDir(asyncDir, fi),
4639
3922
  piPackageRoot: config.piPackageRoot,
4640
- piArgv1: config.piArgv1,
3923
+ childSessions,
3924
+ inheritedChildRuntime: config.inheritedChildRuntime,
4641
3925
  childIntercomTarget: config.childIntercomTargets?.[fi],
4642
3926
  orchestratorIntercomTarget: config.controlIntercomTarget,
4643
3927
  nestedRoute: config.nestedRoute,
@@ -4646,6 +3930,7 @@ async function runSubagent(
4646
3930
  registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
4647
3931
  registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
4648
3932
  registerStop: (stop) => registerStepStop(fi, stop),
3933
+ registerSteer: (steer) => registerStepSteer(fi, steer),
4649
3934
  timeoutSignal: timeoutAbortController.signal,
4650
3935
  stopSignal: stopAbortController.signal,
4651
3936
  trackedMutationEvidenceForCompletionGuard: Boolean(worktreeSetup),
@@ -4654,7 +3939,6 @@ async function runSubagent(
4654
3939
  toolTimeoutMs: taskForRun.toolTimeoutMs ?? config.toolTimeoutMs,
4655
3940
  onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking, attempt.contextLimit),
4656
3941
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
4657
- onWriterProcess,
4658
3942
  onExternalProcess: (process) => updateExternalProcess(fi, process),
4659
3943
  onExternalJob: (externalJob) => updateExternalJob(fi, externalJob),
4660
3944
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(fi),
@@ -4782,7 +4066,6 @@ async function runSubagent(
4782
4066
  output: pr.output,
4783
4067
  outputState: pr.outputState,
4784
4068
  error: pr.error,
4785
- protocolError: pr.protocolError,
4786
4069
  success: pr.stopped !== true && pr.interrupted !== true && pr.exitCode === 0,
4787
4070
  exitCode: pr.interrupted === true ? 0 : pr.exitCode,
4788
4071
  skipped: pr.skipped,
@@ -4794,6 +4077,7 @@ async function runSubagent(
4794
4077
  sessionFile: pr.sessionFile,
4795
4078
  intercomTarget: pr.intercomTarget,
4796
4079
  model: pr.model,
4080
+ thinking: pr.thinking,
4797
4081
  attemptedModels: pr.attemptedModels,
4798
4082
  modelAttempts: pr.modelAttempts,
4799
4083
  contextOverflow: pr.contextOverflow,
@@ -4932,6 +4216,7 @@ async function runSubagent(
4932
4216
  labels: [seqStep.lane?.key ?? config.workflowKey ?? seqStep.outputName ?? seqStep.label],
4933
4217
  tasks: [seqStep.task],
4934
4218
  provider: config.worktreeProvider,
4219
+ baseRef: config.baseRef,
4935
4220
  branchPrefix: config.worktreeBranchPrefix,
4936
4221
  setupHook: config.worktreeSetupHook
4937
4222
  ? omitUndefinedProperties({ hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs })
@@ -4999,11 +4284,9 @@ async function runSubagent(
4999
4284
  artifactsDir, artifactConfig, id,
5000
4285
  flatIndex, flatStepCount: Math.max(statusPayload.steps.length, 1),
5001
4286
  outputFile: path.join(asyncDir, `output-${flatIndex}.log`),
5002
- steerInboxDir: stepSteerInboxDir(asyncDir, flatIndex),
5003
- steerCapabilityPath: steerCapabilityPath(asyncDir, flatIndex),
5004
- steerAckDir: steerAcksDir(asyncDir, flatIndex),
5005
4287
  piPackageRoot: config.piPackageRoot,
5006
- piArgv1: config.piArgv1,
4288
+ childSessions,
4289
+ inheritedChildRuntime: config.inheritedChildRuntime,
5007
4290
  childIntercomTarget: config.childIntercomTargets?.[flatIndex],
5008
4291
  orchestratorIntercomTarget: config.controlIntercomTarget,
5009
4292
  nestedRoute: config.nestedRoute,
@@ -5012,6 +4295,7 @@ async function runSubagent(
5012
4295
  registerInterrupt: (interrupt) => registerStepInterrupt(flatIndex, interrupt),
5013
4296
  registerTimeout: (interrupt) => registerStepTimeout(flatIndex, interrupt),
5014
4297
  registerStop: (stop) => registerStepStop(flatIndex, stop),
4298
+ registerSteer: (steer) => registerStepSteer(flatIndex, steer),
5015
4299
  timeoutSignal: timeoutAbortController.signal,
5016
4300
  stopSignal: stopAbortController.signal,
5017
4301
  timeoutMessage,
@@ -5019,7 +4303,6 @@ async function runSubagent(
5019
4303
  toolTimeoutMs: seqStep.toolTimeoutMs ?? config.toolTimeoutMs,
5020
4304
  onAttemptStart: (attempt) => updateStepModel(flatIndex, attempt.model, attempt.thinking, attempt.contextLimit),
5021
4305
  onChildEvent: (event) => updateStepFromChildEvent(flatIndex, event),
5022
- onWriterProcess,
5023
4306
  onExternalProcess: (process) => updateExternalProcess(flatIndex, process),
5024
4307
  onExternalJob: (externalJob) => updateExternalJob(flatIndex, externalJob),
5025
4308
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(flatIndex),
@@ -5047,12 +4330,12 @@ async function runSubagent(
5047
4330
  output: stopped || childStopped ? stopMessage : timedOut ? singleResult.output || (timeoutMessage ?? "Subagent timed out.") : singleResult.output,
5048
4331
  outputState: singleResult.outputState,
5049
4332
  error: stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error,
5050
- protocolError: singleResult.protocolError,
5051
4333
  success: !stopped && !childStopped && !timedOut && singleResult.interrupted !== true && singleResult.exitCode === 0,
5052
4334
  exitCode: stopped || childStopped ? 1 : timedOut ? 1 : singleResult.interrupted === true ? 0 : singleResult.exitCode,
5053
4335
  sessionFile: singleResult.sessionFile,
5054
4336
  intercomTarget: singleResult.intercomTarget,
5055
4337
  model: singleResult.model,
4338
+ thinking: singleResult.thinking,
5056
4339
  attemptedModels: singleResult.attemptedModels,
5057
4340
  modelAttempts: singleResult.modelAttempts,
5058
4341
  contextOverflow: singleResult.contextOverflow,
@@ -5314,7 +4597,7 @@ async function runSubagent(
5314
4597
  clearTimeout(timeoutTimer);
5315
4598
  timeoutTimer = undefined;
5316
4599
  }
5317
- 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))) {
5318
4601
  timedOut = true;
5319
4602
  }
5320
4603
  const signalTerminated = !stopped && !timedOut && !interrupted && results.some((result) => result.exitCode !== 0 && isUnexplainedProcessSignal(omitUndefinedProperties({
@@ -5404,7 +4687,6 @@ async function runSubagent(
5404
4687
  output: r.output,
5405
4688
  outputState: r.outputState,
5406
4689
  error: r.error,
5407
- protocolError: r.protocolError,
5408
4690
  success: r.success,
5409
4691
  skipped: r.skipped || undefined,
5410
4692
  interrupted: r.interrupted || undefined,
@@ -5416,6 +4698,7 @@ async function runSubagent(
5416
4698
  sessionFile: r.sessionFile,
5417
4699
  intercomTarget: r.intercomTarget,
5418
4700
  model: r.model,
4701
+ thinking: r.thinking,
5419
4702
  attemptedModels: r.attemptedModels,
5420
4703
  modelAttempts: r.modelAttempts,
5421
4704
  contextOverflow: r.contextOverflow,
@@ -5485,7 +4768,7 @@ async function runSubagent(
5485
4768
  statusPayload.lastUpdate = Date.now();
5486
4769
  }
5487
4770
  writeStatusPayload();
5488
- 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);
5489
4772
  appendJsonl(
5490
4773
  eventsPath,
5491
4774
  JSON.stringify({
@@ -5520,14 +4803,21 @@ async function runSubagent(
5520
4803
  }), (filePath, content) => runPersistence.write(filePath, { content }, (_path, payload) => {
5521
4804
  fs.writeFileSync(_path, (payload as { content: string }).content, "utf-8");
5522
4805
  }));
5523
- if (runPersistence.pendingCount() === 0) runPersistence.dispose();
5524
- 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();
5525
4814
  if (config.runnerProcessInstanceId) {
4815
+ // Children run inside this process, so no step has writer processes to prove terminal.
5526
4816
  const writers: Record<string, PiWriterProcessInstanceExitV1[]> = {};
5527
4817
  const expectedWriters: Record<string, number> = {};
5528
- for (const [index, result] of results.entries()) {
5529
- writers[String(index)] = result.writerProcesses ?? [];
5530
- expectedWriters[String(index)] = result.writerAttemptCount ?? 0;
4818
+ for (const index of results.keys()) {
4819
+ writers[String(index)] = [];
4820
+ expectedWriters[String(index)] = 0;
5531
4821
  }
5532
4822
  const candidate: ProcessTerminalCandidate = {
5533
4823
  version: 1,
@@ -5609,7 +4899,16 @@ async function runConfiguredSubagent(config: SubagentRunConfig): Promise<void> {
5609
4899
  }
5610
4900
  }
5611
4901
  }
5612
- 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
+ }
5613
4912
  } catch (error) {
5614
4913
  if (!startupCommitted) {
5615
4914
  try {
@@ -5638,10 +4937,16 @@ async function runConfiguredSubagent(config: SubagentRunConfig): Promise<void> {
5638
4937
  }
5639
4938
 
5640
4939
  function startConfiguredSubagent(config: SubagentRunConfig): void {
5641
- runConfiguredSubagent(config).catch((runErr) => {
5642
- console.error("Subagent runner error:", runErr);
5643
- process.exit(1);
5644
- });
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
+ );
5645
4950
  }
5646
4951
 
5647
4952
  const configArg = process.argv[2];