pi-subagents 0.66.0 → 0.68.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 (172) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/README.md +5 -4
  3. package/agents/evidence-auditor.md +34 -0
  4. package/agents/reviewer.md +3 -2
  5. package/docs/agents.md +43 -15
  6. package/docs/configuration.md +67 -23
  7. package/docs/extension-api.md +38 -19
  8. package/docs/missions.md +10 -2
  9. package/docs/models.md +11 -79
  10. package/docs/observability.md +22 -12
  11. package/docs/standalone-background.md +59 -0
  12. package/docs/tool-reference.md +33 -20
  13. package/docs/watchdog.md +39 -10
  14. package/docs/workflows.md +37 -13
  15. package/index.ts +5 -2
  16. package/inspector-runner.mjs +2 -2
  17. package/package.json +4 -2
  18. package/prompts/parallel-review.md +1 -1
  19. package/runner-peer-loader.mjs +24 -0
  20. package/runner-peer-preload.mjs +32 -0
  21. package/skills/pi-subagents/SKILL.md +32 -21
  22. package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
  23. package/skills/pi-subagents/references/execution-controls.md +11 -9
  24. package/skills/pi-subagents/references/management-authoring-rpc.md +0 -1
  25. package/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
  26. package/skills/pi-subagents/references/prompting-and-roles.md +17 -13
  27. package/skills/pi-subagents/references/review-and-validation.md +3 -3
  28. package/src/agents/advertised-agent-prompt.ts +34 -3
  29. package/src/agents/agent-management.ts +57 -58
  30. package/src/agents/agent-serializer.ts +4 -3
  31. package/src/agents/agents.ts +190 -71
  32. package/src/agents/builtin-names.ts +1 -0
  33. package/src/agents/chain-serializer.ts +5 -0
  34. package/src/agents/runtime-agent-registry.ts +7 -6
  35. package/src/api/delegation.ts +4 -0
  36. package/src/api/preflight.ts +94 -59
  37. package/src/api/required-child-extensions.ts +6 -0
  38. package/src/api/shared-types.ts +2 -0
  39. package/src/extension/config.ts +10 -37
  40. package/src/extension/fanout-child.ts +66 -4
  41. package/src/extension/herdr-pi-bridge.ts +160 -0
  42. package/src/extension/index.ts +62 -39
  43. package/src/extension/public-execution.ts +7 -5
  44. package/src/extension/rpc.ts +4 -0
  45. package/src/extension/schemas.ts +81 -81
  46. package/src/extension/tool-description.ts +30 -82
  47. package/src/inspectors/actions.ts +148 -0
  48. package/src/inspectors/ghostty/actions.ts +74 -0
  49. package/src/inspectors/ghostty/plugin.ts +17 -0
  50. package/src/inspectors/herdr/actions.ts +99 -179
  51. package/src/inspectors/herdr/plugin.ts +20 -0
  52. package/src/inspectors/herdr/project-panes.ts +1 -1
  53. package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
  54. package/src/inspectors/plugins.ts +8 -0
  55. package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
  56. package/src/inspectors/types.ts +51 -0
  57. package/src/intercom/intercom-bridge.ts +50 -8
  58. package/src/intercom/native-supervisor-channel.ts +44 -31
  59. package/src/policy/authority.ts +4 -0
  60. package/src/profiles/profiles.ts +12 -6
  61. package/src/runs/background/active-async-capacity.ts +4 -0
  62. package/src/runs/background/active-run-index.ts +17 -1
  63. package/src/runs/background/async-execution.ts +348 -176
  64. package/src/runs/background/async-job-tracker.ts +8 -6
  65. package/src/runs/background/async-resume.ts +17 -12
  66. package/src/runs/background/async-status.ts +15 -4
  67. package/src/runs/background/auto-drain.ts +23 -10
  68. package/src/runs/background/binary-bootstrap.ts +38 -0
  69. package/src/runs/background/chain-append.ts +1 -1
  70. package/src/runs/background/chain-root-attachment.ts +14 -33
  71. package/src/runs/background/fleet-view.ts +30 -2
  72. package/src/runs/background/notify.ts +105 -7
  73. package/src/runs/background/owned-process-tree.ts +29 -2
  74. package/src/runs/background/result-files.ts +8 -4
  75. package/src/runs/background/result-watcher.ts +19 -2
  76. package/src/runs/background/run-child-session.ts +81 -33
  77. package/src/runs/background/run-status.ts +3 -0
  78. package/src/runs/background/runner-aliases.ts +12 -33
  79. package/src/runs/background/runner-child-launch.ts +6 -1
  80. package/src/runs/background/runner-child-sessions.ts +5 -4
  81. package/src/runs/background/runner-http-dispatcher.ts +119 -0
  82. package/src/runs/background/scheduled-runs.ts +51 -18
  83. package/src/runs/background/stale-run-reconciler.ts +35 -11
  84. package/src/runs/background/steering.ts +20 -2
  85. package/src/runs/background/subagent-runner.ts +441 -304
  86. package/src/runs/background/subagent-wait.ts +176 -28
  87. package/src/runs/background/wait-completions.ts +75 -27
  88. package/src/runs/background/wait-subscriptions.ts +9 -3
  89. package/src/runs/background/wait-tool.ts +5 -3
  90. package/src/runs/foreground/async-steering-action.ts +18 -7
  91. package/src/runs/foreground/async-stop-action.ts +93 -3
  92. package/src/runs/foreground/execution.ts +134 -248
  93. package/src/runs/foreground/foreground-history.ts +2 -1
  94. package/src/runs/foreground/prompt-audit.ts +3 -1
  95. package/src/runs/foreground/subagent-executor.ts +374 -178
  96. package/src/runs/foreground/workflow-detach-reconcile.ts +2 -0
  97. package/src/runs/foreground/workflow-foreground-steering.ts +2 -1
  98. package/src/runs/shared/acceptance.ts +38 -11
  99. package/src/runs/shared/async-status-projection.ts +127 -33
  100. package/src/runs/shared/capability-ceiling.ts +2 -0
  101. package/src/runs/shared/child-hooks.ts +25 -10
  102. package/src/runs/shared/child-launch-plan.ts +15 -3
  103. package/src/runs/shared/child-launch.ts +28 -5
  104. package/src/runs/shared/child-lifecycle.ts +6 -3
  105. package/src/runs/shared/child-runtime-config.ts +8 -1
  106. package/src/runs/shared/child-session.ts +127 -52
  107. package/src/runs/shared/child-tool-plan.ts +142 -11
  108. package/src/runs/shared/completion-guard.ts +5 -3
  109. package/src/runs/shared/dynamic-fanout.ts +2 -2
  110. package/src/runs/shared/effective-system-prompt.ts +33 -0
  111. package/src/runs/shared/external-cli-contract.ts +11 -1
  112. package/src/runs/shared/external-cli-preflight.ts +6 -2
  113. package/src/runs/shared/external-cli-runner.ts +9 -7
  114. package/src/runs/shared/herdr-connection.ts +134 -0
  115. package/src/runs/shared/herdr-external-adapters.ts +169 -0
  116. package/src/runs/shared/herdr-machine.ts +279 -0
  117. package/src/runs/shared/herdr-pi-protocol.ts +59 -0
  118. package/src/runs/shared/herdr-placed-run.ts +263 -0
  119. package/src/runs/shared/llm-intent-arbiter.ts +12 -3
  120. package/src/runs/shared/model-resolution-diagnostic.ts +76 -0
  121. package/src/runs/shared/{model-fallback.ts → model-resolution.ts} +22 -235
  122. package/src/runs/shared/model-scope.ts +1 -1
  123. package/src/runs/shared/nested-events.ts +11 -2
  124. package/src/runs/shared/orca-progress-tabs.ts +1 -1
  125. package/src/runs/shared/parallel-utils.ts +7 -2
  126. package/src/runs/shared/pi-spawn.ts +10 -0
  127. package/src/runs/shared/subagent-prompt-runtime.ts +12 -4
  128. package/src/runs/shared/task-intent.ts +46 -13
  129. package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
  130. package/src/runs/shared/worktree-setup-command.ts +27 -4
  131. package/src/runs/shared/worktree.ts +45 -15
  132. package/src/shared/child-cache-retention.ts +43 -0
  133. package/src/shared/fork-context.ts +15 -72
  134. package/src/shared/launch-contract.ts +68 -8
  135. package/src/shared/opencode-session-headers.ts +30 -0
  136. package/src/shared/pruned-fork.ts +1 -1
  137. package/src/shared/required-child-extensions.ts +81 -0
  138. package/src/shared/settings.ts +5 -2
  139. package/src/shared/shortcuts.ts +0 -4
  140. package/src/shared/types.ts +74 -30
  141. package/src/slash/delegation-adapters.ts +3 -1
  142. package/src/slash/delegation-request.ts +14 -0
  143. package/src/slash/slash-commands.ts +2 -7
  144. package/src/slash/subagents-admin.ts +24 -13
  145. package/src/tui/fleet-status.ts +164 -19
  146. package/src/tui/fleet.ts +16 -14
  147. package/src/tui/render.ts +168 -37
  148. package/src/watchdog/child-status.ts +28 -28
  149. package/src/watchdog/lsp-diagnostics.ts +1 -1
  150. package/src/watchdog/model-selection.ts +21 -1
  151. package/src/watchdog/permission-arbiter.ts +3 -1
  152. package/src/watchdog/register-child.ts +10 -2
  153. package/src/watchdog/register-main.ts +39 -35
  154. package/src/watchdog/render.ts +1 -1
  155. package/src/watchdog/review.ts +123 -74
  156. package/src/watchdog/rules.ts +1 -1
  157. package/src/watchdog/runtime.ts +100 -27
  158. package/src/watchdog/scope.ts +1 -1
  159. package/src/watchdog/settings.ts +3 -0
  160. package/src/watchdog/tool-actions.ts +13 -12
  161. package/src/watchdog/turn-delta.ts +23 -0
  162. package/src/watchdog/types.ts +5 -3
  163. package/src/watchdog/warning-format.ts +1 -1
  164. package/src/workflows/scripted-workflow.ts +279 -10
  165. package/src/workflows/workflow-checklist.ts +2 -2
  166. package/src/workflows/workflow-receipt.ts +21 -3
  167. package/src/workflows/workflow-resources.ts +13 -2
  168. package/runner-server-preload.mjs +0 -13
  169. package/src/runs/shared/model-exclusions.ts +0 -374
  170. package/src/runs/shared/readonly-model-continuation.ts +0 -69
  171. package/src/runs/shared/readonly-session-evidence.ts +0 -307
  172. /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
@@ -1,28 +1,15 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { EventEmitter } from "node:events";
3
2
  import * as fs from "node:fs";
4
- import { createRequire } from "node:module";
5
3
  import * as path from "node:path";
4
+ import { performance } from "node:perf_hooks";
6
5
  import { pathToFileURL } from "node:url";
7
6
  import type { Message } from "@earendil-works/pi-ai";
8
7
  import { arbitrateCompletionGuardRescue, createTaskMutationArbiter } from "../shared/llm-intent-arbiter.ts";
8
+ import { installRunnerHttpDispatcher } from "./runner-http-dispatcher.ts";
9
9
 
10
- // Detached runners skip Pi's CLI proxy setup. Keep fetch on the same Undici dispatcher.
11
- function ensureProxyAwareHttpDispatcher(): void {
12
- try {
13
- // SAFETY: require loads the pinned direct dependency described by these types.
14
- const undici = createRequire(import.meta.url)("undici") as typeof import("undici");
15
- const dispatcher = new undici.EnvHttpProxyAgent({ allowH2: false });
16
- // Fetch rejects stream errors; the listener prevents an unhandled EventEmitter error.
17
- EventEmitter.prototype.on.call(dispatcher, "error", () => {});
18
- undici.setGlobalDispatcher(dispatcher);
19
- undici.install();
20
- } catch (error) {
21
- console.error(`[pi-subagents] proxy-aware HTTP dispatcher not installed: ${error instanceof Error ? error.message : String(error)}`);
22
- }
23
- }
24
10
  const isRunnerEntrypoint = Boolean(process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href);
25
- if (isRunnerEntrypoint) ensureProxyAwareHttpDispatcher();
11
+ // Detached Node runners skip Pi's CLI dispatcher setup; install the runner's own.
12
+ if (isRunnerEntrypoint) installRunnerHttpDispatcher({ agentDir: getAgentDir(), cwd: process.cwd() });
26
13
  import { writeAtomicJson } from "../../shared/atomic-json.ts";
27
14
  import { writeAsyncResultFile, writePendingAsyncResultFile } from "./result-files.ts";
28
15
  import { createFileCoalescer } from "../../shared/file-coalescer.ts";
@@ -30,11 +17,12 @@ import { createCapacityResilientJsonWriter } from "../../shared/capacity-resilie
30
17
  import { isStorageCapacityError } from "../../shared/file-system-retry.ts";
31
18
  import { updateActiveRunIndex } from "./active-run-index.ts";
32
19
  import { createChildTranscriptWriter, type ChildTranscriptWriter } from "../../shared/child-transcript.ts";
33
- import { closeSteerInbox, consumeInterruptRequest, consumeSteerRequests, deliverInterruptRequest, deliverStopRequest, deliverTimeoutRequest, watchAsyncControlInbox, type SteerRequest, type StopRequest } from "./control-channel.ts";
20
+ import { closeSteerInbox, consumeInterruptRequest, consumeSteerRequests, consumeStopRequestPayloads, deliverInterruptRequest, deliverStopRequest, deliverTimeoutRequest, watchAsyncControlInbox, type SteerRequest, type StopRequest } from "./control-channel.ts";
34
21
  import { appendJsonl as appendRawJsonl, formatOutputArtifactContent, getArtifactPaths, writeArtifact, writeMetadata } from "../../shared/artifacts.ts";
35
22
  import { PI_CODING_AGENT_PACKAGE, resolveInstalledPiPackageRoot } from "../shared/pi-spawn.ts";
36
23
  import { preflightLaunchCwd } from "../shared/launch-cwd.ts";
37
24
  import { captureSingleOutputSnapshot, extractChildWrittenOutput, finalizeSingleOutput, formatSavedOutputReference, injectOutputPathSystemPrompt, injectSingleOutputInstruction, resolveSingleOutput, type SingleOutputSnapshot } from "../shared/single-output.ts";
25
+ import { runSetupCommand } from "../shared/worktree-setup-command.ts";
38
26
  import {
39
27
  type ActivityState,
40
28
  type ArtifactConfig,
@@ -49,7 +37,6 @@ import {
49
37
  type CostSummary,
50
38
  type LaunchResolvedChildExtensions,
51
39
  type RuntimeAcknowledgedChildExtensions,
52
- type ModelAttempt,
53
40
  type PiWriterProcessInstanceExit,
54
41
  type NestedRouteInfo,
55
42
  type NestedRunSummary,
@@ -66,6 +53,7 @@ import {
66
53
  type SteeringTargetStatus,
67
54
  type SubagentChildStatusEvent,
68
55
  type WorkflowLaneMetadata,
56
+ type HerdrMachineReference,
69
57
  DEFAULT_MAX_OUTPUT,
70
58
  type MaxOutputConfig,
71
59
  SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
@@ -96,10 +84,8 @@ import { applyThinkingSuffix, projectLaunchResolvedChildExtensions, resolvePiLau
96
84
  import type { InheritedChildRuntime } from "../shared/child-launch.ts";
97
85
  import { buildRunnerChildLaunch } from "./runner-child-launch.ts";
98
86
  import { normalizeExtensionBindings } from "../shared/extension-bindings.ts";
99
- import type { ChildSessionFactory } from "../shared/child-session.ts";
100
- import { getSettledReadonlyChild, runChildSession, type ChildEvent, type RunChildSessionInput, type RunChildSessionResult, type StepSteerHandler } from "./run-child-session.ts";
101
- import { planReadonlyModelContinuation, READONLY_CONTINUATION_PROMPT, type LogicalRecoveryState } from "../shared/readonly-model-continuation.ts";
102
- import { getReadonlySessionEvidence } from "../shared/readonly-session-evidence.ts";
87
+ import type { ChildSessionFactory, DefaultChildSessionFactoryOptions } from "../shared/child-session.ts";
88
+ import { runChildSession, type ChildEvent, type RunChildSessionInput, type RunChildSessionResult, type SteerDelivery, type StepSteerHandler } from "./run-child-session.ts";
103
89
  import { loadRunnerChildSessionFactory } from "./runner-child-sessions.ts";
104
90
  import { SUBAGENT_CHILD_ENV } from "../shared/child-runtime-config.ts";
105
91
  import { deriveChildSessionName } from "../../shared/child-session-name.ts";
@@ -112,10 +98,10 @@ import { buildTimeoutRecoverySummary, collectTrackedMutationEvidence, snapshotTr
112
98
  import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection } from "../shared/dynamic-fanout.ts";
113
99
  import { claimRunFanoutBatch, getRunFanoutBudgetSnapshot } from "../shared/run-fanout-budget.ts";
114
100
  import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDir, writeNestedEvent } from "../shared/nested-events.ts";
115
- import { formatModelAttemptNote, formatSubagentModelVerificationError, isContextOverflow, isRetryableModelFailureAttempt, recordRetryableModelFailure } from "../shared/model-fallback.ts";
101
+ import { formatSubagentModelVerificationError, isContextOverflow } from "../shared/model-resolution.ts";
116
102
  import { markProcessTerminalCandidateLeaseRelease, processTerminalPath, writeProcessTerminalCandidate, type ProcessTerminalCandidate } from "./process-terminal.ts";
117
- import { createSteeringStatus, recordSteeringRequest, steeringStatus, terminalSteeringNoticeState, updateSteeringTarget } from "./steering.ts";
118
- import { PROMPT_REDACTED, detectSubagentError, extractTextFromContent, extractToolArgsPreview, formatEmptyTerminalAssistantResponseError, getFinalOutput, hasEmptyTerminalAssistantResponse, readStatus } from "../../shared/utils.ts";
103
+ import { createSteeringStatus, recordSteeringRequest, steeringStatus, terminalSteeringNoticeState, unconsumedSteerReason, updateSteeringTarget } from "./steering.ts";
104
+ import { PROMPT_REDACTED, detectSubagentError, extractTextFromContent, extractToolArgsPreview, formatEmptyTerminalAssistantResponseError, getAgentDir, getFinalOutput, hasEmptyTerminalAssistantResponse, readStatus } from "../../shared/utils.ts";
119
105
  import { evaluateCompletionMutationGuard, expectsImplementationMutation, hasMutationToolCapability, validateImplementationToolContract } from "../shared/completion-guard.ts";
120
106
  import { planCompletionEvidence, projectSettlementDiagnostic } from "../shared/completion-evidence.ts";
121
107
  import { planAbortRecovery } from "../shared/abort-recovery.ts";
@@ -147,7 +133,7 @@ import {
147
133
  } from "../shared/worktree.ts";
148
134
  import { findModelInfo, resolveEffectiveThinking, splitKnownThinkingSuffix } from "../../shared/model-info.ts";
149
135
  import { assertThinkingWithinCeiling } from "../../shared/thinking-ceiling.ts";
150
- import { launchBindingDigest } from "../../shared/launch-contract.ts";
136
+ import { resolveLaunchBinding } from "../../shared/launch-contract.ts";
151
137
  import { writeInitialProgressFile } from "../../shared/settings.ts";
152
138
  import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
153
139
  import { acceptanceFailureMessage, aggregateAcceptanceReport, buildSkippedAcceptanceLedger, evaluateAcceptance, formatAcceptancePrompt, resolveAcceptanceReportMode, resolveEffectiveAcceptance, stripAcceptanceReport } from "../shared/acceptance.ts";
@@ -166,6 +152,8 @@ import { resolveClaudeCodeLaunch } from "../shared/claude-code-adapter.ts";
166
152
  import { resolveCodexExecLaunch } from "../shared/codex-exec-adapter.ts";
167
153
  import { resolveCursorAgentLaunch } from "../shared/cursor-agent-adapter.ts";
168
154
  import { resolveExternalCliRunnerStatus } from "../shared/external-cli-contract.ts";
155
+ import { formatHerdrMachineHint, prepareHerdrMachineExternalCliRun } from "../shared/herdr-machine.ts";
156
+ import { HerdrExternalNeedsAttentionError, createHerdrExternalAdapter, prepareInternalHerdrExternalAdapter, type HerdrExternalAdapterId, type HerdrExternalResult } from "../shared/herdr-external-adapters.ts";
169
157
  import { runExternalJob } from "../shared/external-job-runner.ts";
170
158
  import { createOrcaProgressTab, type OrcaProgressTab } from "../shared/orca-progress-tabs.ts";
171
159
  import type { ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
@@ -184,7 +172,7 @@ const INTERCOM_DETACH_RECEIPT = "Detached for intercom coordination before task
184
172
  // child host.
185
173
  process.env[SUBAGENT_CHILD_ENV] = "1";
186
174
 
187
- interface SubagentRunConfig {
175
+ export interface SubagentRunConfig {
188
176
  id: string;
189
177
  steps: RunnerStep[];
190
178
  resultPath: string;
@@ -224,6 +212,8 @@ interface SubagentRunConfig {
224
212
  deadlineAt?: number;
225
213
  /** Resolved configured hard per-tool-call timeout (ms); fast tools still have a default when undefined. */
226
214
  toolTimeoutMs?: number;
215
+ /** Steer the running steps to checkpoint and stop this many ms before `deadlineAt`; absent = no checkpoint steer. */
216
+ checkpointBeforeDeadlineMs?: number;
227
217
  toolBudget?: ResolvedToolBudget;
228
218
  usageBudget?: UsageBudgetConfig;
229
219
  revivalLease?: SessionLeaseRequest;
@@ -232,6 +222,8 @@ interface SubagentRunConfig {
232
222
  globalConcurrencyLimit?: number;
233
223
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
234
224
  runFanoutBudget?: RunFanoutBudgetDescriptor;
225
+ /** Builtin tool names the host runtime provides; used to intersect agent-declared tools. */
226
+ hostAvailableBuiltins?: readonly string[];
235
227
  launchContractDigest?: string;
236
228
  launchResolvedExtensions?: LaunchResolvedChildExtensions;
237
229
  runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensions;
@@ -270,14 +262,15 @@ interface StepResult {
270
262
  sessionFile?: string;
271
263
  intercomTarget?: string;
272
264
  model?: string;
265
+ nativeMachine?: import("../../shared/types.ts").SingleResult["nativeMachine"];
273
266
  thinking?: string;
274
- attemptedModels?: string[];
275
- modelAttempts?: ModelAttempt[];
267
+ requestedModel?: string;
276
268
  /** True when the dispatch failed because the input exceeded the model's context window. */
277
269
  contextOverflow?: boolean;
278
270
  totalCost?: CostSummary;
279
271
  artifactPaths?: ArtifactPaths;
280
272
  outputSaveError?: string;
273
+ artifactOutputSaveFailed?: true;
281
274
  metadataSaveError?: string;
282
275
  truncated?: boolean;
283
276
  transcriptPath?: string;
@@ -420,50 +413,22 @@ function emptyUsage(): Usage {
420
413
  return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
421
414
  }
422
415
 
423
- function tokenUsageFromAttempts(attempts: ModelAttempt[] | undefined): TokenUsage | null {
424
- if (!attempts || attempts.length === 0) return null;
425
- let input = 0;
426
- let output = 0;
427
- for (const attempt of attempts) {
428
- input += attempt.usage?.input ?? 0;
429
- output += attempt.usage?.output ?? 0;
430
- }
416
+ function tokenUsageFromUsage(usage: Usage | undefined): TokenUsage | null {
417
+ const input = usage?.input ?? 0;
418
+ const output = usage?.output ?? 0;
431
419
  const total = input + output;
432
420
  return total > 0 ? { input, output, total } : null;
433
421
  }
434
422
 
435
- function costSummaryFromAttempts(attempts: ModelAttempt[] | undefined): CostSummary | undefined {
436
- if (!attempts || attempts.length === 0) return undefined;
437
- let inputTokens = 0;
438
- let outputTokens = 0;
439
- let costUsd = 0;
440
- for (const attempt of attempts) {
441
- inputTokens += attempt.usage?.input ?? 0;
442
- outputTokens += attempt.usage?.output ?? 0;
443
- costUsd += attempt.usage?.cost ?? 0;
444
- }
423
+ function costSummaryFromUsage(usage: Usage | undefined): CostSummary | undefined {
424
+ const inputTokens = usage?.input ?? 0;
425
+ const outputTokens = usage?.output ?? 0;
426
+ const costUsd = usage?.cost ?? 0;
445
427
  return inputTokens > 0 || outputTokens > 0 || costUsd > 0
446
428
  ? { inputTokens, outputTokens, costUsd }
447
429
  : undefined;
448
430
  }
449
431
 
450
- function usageFromAttempts(attempts: ModelAttempt[] | undefined): Usage | undefined {
451
- if (!attempts || attempts.length === 0) return undefined;
452
- const usage = emptyUsage();
453
- for (const attempt of attempts) {
454
- if (!attempt.usage) continue;
455
- usage.input += attempt.usage.input;
456
- usage.output += attempt.usage.output;
457
- usage.cacheRead += attempt.usage.cacheRead;
458
- usage.cacheWrite += attempt.usage.cacheWrite;
459
- usage.cost += attempt.usage.cost;
460
- usage.turns += attempt.usage.turns;
461
- }
462
- return usage.input !== 0 || usage.output !== 0 || usage.cacheRead !== 0 || usage.cacheWrite !== 0 || usage.cost !== 0 || usage.turns !== 0
463
- ? usage
464
- : undefined;
465
- }
466
-
467
432
  function appendRecentStepOutput(step: RunnerStatusStep, lines: string[]): void {
468
433
  const nonEmpty = lines.filter((line) => line.trim());
469
434
  if (nonEmpty.length === 0) return;
@@ -657,6 +622,38 @@ function writeRunLog(
657
622
  write(logPath, lines.join("\n"));
658
623
  }
659
624
 
625
+ function expectedMissingGitEvidence(error: unknown): boolean {
626
+ if (typeof (error as { code?: unknown })?.code !== "number") return false;
627
+ const stderr = (error as { stderr?: unknown }).stderr;
628
+ const detail = Buffer.isBuffer(stderr) ? stderr.toString("utf-8") : String(stderr ?? "");
629
+ return /not a git repository|Needed a single revision/i.test(detail);
630
+ }
631
+
632
+ async function readGitFingerprint(cwd: string, signal: AbortSignal, onError: (error: unknown) => void): Promise<string | undefined> {
633
+ try {
634
+ const runGit = async (args: string[]) => {
635
+ const result = await runSetupCommand("git", args, {
636
+ cwd, signal, maxBuffer: 16 * 1024 * 1024, deadlineAt: Date.now() + 30_000,
637
+ acceptedExitCodes: Array.from({ length: 256 }, (_, code) => code),
638
+ });
639
+ if (result.error) throw result.error;
640
+ if (result.status !== 0) {
641
+ throw Object.assign(new Error(result.stderr || `git exited with ${result.status}`), { code: result.status, stderr: result.stderr });
642
+ }
643
+ return result.stdoutBuffer;
644
+ };
645
+ const headOutput = await runGit(["rev-parse", "--verify", "HEAD"]);
646
+ const head = headOutput.toString("utf-8").trim();
647
+ if (!head) return undefined;
648
+ const status = await runGit(["status", "--porcelain=v1", "-z", "--untracked-files=normal"]);
649
+ return `${head}\0${status.toString("base64")}`;
650
+ } catch (error) {
651
+ if ((error as NodeJS.ErrnoException)?.code === "PROCESS_TREE_UNVERIFIED") throw error;
652
+ if (!signal.aborted && !expectedMissingGitEvidence(error)) onError(error);
653
+ return undefined;
654
+ }
655
+ }
656
+
660
657
  /** Context for running a single step */
661
658
  interface SingleStepContext {
662
659
  previousOutput: string;
@@ -682,6 +679,8 @@ interface SingleStepContext {
682
679
  registerStop?: (stop: (() => void) | undefined) => void;
683
680
  /** Receives the live child's steer handler while its session runs. */
684
681
  registerSteer?: (steer: StepSteerHandler | undefined) => void;
682
+ /** Reports a live child's later steer consumption or unconsumed settlement. */
683
+ onSteerOutcome?: (request: SteerRequest, delivery: SteerDelivery) => void;
685
684
  timeoutSignal?: AbortSignal;
686
685
  stopSignal?: AbortSignal;
687
686
  timeoutMessage?: string;
@@ -695,9 +694,12 @@ interface SingleStepContext {
695
694
  nestedRoute?: NestedRouteInfo;
696
695
  capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
697
696
  runFanoutBudget?: RunFanoutBudgetDescriptor;
697
+ hostAvailableBuiltins?: readonly string[];
698
698
  onAttemptStart?: (attempt: { model?: string; thinking?: string; contextLimit?: number }) => void;
699
699
  onChildEvent?: (event: ChildEvent) => void;
700
700
  onExternalProcess?: (process: ExternalProcessStatus) => void;
701
+ prepareExternalActivity?: (cwd: string, signal: AbortSignal) => Promise<void>;
702
+ onExternalStreamActivity?: () => void;
701
703
  onExternalJob?: (status: ExternalJobStatus) => void;
702
704
  skipAcceptance?: () => boolean;
703
705
  /** Authoritative owner decision after event delivery; undefined includes incomplete run-wide usage. */
@@ -709,7 +711,26 @@ interface SingleStepContext {
709
711
  orcaProgressTab?: OrcaProgressTab;
710
712
  }
711
713
 
712
- /** Run a single pi agent step, returning output and metadata */
714
+ /** Machine runs: a one-line hint for predictable remote failures, and a note that writer changes live on the machine. */
715
+ function decorateHerdrMachineResult<T extends { output: string; exitCode: number | null; error?: string; externalProcess: { stderrPath: string } }>(result: T, machine: HerdrMachineReference, adapter: string | undefined): T {
716
+ let stderrTail = "";
717
+ try {
718
+ const stderr = fs.readFileSync(result.externalProcess.stderrPath, "utf-8");
719
+ stderrTail = stderr.slice(-4096);
720
+ } catch { /* stderr log is best-effort evidence. */ }
721
+ const hint = result.exitCode === 0 ? undefined : formatHerdrMachineHint(machine, `${result.error ?? ""}\n${stderrTail}\nexit code ${result.exitCode}`);
722
+ const error = hint ? `${result.error ?? `Remote command exited with code ${result.exitCode}.`}\n${hint}` : result.error;
723
+ const note = result.exitCode === 0 && adapter?.endsWith("-writer") ? `Changes made by this run live on ${machine.label ?? machine.id} at ${machine.cwd}; the local checkout is unchanged.` : undefined;
724
+ const output = note ? (result.output.trim() ? `${result.output.trimEnd()}\n\n${note}` : note) : result.output;
725
+ return { ...result, output, ...(error !== undefined ? { error } : {}) };
726
+ }
727
+
728
+ export async function settleHerdrExternalRunnerError(error: unknown, adapter: HerdrExternalAdapterId, evidence: Parameters<ReturnType<typeof createHerdrExternalAdapter>["normalize"]>[0], retain: () => Promise<void>): Promise<HerdrExternalResult> {
729
+ await retain();
730
+ if (!(error instanceof HerdrExternalNeedsAttentionError)) throw error;
731
+ return createHerdrExternalAdapter(adapter).normalize(evidence, error.message);
732
+ }
733
+
713
734
  export async function runSingleStepInner(
714
735
  step: SubagentStep,
715
736
  ctx: SingleStepContext,
@@ -768,8 +789,7 @@ export async function runSingleStepInner(
768
789
  sessionFile: imported.sessionFile,
769
790
  intercomTarget: imported.intercomTarget,
770
791
  model: imported.model,
771
- attemptedModels: imported.attemptedModels,
772
- modelAttempts: imported.modelAttempts,
792
+ requestedModel: imported.requestedModel,
773
793
  contextOverflow: imported.contextOverflow,
774
794
  totalCost: imported.totalCost,
775
795
  usage: imported.usage,
@@ -803,14 +823,15 @@ export async function runSingleStepInner(
803
823
  subagentOnlyExtensions: step.subagentOnlyExtensions,
804
824
  fast: step.fast,
805
825
  model: step.model,
806
- modelCandidates: step.modelCandidates,
807
826
  mcpDirectTools: step.mcpDirectTools,
808
827
  cwd: step.cwd ?? ctx.cwd,
809
828
  requireReadTool: Boolean(step.skills?.length),
810
829
  structuredOutput: Boolean(effectiveStructuredOutput),
811
830
  capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
812
831
  inheritedCapabilityCeiling: ctx.inheritedChildRuntime?.capabilityCeiling,
832
+ requiredExtensions: step.requiredExtensions ?? ctx.inheritedChildRuntime?.requiredExtensions,
813
833
  permissionRules: step.permissionRules,
834
+ hostAvailableBuiltins: ctx.hostAvailableBuiltins,
814
835
  }));
815
836
  const contractTools = resolvedTaskToolPlan.explicitToolAllowlist ? resolvedTaskToolPlan.effectiveToolAllowlist : undefined;
816
837
  const contractError = validateImplementationToolContract({
@@ -868,7 +889,53 @@ export async function runSingleStepInner(
868
889
  transcriptWriter?.writeInitialUserMessage(`${PROMPT_REDACTED}; live Prompt Audit only.`);
869
890
 
870
891
  if (step.runner?.type === "external-cli") {
871
- const externalCwd = step.cwd ?? ctx.cwd;
892
+ const externalCwd = step.machine?.cwd ?? step.cwd ?? ctx.cwd;
893
+ const externalAbortSignal = combinedAbortSignal([ctx.timeoutSignal, ctx.stopSignal]);
894
+ if (!step.machine && externalAbortSignal) await ctx.prepareExternalActivity?.(externalCwd, externalAbortSignal);
895
+ if (externalAbortSignal?.aborted) {
896
+ const stopped = ctx.stopSignal?.aborted === true;
897
+ const message = stopped ? ctx.stopMessage ?? "Subagent stopped by user." : ctx.timeoutMessage ?? "Subagent timed out.";
898
+ return omitUndefinedProperties({ agent: step.agent, context: step.context, output: message, error: message, exitCode: 1, stopped: stopped || undefined, timedOut: stopped ? undefined : true });
899
+ }
900
+ if (step.machine) {
901
+ const runner = resolveExternalCliRunnerStatus({ ...step.runner, machine: step.machine });
902
+ const outputSnapshot = captureSingleOutputSnapshot(step.outputPath);
903
+ const placedRunId = `${ctx.id}-${ctx.flatIndex}`;
904
+ const session = await prepareInternalHerdrExternalAdapter({ adapter: step.runner.adapter as HerdrExternalAdapterId, machine: step.machine, runId: placedRunId }, {});
905
+ let timedOut = false, stopped = false, retain = false;
906
+ const evidenceInput = { runId: placedRunId, requestId: `prompt-${ctx.flatIndex}`, task: buildExternalCliPrompt(step.systemPrompt ?? "", task), cwd: externalCwd, nativeSessionId: session.launch.nativeSessionId };
907
+ let resolveInterruption!: (value: "timeout" | "stop") => void, rejectInterruption!: (error: unknown) => void;
908
+ const interruption = new Promise<"timeout" | "stop">((resolve, reject) => { resolveInterruption = resolve; rejectInterruption = reject; });
909
+ ctx.registerTimeout?.(() => { timedOut = true; retain = true; resolveInterruption("timeout"); });
910
+ ctx.registerStop?.(() => { stopped = true; void session.abort().then(() => resolveInterruption("stop"), rejectInterruption); });
911
+ try {
912
+ let settled: HerdrExternalResult;
913
+ try {
914
+ const raced = await Promise.race([session.promptAndSettle(evidenceInput).then((value) => ({ kind: "settled" as const, value })), interruption.then((kind) => ({ kind }))]);
915
+ if (raced.kind === "settled") settled = raced.value;
916
+ else if (raced.kind === "timeout") settled = createHerdrExternalAdapter(step.runner.adapter as HerdrExternalAdapterId).normalize(evidenceInput, "Placed external run timed out; truthful pane retained for inspection.");
917
+ else return { agent: step.agent, context: step.context, output: "Placed external run stopped by user.", outputState: "present", exitCode: 1, stopped: true, runner, execution: { status: "stopped", success: false, exitCode: 1, stopped: true } };
918
+ } catch (error) {
919
+ retain = true;
920
+ settled = await settleHerdrExternalRunnerError(error, step.runner.adapter as HerdrExternalAdapterId, evidenceInput, () => session.retain());
921
+ }
922
+ const output = settled.output;
923
+ try { fs.writeFileSync(ctx.outputFile, output, "utf-8"); } catch { /* Observability output is best-effort. */ }
924
+ const resolvedOutput = step.outputPath ? resolveSingleOutput(step.outputPath, output, outputSnapshot, step.outputClaimPath) : { fullOutput: output };
925
+ const outputReference = resolvedOutput.savedPath ? formatSavedOutputReference(resolvedOutput.savedPath, resolvedOutput.fullOutput) : undefined;
926
+ const finalizedOutput = finalizeSingleOutput(omitUndefinedProperties({ fullOutput: resolvedOutput.fullOutput, outputPath: step.outputPath, outputMode: step.outputMode, exitCode: 1, preserveSavedOutput: true, savedPath: resolvedOutput.savedPath, outputReference, saveError: resolvedOutput.saveError }));
927
+ const artifactErrors = artifactPaths && ctx.artifactConfig?.enabled !== false ? persistStepArtifacts({ artifactPaths, artifactConfig: ctx.artifactConfig, output: formatOutputArtifactContent(omitUndefinedProperties({ output: resolvedOutput.fullOutput, metadataPath: ctx.artifactConfig?.includeMetadata === false ? undefined : artifactPaths.metadataPath })), metadata: { runId: ctx.id, agent: step.agent, task: PROMPT_REDACTED, runner, placement: session.owner.identity, settlement: settled.settlement, outcome: settled.outcome, timestamp: Date.now() } }) : {};
928
+ return omitUndefinedProperties({ agent: step.agent, ...(childSessionName ? { sessionName: childSessionName } : {}), context: step.context, output: finalizedOutput.displayOutput, outputState: output.trim() ? "present" : "absent", exitCode: 1, error: resolvedOutput.fatalError ? resolvedOutput.saveError : undefined, timedOut, stopped, artifactPaths, outputSaveError: [resolvedOutput.saveError, artifactErrors.outputSaveError].filter(Boolean).join("\n") || undefined, metadataSaveError: artifactErrors.metadataSaveError, runner, execution: { status: "partial", success: false, exitCode: 1 } });
929
+ } catch (error) {
930
+ // Any post-allocation uncertainty retains the pane; only explicit stop
931
+ // and successful exact settlement are destructive.
932
+ retain = true;
933
+ throw error;
934
+ } finally {
935
+ ctx.registerTimeout?.(undefined); ctx.registerStop?.(undefined);
936
+ if (retain) await session.retain(); else await session.dispose();
937
+ }
938
+ }
872
939
  const adapterLaunch = step.runner.adapter === "codex-exec" || step.runner.adapter === "codex-exec-writer"
873
940
  ? resolveCodexExecLaunch({ adapter: step.runner.adapter, command: step.runner.command, asyncDir: path.dirname(ctx.outputFile), stepIndex: ctx.flatIndex })
874
941
  : step.runner.adapter === "claude-code" || step.runner.adapter === "claude-code-writer"
@@ -876,9 +943,13 @@ export async function runSingleStepInner(
876
943
  : step.runner.adapter === "cursor-agent" || step.runner.adapter === "cursor-agent-writer"
877
944
  ? resolveCursorAgentLaunch({ adapter: step.runner.adapter, command: step.runner.command, cwd: externalCwd, asyncDir: path.dirname(ctx.outputFile), stepIndex: ctx.flatIndex })
878
945
  : undefined;
879
- const runner = resolveExternalCliRunnerStatus({ ...step.runner, ...(adapterLaunch ? { args: adapterLaunch.args } : {}) });
946
+ const runner = resolveExternalCliRunnerStatus({ ...step.runner, ...(adapterLaunch ? { args: adapterLaunch.args } : {}), ...(step.machine ? { machine: step.machine } : {}) });
880
947
  const outputSnapshot = captureSingleOutputSnapshot(step.outputPath);
881
- const external = await runExternalCli(omitUndefinedProperties({
948
+ const onExternalOutput = (chunk: Buffer): void => {
949
+ if (chunk.length > 0) ctx.onExternalStreamActivity?.();
950
+ ctx.orcaProgressTab?.append(chunk.toString("utf-8"));
951
+ };
952
+ const externalInput = omitUndefinedProperties({
882
953
  command: adapterLaunch?.command ?? runner.command,
883
954
  args: adapterLaunch?.args ?? runner.args,
884
955
  cwd: externalCwd,
@@ -896,9 +967,13 @@ export async function runSingleStepInner(
896
967
  timeoutMessage: ctx.timeoutMessage,
897
968
  stopMessage: ctx.stopMessage,
898
969
  onProcess: ctx.onExternalProcess,
899
- onStdout: (chunk) => ctx.orcaProgressTab?.append(chunk.toString("utf-8")),
900
- onStderr: (chunk) => ctx.orcaProgressTab?.append(chunk.toString("utf-8")),
901
- }));
970
+ onStdout: onExternalOutput,
971
+ onStderr: onExternalOutput,
972
+ });
973
+ const preparedExternal = prepareHerdrMachineExternalCliRun(externalInput, step.machine ? { machine: step.machine, ...(step.machineEnv ? { env: step.machineEnv } : {}) } : undefined, { localCwd: ctx.cwd });
974
+ const ran = await runExternalCli(preparedExternal.input);
975
+ const externalProcess = preparedExternal.decorateProcess(ran.externalProcess);
976
+ const external = step.machine ? decorateHerdrMachineResult(ran, step.machine, step.runner.adapter) : ran;
902
977
  try { fs.writeFileSync(ctx.outputFile, external.output, "utf-8"); } catch { /* Observability output is best-effort. */ }
903
978
  const resolvedOutput = step.outputPath && external.exitCode === 0
904
979
  ? resolveSingleOutput(step.outputPath, external.output, outputSnapshot, step.outputClaimPath)
@@ -922,7 +997,7 @@ export async function runSingleStepInner(
922
997
  artifactPaths,
923
998
  artifactConfig: ctx.artifactConfig,
924
999
  output: formatOutputArtifactContent(omitUndefinedProperties({ output: resolvedOutput.fullOutput, error: external.error, metadataPath: ctx.artifactConfig?.includeMetadata === false ? undefined : artifactPaths.metadataPath })),
925
- metadata: { runId: ctx.id, agent: step.agent, task: PROMPT_REDACTED, runner, externalProcess: external.externalProcess, exitCode: external.exitCode, error: external.error, timestamp: Date.now() },
1000
+ metadata: { runId: ctx.id, agent: step.agent, task: PROMPT_REDACTED, runner, externalProcess: externalProcess, exitCode: external.exitCode, error: external.error, timestamp: Date.now() },
926
1001
  })
927
1002
  : {};
928
1003
  return omitUndefinedProperties({
@@ -938,9 +1013,10 @@ export async function runSingleStepInner(
938
1013
  processSignal: external.processSignal,
939
1014
  artifactPaths,
940
1015
  outputSaveError: [resolvedOutput.saveError, artifactErrors.outputSaveError].filter(Boolean).join("\n") || undefined,
1016
+ artifactOutputSaveFailed: artifactErrors.outputSaveError ? true : undefined,
941
1017
  metadataSaveError: artifactErrors.metadataSaveError,
942
1018
  runner,
943
- externalProcess: external.externalProcess,
1019
+ externalProcess: externalProcess,
944
1020
  });
945
1021
  }
946
1022
 
@@ -1007,6 +1083,7 @@ export async function runSingleStepInner(
1007
1083
  stopped: external.stopped,
1008
1084
  artifactPaths,
1009
1085
  outputSaveError: [resolvedOutput.saveError, artifactErrors.outputSaveError].filter(Boolean).join("\n") || undefined,
1086
+ artifactOutputSaveFailed: artifactErrors.outputSaveError ? true : undefined,
1010
1087
  metadataSaveError: artifactErrors.metadataSaveError,
1011
1088
  runner,
1012
1089
  externalJob: external.externalJob,
@@ -1020,16 +1097,9 @@ export async function runSingleStepInner(
1020
1097
  alignForkedSessionCwd(step.sessionFile, effectiveCwd);
1021
1098
  }
1022
1099
 
1023
- const candidates = step.modelCandidates !== undefined
1024
- ? step.modelCandidates.length > 0 ? step.modelCandidates : [undefined]
1025
- : step.model
1026
- ? [step.model]
1027
- : [undefined];
1028
- const attemptedModels: string[] = [];
1100
+ const candidate = step.model;
1029
1101
  let capabilityAudit: import("../shared/capability-ceiling.ts").SubagentCapabilityAudit | undefined;
1030
1102
  let launchResolvedExtensions = step.launchResolvedExtensions;
1031
- const modelAttempts: ModelAttempt[] = [];
1032
- const attemptNotes: string[] = [];
1033
1103
  let finalRequiredOutputMissing: boolean | undefined;
1034
1104
  const eventsPath = path.join(path.dirname(ctx.outputFile), "events.jsonl");
1035
1105
  let finalResult: RunChildSessionResult | undefined;
@@ -1040,48 +1110,21 @@ export async function runSingleStepInner(
1040
1110
  let toolBudget = step.toolBudget ? initialToolBudgetState(step.toolBudget) : undefined;
1041
1111
  let toolBudgetBlocked = false;
1042
1112
  let actualLaunchContractDigest = step.launchContractDigest;
1043
- const mutationSnapshot = snapshotTrackedMutations(step.cwd ?? ctx.cwd);
1113
+ const mutationSnapshot = step.machine ? { source: "tracked-files" as const, trackedOnly: true as const, cwd: step.cwd ?? ctx.cwd, dirtyFiles: [], fingerprints: {}, unavailable: "Local Git evidence is not authoritative for a pane-native remote run." } : snapshotTrackedMutations(step.cwd ?? ctx.cwd);
1044
1114
  let finalMutationEvidence = collectTrackedMutationEvidence(mutationSnapshot, step.cwd ?? ctx.cwd);
1045
1115
 
1046
- let modelIndex = 0;
1047
1116
  let contextOverflow = false;
1048
1117
  let launchWarningsEmitted = false;
1049
- let recoveryState: LogicalRecoveryState = "unused";
1050
- let readonlyContinuation: RunChildSessionInput["readonlyContinuation"];
1051
- const continuationBudget = () => {
1052
- if (step.toolBudget) return "tool-budget-configured" as const;
1053
- // Refresh the authoritative run ledger, already fed synchronously by onChildEvent.
1054
- const exhausted = ctx.usageBudgetExhausted?.();
1055
- if (exhausted === true) return "exhausted" as const;
1056
- if (!ctx.usageBudget) return "unconfigured" as const;
1057
- if (ctx.usageBudget.costUsd || !ctx.onChildEvent || exhausted !== false) return "unknown" as const;
1058
- return "available" as const;
1059
- };
1060
- const lifecycleAllowsContinuation = () => !ctx.timeoutSignal?.aborted && !ctx.stopSignal?.aborted
1061
- && !ctx.skipAcceptance?.() && (ctx.deadlineAt === undefined || Date.now() < ctx.deadlineAt);
1062
- const canContinue = () => lifecycleAllowsContinuation() && ["available", "unconfigured"].includes(continuationBudget());
1063
- const failContinuationLaunch = (candidate: string | undefined, error: unknown) => {
1064
- const message = error instanceof Error ? error.message : String(error);
1065
- modelAttempts.push({ model: candidate ?? "default", success: false, exitCode: 1, error: message });
1066
- if (candidate) attemptedModels.push(candidate);
1067
- if (finalResult) finalResult = { ...finalResult, exitCode: 1, error: message };
1068
- };
1069
- let nextAttemptTask = task;
1070
- modelAttemptsLoop: while (modelIndex < candidates.length) {
1071
- if (ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break;
1072
- if (readonlyContinuation && !canContinue()) break;
1073
- const recoveringAbort = recoveryState === "abort-recovery";
1074
- const attemptTask = nextAttemptTask;
1075
- const candidate = candidates[modelIndex];
1076
- const expectedModelForVerification = candidate && !(step.skipPrimaryModelVerification && modelIndex === 0) ? candidate : undefined;
1118
+ const aggregateUsage = emptyUsage();
1119
+ let launched = false;
1120
+ let recoveryTask = task;
1121
+ singleLaunch: for (let attemptIndex = 0; attemptIndex < 2; attemptIndex++) {
1122
+ if (ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break singleLaunch;
1123
+ const expectedModelForVerification = candidate && !step.skipPrimaryModelVerification ? candidate : undefined;
1077
1124
  try {
1078
1125
  assertThinkingWithinCeiling({ model: candidate, configThinking: step.thinking, ceiling: step.thinkingCeiling, agent: step.agent, runId: ctx.id });
1079
1126
  } catch (error) {
1080
1127
  const message = error instanceof Error ? error.message : String(error);
1081
- if (readonlyContinuation) {
1082
- failContinuationLaunch(candidate, error);
1083
- break modelAttemptsLoop;
1084
- }
1085
1128
  return omitUndefinedProperties({ agent: step.agent, output: message, error: message, exitCode: 1, context: step.context, thinkingCeiling: step.thinkingCeiling });
1086
1129
  }
1087
1130
  ctx.onAttemptStart?.(omitUndefinedProperties({
@@ -1118,11 +1161,7 @@ export async function runSingleStepInner(
1118
1161
  childWatchdog,
1119
1162
  watchdogStatus: (event) => watchdogSink?.(event),
1120
1163
  });
1121
- } catch (error) {
1122
- if (!readonlyContinuation) throw error;
1123
- failContinuationLaunch(candidate, error);
1124
- break modelAttemptsLoop;
1125
- }
1164
+ } catch (error) { throw error; }
1126
1165
  if (effectiveStructuredOutput && launch.config.structuredOutput) {
1127
1166
  // The runner reads the value back from the runtime's files after the run.
1128
1167
  launch.config.structuredOutput.capture = createStructuredOutputFileCapture(effectiveStructuredOutput);
@@ -1141,39 +1180,35 @@ export async function runSingleStepInner(
1141
1180
  subagentOnlyExtensions: step.subagentOnlyExtensions,
1142
1181
  fast: step.fast,
1143
1182
  model: step.model,
1144
- modelCandidates: step.modelCandidates,
1145
1183
  mcpDirectTools: step.mcpDirectTools,
1146
1184
  cwd: step.cwd ?? ctx.cwd,
1147
1185
  requireReadTool: Boolean(step.skills?.length),
1148
1186
  structuredOutput: Boolean(effectiveStructuredOutput),
1149
1187
  capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
1150
1188
  inheritedCapabilityCeiling: ctx.inheritedChildRuntime?.capabilityCeiling,
1189
+ requiredExtensions: step.requiredExtensions ?? ctx.inheritedChildRuntime?.requiredExtensions,
1151
1190
  permissionRules: step.permissionRules,
1191
+ hostAvailableBuiltins: ctx.hostAvailableBuiltins,
1152
1192
  }));
1153
1193
  launchResolvedExtensions = projectLaunchResolvedChildExtensions(toolPlan);
1154
- actualLaunchContractDigest = launchBindingDigest(omitUndefinedProperties({
1194
+ actualLaunchContractDigest = resolveLaunchBinding({
1155
1195
  definitionDigest: step.definitionDigest,
1156
- task: step.launchBindingTask ?? task,
1157
- ...(candidate ? { model: candidate } : {}),
1158
- modelCandidates: candidates as string[],
1159
- ...(step.fast !== undefined ? { fast: step.fast } : {}),
1160
- ...(resolveEffectiveThinking(candidate, step.thinking) ? { thinking: resolveEffectiveThinking(candidate, step.thinking) } : {}),
1161
- ...(step.thinkingCeiling ? { thinkingCeiling: step.thinkingCeiling } : {}),
1162
- systemPrompt: step.systemPrompt ?? "",
1163
1196
  systemPromptMode: step.systemPromptMode,
1164
1197
  inheritProjectContext: step.inheritProjectContext,
1165
1198
  inheritGlobalContext: step.inheritGlobalContext,
1166
1199
  inheritSkills: step.inheritSkills,
1200
+ task: step.launchBindingTask ?? task,
1201
+ model: candidate,
1202
+ fast: step.fast,
1203
+ thinking: resolveEffectiveThinking(candidate, step.thinking),
1204
+ systemPrompt: step.systemPrompt ?? "",
1167
1205
  skills: step.skills,
1168
- tools: toolPlan.effectiveToolAllowlist,
1169
- ...(toolPlan.excludeTools.length > 0 ? { excludeTools: toolPlan.excludeTools } : {}),
1170
- extensions: toolPlan.extensionArgs,
1171
- mcpDirectTools: toolPlan.effectiveMcpTools,
1172
- ...(step.outputPath ? { outputPath: step.outputPath } : {}),
1173
- ...(step.outputMode ? { outputMode: step.outputMode } : {}),
1174
- ...(step.structuredOutputSchema ? { structuredOutputSchema: step.structuredOutputSchema } : {}),
1175
- ...(extensionBindings ? { extensionBindings } : {}),
1176
- }));
1206
+ toolPlan,
1207
+ outputPath: step.outputPath,
1208
+ outputMode: step.outputMode,
1209
+ structuredOutputSchema: step.structuredOutputSchema,
1210
+ extensionBindings,
1211
+ }).launchContractDigest;
1177
1212
  }
1178
1213
  capabilityAudit = attemptCapabilityAudit;
1179
1214
  // Each attempt rewrites the step output log; synchronous appends keep a
@@ -1182,10 +1217,7 @@ export async function runSingleStepInner(
1182
1217
  const run = await runChildSession(omitUndefinedProperties({
1183
1218
  factory: ctx.childSessions,
1184
1219
  launch,
1185
- collectReadonlyEvidence: true,
1186
- readonlyContinuation,
1187
- canContinue,
1188
- prompt: `Task: ${attemptTask}`,
1220
+ prompt: `Task: ${recoveryTask}`,
1189
1221
  childWatchdog,
1190
1222
  childEventContext: { runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
1191
1223
  appendChildEvent: (event) => appendDiagnosticJsonl(eventsPath, JSON.stringify(event), typeof event.type === "string" ? event.type : undefined),
@@ -1201,6 +1233,7 @@ export async function runSingleStepInner(
1201
1233
  registerTimeout: ctx.registerTimeout,
1202
1234
  registerStop: ctx.registerStop,
1203
1235
  registerSteer: ctx.registerSteer,
1236
+ onSteerOutcome: ctx.onSteerOutcome,
1204
1237
  registerWatchdogStatus: (sink) => { watchdogSink = sink; },
1205
1238
  timeoutMessage: ctx.timeoutMessage,
1206
1239
  stopMessage: ctx.stopMessage,
@@ -1213,6 +1246,16 @@ export async function runSingleStepInner(
1213
1246
  modelResponseAliases: step.modelResponseAliases,
1214
1247
  mutationTools: step.mutationTools,
1215
1248
  }));
1249
+ launched = true;
1250
+ aggregateUsage.input += run.usage.input;
1251
+ aggregateUsage.output += run.usage.output;
1252
+ aggregateUsage.cacheRead += run.usage.cacheRead;
1253
+ aggregateUsage.cacheWrite += run.usage.cacheWrite;
1254
+ aggregateUsage.cost += run.usage.cost;
1255
+ aggregateUsage.turns += run.usage.turns;
1256
+ // A parked run still owes completion evidence when it actually finishes.
1257
+ // Stopped/timedOut runs already have terminal failures; checking completion evidence there is meaningless.
1258
+ const completionDiagnosticsEligible = !run.interrupted && !run.stopped && !run.timedOut;
1216
1259
  const toolDiagnostic = run.exitCode === 0 && !run.error ? launch.capture.toolDiagnostic() : undefined;
1217
1260
  const toolAvailabilityError = toolDiagnostic ? formatChildToolDiagnostic(toolDiagnostic) : undefined;
1218
1261
  const runtimeAcknowledgedExtensions = launch.capture.runtimeAcknowledgedExtensions();
@@ -1228,7 +1271,7 @@ export async function runSingleStepInner(
1228
1271
  let structuredOutput: unknown;
1229
1272
  let structuredError: string | undefined;
1230
1273
  let validatedStructuredOutput = false;
1231
- if (effectiveStructuredOutput && run.exitCode === 0 && !run.error && !toolAvailabilityError && !midToolExitError) {
1274
+ if (completionDiagnosticsEligible && effectiveStructuredOutput && run.exitCode === 0 && !run.error && !toolAvailabilityError && !midToolExitError) {
1232
1275
  if (!run.structuredOutputToolInvoked) {
1233
1276
  structuredError = MISSING_STRUCTURED_OUTPUT_CALL_ERROR;
1234
1277
  } else {
@@ -1250,13 +1293,13 @@ export async function runSingleStepInner(
1250
1293
  const errorMessages = validatedStructuredOutput
1251
1294
  ? run.messages.slice(run.structuredOutputMessageStartIndex ?? run.messages.length)
1252
1295
  : run.messages;
1253
- const hiddenError = run.exitCode === 0 && !run.error && !toolAvailabilityError && !structuredError && !midToolExitError
1296
+ const hiddenError = completionDiagnosticsEligible && run.exitCode === 0 && !run.error && !toolAvailabilityError && !structuredError && !midToolExitError
1254
1297
  ? detectSubagentError(errorMessages)
1255
1298
  : null;
1256
1299
  const terminalEmptyAfterUsefulWork = !validatedStructuredOutput
1257
1300
  && hasEmptyTerminalAssistantResponse(run.messages)
1258
1301
  && (run.toolCount > 0 || Boolean(run.finalOutput.trim()));
1259
- const emptyOutputError = run.exitCode === 0
1302
+ const emptyOutputError = completionDiagnosticsEligible && run.exitCode === 0
1260
1303
  && !run.error
1261
1304
  && !toolAvailabilityError
1262
1305
  && !structuredError
@@ -1268,10 +1311,11 @@ export async function runSingleStepInner(
1268
1311
  const completionGuardEnabled = isAgentContract(step.agentContract) ? step.completionGuard === true : step.completionGuard !== false;
1269
1312
  const completionToolPlan = resolvedTaskToolPlan;
1270
1313
  const completionTools = completionToolPlan ? (completionToolPlan.explicitToolAllowlist ? completionToolPlan.effectiveToolAllowlist : undefined) : step.tools;
1271
- const mutationEvidence = collectTrackedMutationEvidence(mutationSnapshot, step.cwd ?? ctx.cwd);
1314
+ const remoteGitChanged = run.nativeMachine?.initialGit && run.nativeMachine.finalGit ? run.nativeMachine.initialGit.head !== run.nativeMachine.finalGit.head || run.nativeMachine.initialGit.dirty !== run.nativeMachine.finalGit.dirty : undefined;
1315
+ const mutationEvidence = run.nativeMachine ? { source: "tracked-files" as const, trackedOnly: true as const, changedFiles: [], attemptedMutation: remoteGitChanged === true, ...(remoteGitChanged === undefined ? { unavailable: "Remote Git before/after evidence was incomplete." } : {}) } : collectTrackedMutationEvidence(mutationSnapshot, step.cwd ?? ctx.cwd);
1272
1316
  finalMutationEvidence = mutationEvidence;
1273
1317
  const completionMutationEvidence = ctx.trackedMutationEvidenceForCompletionGuard === false ? undefined : mutationEvidence;
1274
- const completionGuard = run.exitCode === 0 && !run.error && !structuredError && !hiddenError?.hasError && !midToolExitError && !emptyOutputError && completionGuardEnabled
1318
+ const completionGuard = completionDiagnosticsEligible && run.exitCode === 0 && !run.error && !structuredError && !hiddenError?.hasError && !midToolExitError && !emptyOutputError && completionGuardEnabled
1275
1319
  ? evaluateCompletionMutationGuard(omitUndefinedProperties({
1276
1320
  agent: step.agent,
1277
1321
  task: taskForCompletionGuard,
@@ -1313,7 +1357,7 @@ export async function runSingleStepInner(
1313
1357
  ? { kind: "structured" as const, path: effectiveStructuredOutput.outputPath, missing: !fs.existsSync(effectiveStructuredOutput.outputPath) }
1314
1358
  : undefined;
1315
1359
  finalRequiredOutputMissing = requiredOutput?.missing;
1316
- const missingRequiredOutputError = formatRequiredOutputError(requiredOutput);
1360
+ const missingRequiredOutputError = completionDiagnosticsEligible ? formatRequiredOutputError(requiredOutput) : undefined;
1317
1361
  const missingRequiredOutputAfterMutation = Boolean(missingRequiredOutputError) && (mutationAttemptObserved || Boolean(mutationEvidence.changedFiles.length));
1318
1362
  const effectiveExitCode = toolAvailabilityError || completionEvidence.legacyFailureError || midToolExitError || structuredError || emptyOutputError || missingRequiredOutputError
1319
1363
  ? 1
@@ -1334,15 +1378,6 @@ export async function runSingleStepInner(
1334
1378
  : `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
1335
1379
  : undefined);
1336
1380
  const error = underlyingError ?? missingRequiredOutputError ?? completionEvidence.legacyFailureError;
1337
- const attempt: ModelAttempt = omitUndefinedProperties({
1338
- model: candidate ?? run.model ?? step.model ?? "default",
1339
- success: effectiveExitCode === 0 && !error,
1340
- exitCode: effectiveExitCode,
1341
- error,
1342
- usage: run.usage,
1343
- });
1344
- modelAttempts.push(attempt);
1345
- if (!recoveringAbort && candidate) attemptedModels.push(candidate);
1346
1381
  completionGuardTriggeredFinal = completionEvidence.guardTriggered && !underlyingError && !missingRequiredOutputError;
1347
1382
  finalOutputSnapshot = outputSnapshot;
1348
1383
  if (step.toolBudget) {
@@ -1360,11 +1395,14 @@ export async function runSingleStepInner(
1360
1395
  });
1361
1396
  const fileMutationEffect = completionEvidence.fileMutation ?? (missingRequiredOutputAfterMutation ? { status: "observed" as const, expected: completionEvidence.mutationExpected, attempted: true, evidence: mutationEvidence } : undefined);
1362
1397
  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;
1363
- const abortRecovery = !attempt.success ? planAbortRecovery({
1398
+ if (run.stopped || run.timedOut || ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break singleLaunch;
1399
+ if (effectiveExitCode === 0 && !error) break singleLaunch;
1400
+ if (completionEvidence.guardTriggered) break singleLaunch;
1401
+ const recovery = planAbortRecovery({
1364
1402
  messages: run.messages,
1365
1403
  error,
1366
1404
  sessionAvailable: Boolean(step.sessionFile && fs.existsSync(step.sessionFile)),
1367
- alreadyResumed: recoveryState !== "unused",
1405
+ alreadyResumed: attemptIndex > 0,
1368
1406
  stopped: run.stopped || ctx.stopSignal?.aborted || ctx.skipAcceptance?.(),
1369
1407
  interrupted: run.interrupted,
1370
1408
  timedOut: run.timedOut || ctx.timeoutSignal?.aborted,
@@ -1374,83 +1412,26 @@ export async function runSingleStepInner(
1374
1412
  acceptanceFailed: false,
1375
1413
  currentTool: run.currentTool,
1376
1414
  afterCompactionSettlement: run.afterCompactionSettlement,
1377
- }) : undefined;
1378
- if (abortRecovery?.action === "settle" && abortRecovery.diagnostic) {
1379
- attempt.error = attempt.error
1380
- ? `${abortRecovery.diagnostic}\n${attempt.error.slice(0, 8_000)}`
1381
- : abortRecovery.diagnostic;
1382
- if (finalResult) finalResult.abortRecoveryDiagnostic = abortRecovery.diagnostic;
1415
+ });
1416
+ if (recovery.action === "resume") {
1417
+ recoveryTask = recovery.prompt;
1418
+ continue singleLaunch;
1383
1419
  }
1384
- if (run.stopped || run.timedOut || ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break modelAttemptsLoop;
1385
- if (abortRecovery?.action === "settle" && abortRecovery.diagnostic) break modelAttemptsLoop;
1386
- if (attempt.success) break modelAttemptsLoop;
1387
- // The shared token is consumed before sibling creation; no third dispatch of any kind.
1388
- if (recoveryState === "readonly-continuation") break modelAttemptsLoop;
1389
- if (recoveringAbort) break modelAttemptsLoop;
1390
- if (abortRecovery?.action === "resume") {
1391
- recoveryState = "abort-recovery";
1392
- nextAttemptTask = abortRecovery.prompt;
1393
- attemptNotes.push("[abort-recovery] provider/transport abort after useful progress; resuming the retained child session once.");
1394
- continue;
1420
+ if (recovery.diagnostic) {
1421
+ finalResult.abortRecoveryDiagnostic = recovery.diagnostic;
1395
1422
  }
1396
- if (completionEvidence.guardTriggered) break modelAttemptsLoop;
1397
1423
 
1398
- const source = getSettledReadonlyChild(run);
1399
- if (source) {
1400
- const captured = launch.capture.completionIntentContext?.();
1401
- const sourceModel = captured?.model;
1402
- const retained = getReadonlySessionEvidence(source)!;
1403
- // Modality/capacity assessment, not another history/provenance validator.
1404
- // Images and unknown content have no certified token bound in this host slice.
1405
- const textOnly = (JSON.parse(retained.contextJson) as Message[]).every((message) => typeof message.content === "string"
1406
- || (Array.isArray(message.content) && message.content.every((block) => ["text", "thinking", "toolCall"].includes(block.type))));
1407
- const retainedBytes = Buffer.byteLength(retained.contextJson, "utf8");
1408
- const resolvedCandidates = candidates.map((reference, index) => {
1409
- // Only exact registry identities qualify; aliases/default guesses remain ineligible.
1410
- const base = reference ? splitKnownThinkingSuffix(reference).baseModel : "";
1411
- const slash = base.indexOf("/");
1412
- const model = slash > 0 ? captured?.modelRegistry.find(base.slice(0, slash), base.slice(slash + 1)) : undefined;
1413
- // Reserve the source window for runtime/system/tool overhead, plus a conservative
1414
- // byte bound for the complete retained text and new prompt; never assume a fit
1415
- // merely because a 429 request was sent. Equal/smaller windows remain denied.
1416
- const compatible = textOnly && model && sourceModel && model.input?.includes("text")
1417
- && Number.isFinite(sourceModel.contextWindow) && sourceModel.contextWindow > 0
1418
- && model.contextWindow >= sourceModel.contextWindow + retainedBytes + Buffer.byteLength(READONLY_CONTINUATION_PROMPT, "utf8")
1419
- && model.maxTokens > 0 && model.maxTokens <= sourceModel.maxTokens;
1420
- return { resolved: model ? { provider: model.provider, model: model.id, api: model.api } : undefined,
1421
- tried: index <= modelIndex, compatibility: compatible ? "compatible" as const : "unknown" as const };
1422
- });
1423
- const plan = planReadonlyModelContinuation({ source, recoveryState, candidates: resolvedCandidates, currentIndex: modelIndex,
1424
- lifecycleAllowsContinuation: lifecycleAllowsContinuation() && !run.interrupted && !run.stopped && !run.timedOut,
1425
- effectsAllowContinuation: !run.currentTool && !run.observedMutationAttempt && !mutationEvidence.attemptedMutation
1426
- && !structuredError && !effectiveStructuredOutput && !missingRequiredOutputError && !toolAvailabilityError
1427
- && !completionEvidence.guardTriggered && !midToolExitError && !hiddenError?.hasError,
1428
- budget: continuationBudget(), knownContextOverflow: isContextOverflow(error) });
1429
- if (plan.kind === "continue" && canContinue()) {
1430
- recoveryState = plan.recoveryState;
1431
- const selected = resolvedCandidates[plan.candidateIndex]!.resolved!;
1432
- readonlyContinuation = { source, expected: plan.expected, modelId: `${selected.provider}/${selected.model}` };
1433
- nextAttemptTask = plan.prompt;
1434
- modelIndex = plan.candidateIndex;
1435
- attemptNotes.push(`[readonly-continuation] ${attempt.model} returned HTTP 429 after read-only progress; continuing the retained session once with ${candidates[modelIndex]}.`);
1436
- continue;
1437
- }
1438
- }
1439
-
1440
- const retryableModelFailure = isRetryableModelFailureAttempt({ error, messages: run.messages, toolCount: run.toolCount });
1441
- if (retryableModelFailure) recordRetryableModelFailure(candidate ?? run.model ?? step.model, error);
1442
1424
  if (isContextOverflow(error)) {
1443
1425
  contextOverflow = true;
1444
- attemptNotes.push(`[fallback] ${attempt.model} failed: context overflow — the input exceeds this model's context window. Reduce the task input or use a model with a larger context window.`);
1445
- break modelAttemptsLoop;
1426
+ break singleLaunch;
1446
1427
  }
1447
- if (!retryableModelFailure || modelIndex === candidates.length - 1) break modelAttemptsLoop;
1448
- attemptNotes.push(formatModelAttemptNote(attempt, candidates[modelIndex + 1]));
1449
- modelIndex += 1;
1428
+ break singleLaunch;
1450
1429
  }
1451
1430
 
1452
1431
  const rawOutput = finalResult?.finalOutput ?? "";
1453
- const outputForPersistence = stripAcceptanceReport(rawOutput);
1432
+ let outputForPersistence = stripAcceptanceReport(rawOutput);
1433
+ if (!outputForPersistence.trim() && finalResult?.structuredOutput !== undefined)
1434
+ outputForPersistence = JSON.stringify(finalResult.structuredOutput, null, 2);
1454
1435
  const resolvedOutput = step.outputPath && finalResult?.exitCode === 0
1455
1436
  ? resolveSingleOutput(step.outputPath, outputForPersistence, finalOutputSnapshot, step.outputClaimPath)
1456
1437
  : { fullOutput: outputForPersistence };
@@ -1463,9 +1444,6 @@ export async function runSingleStepInner(
1463
1444
  const output = stripAcceptanceReport(resolvedOutput.fullOutput);
1464
1445
  const outputReference = resolvedOutput.savedPath ? formatSavedOutputReference(resolvedOutput.savedPath, output) : undefined;
1465
1446
  let outputForSummary = output;
1466
- if (attemptNotes.length > 0) {
1467
- outputForSummary = `${attemptNotes.join("\n")}\n\n${outputForSummary}`.trim();
1468
- }
1469
1447
  if (finalResult?.stopped && !outputForSummary.trim()) {
1470
1448
  outputForSummary = ctx.stopMessage ?? "Subagent stopped by user.";
1471
1449
  }
@@ -1550,7 +1528,7 @@ export async function runSingleStepInner(
1550
1528
  abortRecoveryDiagnostic: effectiveFinalExitCode !== 0 ? finalResult?.abortRecoveryDiagnostic : undefined,
1551
1529
  requiredOutput: effectiveFinalExitCode !== 0 ? finalResult?.effects?.settlementDiagnostic?.requiredOutput : undefined,
1552
1530
  });
1553
- const usage = usageFromAttempts(modelAttempts);
1531
+ const usage = launched ? aggregateUsage : undefined;
1554
1532
 
1555
1533
  const artifactErrors = artifactPaths && ctx.artifactConfig?.enabled !== false
1556
1534
  ? persistStepArtifacts({
@@ -1568,8 +1546,8 @@ export async function runSingleStepInner(
1568
1546
  task: PROMPT_REDACTED,
1569
1547
  exitCode: effectiveFinalExitCode,
1570
1548
  model: finalResult?.model,
1571
- attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
1572
- modelAttempts,
1549
+ nativeMachine: finalResult?.nativeMachine,
1550
+ requestedModel: step.requestedModel,
1573
1551
  usage,
1574
1552
  error: effectiveFinalError,
1575
1553
  acceptance: effectiveAcceptance,
@@ -1598,15 +1576,16 @@ export async function runSingleStepInner(
1598
1576
  sessionFile: step.sessionFile,
1599
1577
  intercomTarget: ctx.childIntercomTarget,
1600
1578
  model: finalResult?.model,
1579
+ nativeMachine: finalResult?.nativeMachine,
1601
1580
  thinking: resolveEffectiveThinking(finalResult?.model, step.thinking),
1602
- attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
1603
- modelAttempts,
1581
+ requestedModel: step.requestedModel,
1604
1582
  contextOverflow: contextOverflow || undefined,
1605
- totalCost: costSummaryFromAttempts(modelAttempts),
1583
+ totalCost: costSummaryFromUsage(usage),
1606
1584
  usage,
1607
1585
  artifactPaths,
1608
1586
  savedOutputPath: finalizedOutput.savedPath,
1609
1587
  outputSaveError: [resolvedOutput.saveError, artifactErrors.outputSaveError].filter(Boolean).join("\n") || undefined,
1588
+ artifactOutputSaveFailed: artifactErrors.outputSaveError ? true : undefined,
1610
1589
  metadataSaveError: artifactErrors.metadataSaveError,
1611
1590
  transcriptPath: transcriptWriter ? artifactPaths?.transcriptPath : undefined,
1612
1591
  transcriptError: transcriptWriter?.getError(),
@@ -1850,8 +1829,12 @@ function missingRequiredOutputAfterUsefulMutation(result: SingleStepResult): boo
1850
1829
  }
1851
1830
 
1852
1831
  function partialExecutionWithUsefulMutation(result: SingleStepResult): boolean {
1832
+ if (result.execution?.status !== "partial") return false;
1833
+ // Pane-native external results are deliberately partial from bounded terminal
1834
+ // evidence alone; Pi mutation-based partials retain their existing behavior.
1835
+ if (result.runner?.type === "external-cli" && result.runner.machine) return true;
1853
1836
  const fileMutation = result.effects?.fileMutation;
1854
- return result.execution?.status === "partial" && (fileMutation?.attempted === true || Boolean(fileMutation?.evidence?.changedFiles.length));
1837
+ return fileMutation?.attempted === true || Boolean(fileMutation?.evidence?.changedFiles.length);
1855
1838
  }
1856
1839
 
1857
1840
  function partialEvidenceResult(result: SingleStepResult): boolean {
@@ -1954,6 +1937,7 @@ export async function runSubagent(
1954
1937
  let currentActivityState: ActivityState | undefined;
1955
1938
  let activityTimer: NodeJS.Timeout | undefined;
1956
1939
  let timeoutTimer: NodeJS.Timeout | undefined;
1940
+ let checkpointTimer: NodeJS.Timeout | undefined;
1957
1941
  let timedOut = false;
1958
1942
  let stopped = false;
1959
1943
  let usageBudgetExceeded = false;
@@ -1962,6 +1946,7 @@ export async function runSubagent(
1962
1946
  const timeoutAbortController = new AbortController();
1963
1947
  const stopAbortController = new AbortController();
1964
1948
  const setupInterruptController = new AbortController();
1949
+ const runStopSignal = AbortSignal.any([timeoutAbortController.signal, stopAbortController.signal]);
1965
1950
  const setupSignal = AbortSignal.any([timeoutAbortController.signal, stopAbortController.signal, setupInterruptController.signal]);
1966
1951
  let previousCumulativeTokens: TokenUsage = { input: 0, output: 0, total: 0 };
1967
1952
  let latestSessionFile: string | undefined;
@@ -2003,7 +1988,7 @@ export async function runSubagent(
2003
1988
  model: task.model,
2004
1989
  ...(task.contextLimit !== undefined ? { contextLimit: task.contextLimit } : {}),
2005
1990
  thinking: task.thinking,
2006
- attemptedModels: task.modelCandidates && task.modelCandidates.length > 0 ? task.modelCandidates : task.model ? [task.model] : undefined,
1991
+ requestedModel: task.requestedModel,
2007
1992
  recentTools: [],
2008
1993
  recentOutput: [],
2009
1994
  }));
@@ -2057,7 +2042,7 @@ export async function runSubagent(
2057
2042
  model: step.model,
2058
2043
  ...(step.contextLimit !== undefined ? { contextLimit: step.contextLimit } : {}),
2059
2044
  thinking: step.thinking,
2060
- attemptedModels: step.modelCandidates && step.modelCandidates.length > 0 ? step.modelCandidates : step.model ? [step.model] : undefined,
2045
+ requestedModel: step.requestedModel,
2061
2046
  recentTools: [],
2062
2047
  recentOutput: [],
2063
2048
  }));
@@ -2284,9 +2269,7 @@ export async function runSubagent(
2284
2269
  sessionFile: step.sessionFile,
2285
2270
  model: step.model,
2286
2271
  thinking: step.thinking,
2287
- attemptedModels: step.attemptedModels,
2288
- modelAttempts: step.modelAttempts,
2289
- usage: usageFromAttempts(step.modelAttempts),
2272
+ requestedModel: step.requestedModel,
2290
2273
  contextOverflow: step.contextOverflow,
2291
2274
  })),
2292
2275
  exitCode: state === "complete" || state === "paused" ? 0 : 1,
@@ -2451,6 +2434,8 @@ export async function runSubagent(
2451
2434
  yield* nestedRuns(child.steps?.flatMap((step) => step.children ?? []));
2452
2435
  }
2453
2436
  };
2437
+ const isNestedControlDescendant = (run: NestedRunSummary): boolean =>
2438
+ !config.nestedSelf || run.path.some((entry) => entry.runId === id);
2454
2439
  const interruptNestedAsyncDescendants = (): void => {
2455
2440
  if (!config.nestedRoute) return;
2456
2441
  let registry: ReturnType<typeof projectNestedEvents>;
@@ -2466,7 +2451,7 @@ export async function runSubagent(
2466
2451
  return;
2467
2452
  }
2468
2453
  for (const run of nestedRuns(registry.children)) {
2469
- if (run.state !== "running" && run.state !== "queued") continue;
2454
+ if (!isNestedControlDescendant(run) || (run.state !== "running" && run.state !== "queued")) continue;
2470
2455
  const nestedAsyncDir = run.asyncDir ?? resolveNestedAsyncDir(config.nestedRoute.rootRunId, run);
2471
2456
  if (!nestedAsyncDir) continue;
2472
2457
  try {
@@ -2497,7 +2482,7 @@ export async function runSubagent(
2497
2482
  return;
2498
2483
  }
2499
2484
  for (const run of nestedRuns(registry.children)) {
2500
- if (run.state !== "running" && run.state !== "queued") continue;
2485
+ if (!isNestedControlDescendant(run) || (run.state !== "running" && run.state !== "queued")) continue;
2501
2486
  const nestedAsyncDir = run.asyncDir ?? resolveNestedAsyncDir(config.nestedRoute.rootRunId, run);
2502
2487
  if (!nestedAsyncDir) continue;
2503
2488
  try {
@@ -2528,7 +2513,7 @@ export async function runSubagent(
2528
2513
  return;
2529
2514
  }
2530
2515
  for (const run of nestedRuns(registry.children)) {
2531
- if (run.state !== "running" && run.state !== "queued") continue;
2516
+ if (!isNestedControlDescendant(run) || (run.state !== "running" && run.state !== "queued")) continue;
2532
2517
  const nestedAsyncDir = run.asyncDir ?? resolveNestedAsyncDir(config.nestedRoute.rootRunId, run);
2533
2518
  if (!nestedAsyncDir) continue;
2534
2519
  try {
@@ -2584,6 +2569,7 @@ export async function runSubagent(
2584
2569
  }
2585
2570
  const appendedSteps = requests.flatMap((request) => request.steps);
2586
2571
  steps.push(...appendedSteps);
2572
+ flatSteps.push(...flattenSteps(appendedSteps));
2587
2573
  const now = Date.now();
2588
2574
  const pendingAppends = countPendingChainAppendRequests(asyncDir);
2589
2575
  const added = appendRunnerStepsToStatus({
@@ -2617,9 +2603,66 @@ export async function runSubagent(
2617
2603
  setOptionalProperty(groupNode, "acceptanceStatus", acceptance?.status ?? groupNode.acceptanceStatus);
2618
2604
  };
2619
2605
 
2606
+ const EXTERNAL_GIT_PROBE_MIN_INTERVAL_MS = 2_000;
2607
+ type ExternalActivityEvidence = {
2608
+ cwd?: string;
2609
+ lastStreamActivityAt?: number;
2610
+ fingerprint?: string;
2611
+ probeInFlight?: Promise<void>;
2612
+ lastProbeStartedAt?: number;
2613
+ };
2614
+ const externalActivityEvidence = new Map<number, ExternalActivityEvidence>();
2615
+ const gitProbesByCwd = new Map<string, Promise<string | undefined>>();
2616
+ let periodicGitProbeFailure: { index: number; error: Error; message: string } | undefined;
2617
+ const reportedGitProbeErrors = new Set<string>();
2618
+ const reportGitProbeError = (externalCwd: string, error: unknown): void => {
2619
+ if (reportedGitProbeErrors.has(externalCwd)) return;
2620
+ reportedGitProbeErrors.add(externalCwd);
2621
+ const detail = error instanceof Error ? error.message : String(error);
2622
+ console.error(`[pi-subagents] Git activity evidence unavailable for '${externalCwd.slice(-300)}'; check the cwd and Git installation: ${detail.slice(0, 500)}`);
2623
+ };
2624
+ const readSharedGitFingerprint = (externalCwd: string): Promise<string | undefined> => {
2625
+ const inFlight = gitProbesByCwd.get(externalCwd);
2626
+ if (inFlight) return inFlight;
2627
+ const probe = readGitFingerprint(externalCwd, runStopSignal, (error) => reportGitProbeError(externalCwd, error)).finally(() => gitProbesByCwd.delete(externalCwd));
2628
+ gitProbesByCwd.set(externalCwd, probe);
2629
+ return probe;
2630
+ };
2631
+ const recordPeriodicGitProbeFailure = (index: number, error: unknown): void => {
2632
+ if (periodicGitProbeFailure) return;
2633
+ const cause = error instanceof Error ? error : new Error(String(error));
2634
+ const message = `PROCESS_TREE_UNVERIFIED: ${cause.message}`;
2635
+ periodicGitProbeFailure = { index, error: cause, message };
2636
+ statusPayload.error = message;
2637
+ const step = statusPayload.steps[index];
2638
+ if (step) step.error = message;
2639
+ statusPayload.lastUpdate = Date.now();
2640
+ writeStatusPayload();
2641
+ appendJsonl(eventsPath, JSON.stringify({
2642
+ type: "subagent.run.process_tree_unverified",
2643
+ ts: statusPayload.lastUpdate,
2644
+ runId: id,
2645
+ index,
2646
+ code: "PROCESS_TREE_UNVERIFIED",
2647
+ message,
2648
+ }));
2649
+ };
2650
+ const prepareExternalActivity = async (index: number, externalCwd: string, signal: AbortSignal): Promise<void> => {
2651
+ if (!controlConfig.enabled) return;
2652
+ const resolvedCwd = path.resolve(externalCwd);
2653
+ const evidence: ExternalActivityEvidence = { cwd: resolvedCwd, lastProbeStartedAt: performance.now() };
2654
+ externalActivityEvidence.set(index, evidence);
2655
+ evidence.fingerprint = await readGitFingerprint(resolvedCwd, signal, (error) => reportGitProbeError(resolvedCwd, error));
2656
+ };
2657
+ const recordExternalStreamActivity = (index: number): void => {
2658
+ const evidence = externalActivityEvidence.get(index) ?? {};
2659
+ externalActivityEvidence.set(index, evidence);
2660
+ evidence.lastStreamActivityAt = Date.now();
2661
+ };
2620
2662
  const stepOutputActivityAt = (index: number): number => {
2621
2663
  const step = statusPayload.steps[index];
2622
2664
  let lastActivityAt = step?.lastActivityAt ?? step?.startedAt ?? overallStartTime;
2665
+ lastActivityAt = Math.max(lastActivityAt, externalActivityEvidence.get(index)?.lastStreamActivityAt ?? 0);
2623
2666
  const outputPath = path.join(asyncDir, `output-${index}.log`);
2624
2667
  try {
2625
2668
  lastActivityAt = Math.max(lastActivityAt, fs.statSync(outputPath).mtimeMs);
@@ -2911,7 +2954,10 @@ export async function runSubagent(
2911
2954
  const applySteerDelivery = (requestId: string, index: number, delivery: { state: "delivered" | "queued" | "failed"; message: string }): void => {
2912
2955
  const lifecycle = steeringStatus(statusPayload);
2913
2956
  const request = lifecycle.recent.find((candidate) => candidate.id === requestId);
2914
- if (!request || !request.targets.some((target) => target.index === index)) return;
2957
+ const target = request?.targets.find((candidate) => candidate.index === index);
2958
+ if (!request || !target) return;
2959
+ if (target.state === "delivered" || target.state === "late" || target.state === "failed") return;
2960
+ if (target.state === delivery.state) return;
2915
2961
  const late = fs.existsSync(steeringMarkerPath(requestId));
2916
2962
  const now = Date.now();
2917
2963
  if (delivery.state === "delivered") {
@@ -2984,7 +3030,7 @@ export async function runSubagent(
2984
3030
  return;
2985
3031
  }
2986
3032
  if (event.type === "message_end") {
2987
- const next = applyChildWatchdogMessage(step.watchdog, event.message, now);
3033
+ const next = applyChildWatchdogMessage(step.watchdog, event.message);
2988
3034
  if (next) step.watchdog = next;
2989
3035
  if (next && (event.message as { role?: unknown } | undefined)?.role === "custom") {
2990
3036
  statusPayload.lastUpdate = now;
@@ -3125,7 +3171,7 @@ export async function runSubagent(
3125
3171
  // A sibling may keep aggregate attention unchanged; publish this step's transition.
3126
3172
  writeStatusPayload(step.activityState !== previousActivityState);
3127
3173
  };
3128
- const updateRunnerActivityState = (now: number): boolean => {
3174
+ const updateRunnerActivityState = (now: number, skipExternalProbeIndex?: number): boolean => {
3129
3175
  if (!controlConfig.enabled) return false;
3130
3176
  let changed = false;
3131
3177
  let runLastActivityAt = statusPayload.lastActivityAt ?? overallStartTime;
@@ -3142,12 +3188,35 @@ export async function runSubagent(
3142
3188
  config: controlConfig,
3143
3189
  startedAt: step.startedAt ?? overallStartTime,
3144
3190
  lastActivityAt,
3145
- turnCount: step.turnCount,
3191
+ turnCount: flatSteps[index]?.runner?.type === "external-cli" ? 1 : step.turnCount,
3146
3192
  currentTool: step.currentTool,
3147
3193
  thinking: step.thinking,
3148
3194
  now,
3149
3195
  }));
3150
3196
  if (idleState === "needs_attention") {
3197
+ const evidence = externalActivityEvidence.get(index);
3198
+ if (index !== skipExternalProbeIndex && evidence?.cwd && evidence.fingerprint) {
3199
+ if (evidence.probeInFlight) continue;
3200
+ if (performance.now() - (evidence.lastProbeStartedAt ?? 0) >= EXTERNAL_GIT_PROBE_MIN_INTERVAL_MS) {
3201
+ evidence.lastProbeStartedAt = performance.now();
3202
+ const baseline = evidence.fingerprint;
3203
+ evidence.probeInFlight = readSharedGitFingerprint(evidence.cwd).then((fingerprint) => {
3204
+ if (requiredStatusStep(statusPayload, index).status !== "running") return;
3205
+ if (fingerprint && fingerprint !== baseline) {
3206
+ evidence.fingerprint = fingerprint;
3207
+ evidence.lastStreamActivityAt = Date.now();
3208
+ updateRunnerActivityState(Date.now());
3209
+ } else {
3210
+ updateRunnerActivityState(Date.now(), index);
3211
+ }
3212
+ }).catch((error: unknown) => {
3213
+ recordPeriodicGitProbeFailure(index, error);
3214
+ }).finally(() => {
3215
+ delete evidence.probeInFlight;
3216
+ });
3217
+ continue;
3218
+ }
3219
+ }
3151
3220
  const previous = step.activityState;
3152
3221
  step.activityState = "needs_attention";
3153
3222
  if (previous !== "needs_attention") {
@@ -3209,11 +3278,15 @@ export async function runSubagent(
3209
3278
  };
3210
3279
  const finalizeWorktree = async (setup: WorktreeSetup, stepIndex: number, flatStartIndex: number, action: () => void, deadlineAt?: number): Promise<void> => {
3211
3280
  let admitted = false;
3212
- try { await withWorktreeTransaction(() => {
3213
- if (deadlineAt !== undefined && Date.now() >= deadlineAt) throw new Error("Run deadline expired before worktree cleanup");
3214
- admitted = true;
3215
- action();
3216
- }); }
3281
+ try {
3282
+ await Promise.all([...externalActivityEvidence.values()].map((evidence) => evidence.probeInFlight).filter((probe): probe is Promise<void> => probe !== undefined));
3283
+ if (periodicGitProbeFailure) throw periodicGitProbeFailure.error;
3284
+ await withWorktreeTransaction(() => {
3285
+ if (deadlineAt !== undefined && Date.now() >= deadlineAt) throw new Error("Run deadline expired before worktree cleanup");
3286
+ admitted = true;
3287
+ action();
3288
+ });
3289
+ }
3217
3290
  catch (error) {
3218
3291
  if (admitted) throw error;
3219
3292
  const reason = `Worktree finalization retained; manual reconciliation required: ${error instanceof Error ? error.message : String(error)}`;
@@ -3268,16 +3341,17 @@ export async function runSubagent(
3268
3341
  interruptActiveChildren();
3269
3342
  };
3270
3343
  const stopRunner = () => {
3271
- if (stopped || timedOut || interrupted || statusPayload.state !== "running") return;
3344
+ if (stopped || timedOut || (statusPayload.state !== "running" && statusPayload.state !== "paused")) return;
3272
3345
  stopped = true;
3346
+ interrupted = false;
3273
3347
  const now = Date.now();
3274
3348
  statusPayload.stopped = true;
3275
3349
  statusPayload.error = stopMessage;
3276
3350
  currentActivityState = undefined;
3277
3351
  delete statusPayload.activityState;
3278
3352
  statusPayload.lastUpdate = now;
3279
- for (const step of statusPayload.steps) {
3280
- if (step.status !== "running" && step.status !== "pending") continue;
3353
+ for (const [index, step] of statusPayload.steps.entries()) {
3354
+ if (step.status !== "running" && step.status !== "pending" && step.status !== "paused") continue;
3281
3355
  step.status = "stopped";
3282
3356
  step.error = stopMessage;
3283
3357
  step.exitCode = 1;
@@ -3286,6 +3360,8 @@ export async function runSubagent(
3286
3360
  step.endedAt = now;
3287
3361
  step.durationMs = step.startedAt ? now - step.startedAt : 0;
3288
3362
  step.lastActivityAt = now;
3363
+ const result = results[index];
3364
+ if (result?.interrupted) results[index] = { ...result, output: stopMessage, error: stopMessage, exitCode: 1, success: false, interrupted: false, stopped: true };
3289
3365
  }
3290
3366
  writeStatusPayload();
3291
3367
  appendJsonl(eventsPath, JSON.stringify({
@@ -3357,6 +3433,30 @@ export async function runSubagent(
3357
3433
  const remainingMs = Math.max(0, config.deadlineAt - Date.now());
3358
3434
  timeoutTimer = setTimeout(timeoutRunner, remainingMs);
3359
3435
  timeoutTimer.unref?.();
3436
+ // Route the pre-deadline checkpoint like any external steer so its lifecycle records the receipt.
3437
+ const checkpointBeforeDeadlineMs = config.checkpointBeforeDeadlineMs;
3438
+ const checkpointDelayMs = checkpointBeforeDeadlineMs === undefined || !Number.isInteger(checkpointBeforeDeadlineMs) || checkpointBeforeDeadlineMs <= 0
3439
+ ? undefined
3440
+ : remainingMs - checkpointBeforeDeadlineMs;
3441
+ if (checkpointDelayMs !== undefined && checkpointDelayMs >= 1_000) {
3442
+ const deadlineAt = config.deadlineAt;
3443
+ checkpointTimer = setTimeout(() => {
3444
+ checkpointTimer = undefined;
3445
+ if (timedOut || stopped || interrupted) return;
3446
+ if (!statusPayload.steps.some((step) => step.status === "running")) return;
3447
+ const now = Date.now();
3448
+ const seconds = Math.round(Math.max(0, deadlineAt - now) / 1000);
3449
+ deliverSteerRequest({
3450
+ type: "steer",
3451
+ id: `deadline-checkpoint-${now}`,
3452
+ ts: now,
3453
+ mode: "steer",
3454
+ source: "deadline-checkpoint",
3455
+ message: `Deadline checkpoint from the runner: this run is killed in about ${seconds} seconds. Finish the current tool call only, then stop and reply with a handoff: changed files, build/test state, remaining work, and commit/PR state. Do not start new work.`,
3456
+ });
3457
+ }, checkpointDelayMs);
3458
+ checkpointTimer.unref?.();
3459
+ }
3360
3460
  }
3361
3461
  appendJsonl(
3362
3462
  eventsPath,
@@ -3402,14 +3502,7 @@ export async function runSubagent(
3402
3502
  const thinkingOverride = step.thinkingOverrides?.[itemIndex];
3403
3503
  const model = thinkingOverride ? applyThinkingSuffix(step.parallel.model, thinkingOverride, true) : step.parallel.model;
3404
3504
  const configThinking = thinkingOverride ? thinkingOverride : step.parallel.thinking;
3405
- const candidates = step.parallel.modelCandidates !== undefined
3406
- ? step.parallel.modelCandidates.length > 0
3407
- ? step.parallel.modelCandidates.map((candidate) => thinkingOverride ? applyThinkingSuffix(candidate, thinkingOverride, true) ?? candidate : candidate)
3408
- : [undefined]
3409
- : model ? [model] : [undefined];
3410
- for (const candidate of candidates) {
3411
- assertThinkingWithinCeiling({ model: candidate, configThinking, ceiling: step.parallel.thinkingCeiling, agent: step.parallel.agent, runId: id });
3412
- }
3505
+ assertThinkingWithinCeiling({ model, configThinking, ceiling: step.parallel.thinkingCeiling, agent: step.parallel.agent, runId: id });
3413
3506
  }
3414
3507
  if (materialized.collectedOnEmpty) await validateDynamicCollection(step.collect.outputSchema, materialized.collectedOnEmpty);
3415
3508
  if (!config.runFanoutBudget) throw new Error("Async runner is missing its run fan-out budget identity.");
@@ -3542,10 +3635,6 @@ export async function runSubagent(
3542
3635
  ...(thinkingOverride ? {
3543
3636
  ...(model ? { model } : {}),
3544
3637
  ...(thinking ? { thinking } : {}),
3545
- ...(step.parallel.modelCandidates ? { modelCandidates: step.parallel.modelCandidates.flatMap((candidate) => {
3546
- const resolved = applyThinkingSuffix(candidate, thinkingOverride, true);
3547
- return resolved ? [resolved] : [];
3548
- }) } : {}),
3549
3638
  } : {}),
3550
3639
  structuredOutputSchema: step.parallel.structuredOutputSchema ?? step.parallel.structuredOutput?.schema,
3551
3640
  });
@@ -3572,7 +3661,7 @@ export async function runSubagent(
3572
3661
  ...(task.contextLimit !== undefined ? { contextLimit: task.contextLimit } : {}),
3573
3662
  ...(task.thinking ? { thinking: task.thinking } : {}),
3574
3663
  ...(task.thinkingCeiling ? { thinkingCeiling: task.thinkingCeiling } : {}),
3575
- ...(task.modelCandidates && task.modelCandidates.length > 0 ? { attemptedModels: task.modelCandidates } : task.model ? { attemptedModels: [task.model] } : {}),
3664
+ ...(task.requestedModel ? { requestedModel: task.requestedModel } : {}),
3576
3665
  recentTools: [],
3577
3666
  recentOutput: [],
3578
3667
  });
@@ -3686,10 +3775,12 @@ export async function runSubagent(
3686
3775
  nestedRoute: config.nestedRoute,
3687
3776
  capabilityCeiling: config.capabilityCeiling,
3688
3777
  runFanoutBudget: config.runFanoutBudget,
3778
+ hostAvailableBuiltins: config.hostAvailableBuiltins,
3689
3779
  registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
3690
3780
  registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
3691
3781
  registerStop: (stop) => registerStepStop(fi, stop),
3692
3782
  registerSteer: (steer) => registerStepSteer(fi, steer),
3783
+ onSteerOutcome: (request, delivery) => applySteerDelivery(request.id, fi, delivery),
3693
3784
  timeoutSignal: timeoutAbortController.signal,
3694
3785
  stopSignal: stopAbortController.signal,
3695
3786
  trackedMutationEvidenceForCompletionGuard: false,
@@ -3699,6 +3790,8 @@ export async function runSubagent(
3699
3790
  onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking, attempt.contextLimit),
3700
3791
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
3701
3792
  onExternalProcess: (process) => updateExternalProcess(fi, process),
3793
+ prepareExternalActivity: (externalCwd, signal) => prepareExternalActivity(fi, externalCwd, signal),
3794
+ onExternalStreamActivity: () => recordExternalStreamActivity(fi),
3702
3795
  onExternalJob: (externalJob) => updateExternalJob(fi, externalJob),
3703
3796
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(fi),
3704
3797
  usageBudgetExhausted: continuationUsageBudgetExhausted,
@@ -3708,7 +3801,7 @@ export async function runSubagent(
3708
3801
  const taskEndTime = Date.now();
3709
3802
  const childInterrupted = singleResult.interrupted === true;
3710
3803
  const childStopped = singleResult.stopped === true;
3711
- requiredStatusStep(statusPayload, fi).status = stopped || childStopped ? "stopped" : timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
3804
+ requiredStatusStep(statusPayload, fi).status = stopped || childStopped ? "stopped" : timedOut ? "failed" : childInterrupted ? "paused" : singleResult.execution?.status === "partial" ? "partial" : singleResult.exitCode === 0 ? "complete" : "failed";
3712
3805
  requiredStatusStep(statusPayload, fi).endedAt = taskEndTime;
3713
3806
  requiredStatusStep(statusPayload, fi).durationMs = taskEndTime - taskStartTime;
3714
3807
  requiredStatusStep(statusPayload, fi).exitCode = stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
@@ -3721,8 +3814,7 @@ export async function runSubagent(
3721
3814
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "sessionName", singleResult.sessionName);
3722
3815
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "model", singleResult.model);
3723
3816
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "thinking", resolveEffectiveThinking(singleResult.model, requiredStatusStep(statusPayload, fi).thinking));
3724
- setOptionalProperty(requiredStatusStep(statusPayload, fi), "attemptedModels", singleResult.attemptedModels);
3725
- setOptionalProperty(requiredStatusStep(statusPayload, fi), "modelAttempts", singleResult.modelAttempts);
3817
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "requestedModel", singleResult.requestedModel);
3726
3818
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "contextOverflow", singleResult.contextOverflow);
3727
3819
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "totalCost", singleResult.totalCost);
3728
3820
  if (singleResult.totalCost) {
@@ -3779,7 +3871,7 @@ export async function runSubagent(
3779
3871
  output: pr.output,
3780
3872
  outputState: pr.outputState,
3781
3873
  error: pr.error,
3782
- success: pr.stopped !== true && pr.interrupted !== true && pr.exitCode === 0,
3874
+ success: pr.stopped !== true && pr.interrupted !== true && pr.exitCode === 0 && pr.execution?.status !== "partial",
3783
3875
  exitCode: pr.interrupted === true ? 0 : pr.exitCode,
3784
3876
  skipped: pr.skipped,
3785
3877
  interrupted: pr.interrupted,
@@ -3791,12 +3883,13 @@ export async function runSubagent(
3791
3883
  intercomTarget: pr.intercomTarget,
3792
3884
  model: pr.model,
3793
3885
  thinking: pr.thinking,
3794
- attemptedModels: pr.attemptedModels,
3795
- modelAttempts: pr.modelAttempts,
3886
+ requestedModel: pr.requestedModel,
3796
3887
  contextOverflow: pr.contextOverflow,
3797
3888
  totalCost: pr.totalCost,
3798
3889
  usage: pr.usage,
3799
3890
  artifactPaths: pr.artifactPaths,
3891
+ outputSaveError: pr.outputSaveError,
3892
+ artifactOutputSaveFailed: pr.artifactOutputSaveFailed,
3800
3893
  transcriptPath: pr.transcriptPath,
3801
3894
  transcriptError: pr.transcriptError,
3802
3895
  effects: pr.effects,
@@ -4096,10 +4189,12 @@ export async function runSubagent(
4096
4189
  nestedRoute: config.nestedRoute,
4097
4190
  capabilityCeiling: config.capabilityCeiling,
4098
4191
  runFanoutBudget: config.runFanoutBudget,
4192
+ hostAvailableBuiltins: config.hostAvailableBuiltins,
4099
4193
  registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
4100
4194
  registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
4101
4195
  registerStop: (stop) => registerStepStop(fi, stop),
4102
4196
  registerSteer: (steer) => registerStepSteer(fi, steer),
4197
+ onSteerOutcome: (request, delivery) => applySteerDelivery(request.id, fi, delivery),
4103
4198
  timeoutSignal: timeoutAbortController.signal,
4104
4199
  stopSignal: stopAbortController.signal,
4105
4200
  trackedMutationEvidenceForCompletionGuard: Boolean(worktreeSetup),
@@ -4109,6 +4204,8 @@ export async function runSubagent(
4109
4204
  onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking, attempt.contextLimit),
4110
4205
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
4111
4206
  onExternalProcess: (process) => updateExternalProcess(fi, process),
4207
+ prepareExternalActivity: (externalCwd, signal) => prepareExternalActivity(fi, externalCwd, signal),
4208
+ onExternalStreamActivity: () => recordExternalStreamActivity(fi),
4112
4209
  onExternalJob: (externalJob) => updateExternalJob(fi, externalJob),
4113
4210
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(fi),
4114
4211
  usageBudgetExhausted: continuationUsageBudgetExhausted,
@@ -4124,7 +4221,7 @@ export async function runSubagent(
4124
4221
  const childInterrupted = singleResult.interrupted === true;
4125
4222
  const childStopped = singleResult.stopped === true;
4126
4223
 
4127
- requiredStatusStep(statusPayload, fi).status = stopped || childStopped ? "stopped" : timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
4224
+ requiredStatusStep(statusPayload, fi).status = stopped || childStopped ? "stopped" : timedOut ? "failed" : childInterrupted ? "paused" : singleResult.execution?.status === "partial" ? "partial" : singleResult.exitCode === 0 ? "complete" : "failed";
4128
4225
  requiredStatusStep(statusPayload, fi).endedAt = taskEndTime;
4129
4226
  requiredStatusStep(statusPayload, fi).durationMs = taskDuration;
4130
4227
  requiredStatusStep(statusPayload, fi).exitCode = stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
@@ -4137,8 +4234,7 @@ export async function runSubagent(
4137
4234
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "sessionName", singleResult.sessionName);
4138
4235
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "model", singleResult.model);
4139
4236
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "thinking", resolveEffectiveThinking(singleResult.model, requiredStatusStep(statusPayload, fi).thinking));
4140
- setOptionalProperty(requiredStatusStep(statusPayload, fi), "attemptedModels", singleResult.attemptedModels);
4141
- setOptionalProperty(requiredStatusStep(statusPayload, fi), "modelAttempts", singleResult.modelAttempts);
4237
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "requestedModel", singleResult.requestedModel);
4142
4238
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "contextOverflow", singleResult.contextOverflow);
4143
4239
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "totalCost", singleResult.totalCost);
4144
4240
  if (singleResult.totalCost) {
@@ -4205,7 +4301,7 @@ export async function runSubagent(
4205
4301
  const sessionTokens = config.sessionDir
4206
4302
  ? parseSessionTokens(path.join(config.sessionDir, `parallel-${t}`))
4207
4303
  : null;
4208
- const fallbackTokens = tokenUsageFromAttempts(parallelResults[t]?.modelAttempts);
4304
+ const fallbackTokens = tokenUsageFromUsage(parallelResults[t]?.usage);
4209
4305
  const observedTokens = requiredStatusStep(statusPayload, fi).tokens;
4210
4306
  const taskTokens = sessionTokens ?? (fallbackTokens
4211
4307
  ? { ...fallbackTokens, ...(observedTokens?.window !== undefined ? { window: observedTokens.window } : {}), ...(observedTokens?.windowPeak !== undefined ? { windowPeak: observedTokens.windowPeak } : {}) }
@@ -4236,7 +4332,7 @@ export async function runSubagent(
4236
4332
  output: pr.output,
4237
4333
  outputState: pr.outputState,
4238
4334
  error: pr.error,
4239
- success: pr.stopped !== true && pr.interrupted !== true && pr.exitCode === 0,
4335
+ success: pr.stopped !== true && pr.interrupted !== true && pr.exitCode === 0 && pr.execution?.status !== "partial",
4240
4336
  exitCode: pr.interrupted === true ? 0 : pr.exitCode,
4241
4337
  skipped: pr.skipped,
4242
4338
  interrupted: pr.interrupted,
@@ -4248,12 +4344,13 @@ export async function runSubagent(
4248
4344
  intercomTarget: pr.intercomTarget,
4249
4345
  model: pr.model,
4250
4346
  thinking: pr.thinking,
4251
- attemptedModels: pr.attemptedModels,
4252
- modelAttempts: pr.modelAttempts,
4347
+ requestedModel: pr.requestedModel,
4253
4348
  contextOverflow: pr.contextOverflow,
4254
4349
  totalCost: pr.totalCost,
4255
4350
  usage: pr.usage,
4256
4351
  artifactPaths: pr.artifactPaths,
4352
+ outputSaveError: pr.outputSaveError,
4353
+ artifactOutputSaveFailed: pr.artifactOutputSaveFailed,
4257
4354
  transcriptPath: pr.transcriptPath,
4258
4355
  transcriptError: pr.transcriptError,
4259
4356
  effects: pr.effects,
@@ -4286,7 +4383,7 @@ export async function runSubagent(
4286
4383
  exitCode: r.exitCode,
4287
4384
  error: r.error,
4288
4385
  model: r.model,
4289
- attemptedModels: r.attemptedModels,
4386
+ requestedModel: r.requestedModel,
4290
4387
  })),
4291
4388
  );
4292
4389
  if (worktreeSetup) {
@@ -4496,10 +4593,12 @@ export async function runSubagent(
4496
4593
  nestedRoute: config.nestedRoute,
4497
4594
  capabilityCeiling: config.capabilityCeiling,
4498
4595
  runFanoutBudget: config.runFanoutBudget,
4596
+ hostAvailableBuiltins: config.hostAvailableBuiltins,
4499
4597
  registerInterrupt: (interrupt) => registerStepInterrupt(flatIndex, interrupt),
4500
4598
  registerTimeout: (interrupt) => registerStepTimeout(flatIndex, interrupt),
4501
4599
  registerStop: (stop) => registerStepStop(flatIndex, stop),
4502
4600
  registerSteer: (steer) => registerStepSteer(flatIndex, steer),
4601
+ onSteerOutcome: (request, delivery) => applySteerDelivery(request.id, flatIndex, delivery),
4503
4602
  timeoutSignal: timeoutAbortController.signal,
4504
4603
  stopSignal: stopAbortController.signal,
4505
4604
  timeoutMessage,
@@ -4508,6 +4607,8 @@ export async function runSubagent(
4508
4607
  onAttemptStart: (attempt) => updateStepModel(flatIndex, attempt.model, attempt.thinking, attempt.contextLimit),
4509
4608
  onChildEvent: (event) => updateStepFromChildEvent(flatIndex, event),
4510
4609
  onExternalProcess: (process) => updateExternalProcess(flatIndex, process),
4610
+ prepareExternalActivity: (externalCwd, signal) => prepareExternalActivity(flatIndex, externalCwd, signal),
4611
+ onExternalStreamActivity: () => recordExternalStreamActivity(flatIndex),
4511
4612
  onExternalJob: (externalJob) => updateExternalJob(flatIndex, externalJob),
4512
4613
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(flatIndex),
4513
4614
  usageBudgetExhausted: continuationUsageBudgetExhausted,
@@ -4535,19 +4636,20 @@ export async function runSubagent(
4535
4636
  output: stopped || childStopped ? stopMessage : timedOut ? singleResult.output || (timeoutMessage ?? "Subagent timed out.") : singleResult.output,
4536
4637
  outputState: singleResult.outputState,
4537
4638
  error: stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error,
4538
- success: !stopped && !childStopped && !timedOut && singleResult.interrupted !== true && singleResult.exitCode === 0,
4639
+ success: !stopped && !childStopped && !timedOut && singleResult.interrupted !== true && singleResult.exitCode === 0 && singleResult.execution?.status !== "partial",
4539
4640
  exitCode: stopped || childStopped ? 1 : timedOut ? 1 : singleResult.interrupted === true ? 0 : singleResult.exitCode,
4540
4641
  sessionFile: singleResult.sessionFile,
4541
4642
  intercomTarget: singleResult.intercomTarget,
4542
4643
  model: singleResult.model,
4543
4644
  thinking: singleResult.thinking,
4544
- attemptedModels: singleResult.attemptedModels,
4545
- modelAttempts: singleResult.modelAttempts,
4645
+ requestedModel: singleResult.requestedModel,
4546
4646
  contextOverflow: singleResult.contextOverflow,
4547
4647
  totalCost: singleResult.totalCost,
4548
4648
  usage: singleResult.usage,
4549
4649
  artifactPaths: singleResult.artifactPaths,
4550
4650
  savedOutputPath: singleResult.savedOutputPath,
4651
+ outputSaveError: singleResult.outputSaveError,
4652
+ artifactOutputSaveFailed: singleResult.artifactOutputSaveFailed,
4551
4653
  transcriptPath: singleResult.transcriptPath,
4552
4654
  transcriptError: singleResult.transcriptError,
4553
4655
  effects: singleResult.effects,
@@ -4592,7 +4694,7 @@ export async function runSubagent(
4592
4694
  if (cumulativeTokens) {
4593
4695
  previousCumulativeTokens = cumulativeTokens;
4594
4696
  } else {
4595
- const fallbackTokens = tokenUsageFromAttempts(singleResult.modelAttempts);
4697
+ const fallbackTokens = tokenUsageFromUsage(singleResult.usage);
4596
4698
  const observedTokens = requiredStatusStep(statusPayload, flatIndex).tokens;
4597
4699
  stepTokens = fallbackTokens
4598
4700
  ? { ...fallbackTokens, ...(observedTokens?.window !== undefined ? { window: observedTokens.window } : {}), ...(observedTokens?.windowPeak !== undefined ? { windowPeak: observedTokens.windowPeak } : {}) }
@@ -4612,7 +4714,7 @@ export async function runSubagent(
4612
4714
 
4613
4715
  const stepEndTime = Date.now();
4614
4716
  const childInterrupted = singleResult.interrupted === true;
4615
- requiredStatusStep(statusPayload, flatIndex).status = stopped || childStopped ? "stopped" : timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
4717
+ requiredStatusStep(statusPayload, flatIndex).status = stopped || childStopped ? "stopped" : timedOut ? "failed" : childInterrupted ? "paused" : singleResult.execution?.status === "partial" ? "partial" : singleResult.exitCode === 0 ? "complete" : "failed";
4616
4718
  requiredStatusStep(statusPayload, flatIndex).endedAt = stepEndTime;
4617
4719
  requiredStatusStep(statusPayload, flatIndex).durationMs = stepEndTime - stepStartTime;
4618
4720
  requiredStatusStep(statusPayload, flatIndex).exitCode = stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
@@ -4625,8 +4727,7 @@ export async function runSubagent(
4625
4727
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "sessionName", singleResult.sessionName);
4626
4728
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "model", singleResult.model);
4627
4729
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "thinking", resolveEffectiveThinking(singleResult.model, requiredStatusStep(statusPayload, flatIndex).thinking));
4628
- setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "attemptedModels", singleResult.attemptedModels);
4629
- setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "modelAttempts", singleResult.modelAttempts);
4730
+ setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "requestedModel", singleResult.requestedModel);
4630
4731
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "contextOverflow", singleResult.contextOverflow);
4631
4732
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "totalCost", singleResult.totalCost);
4632
4733
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "error", stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error);
@@ -4741,6 +4842,34 @@ export async function runSubagent(
4741
4842
  }
4742
4843
  }
4743
4844
 
4845
+ await Promise.all([...externalActivityEvidence.values()].map((evidence) => evidence.probeInFlight).filter((probe): probe is Promise<void> => probe !== undefined));
4846
+ if (periodicGitProbeFailure) {
4847
+ stopped = false;
4848
+ timedOut = false;
4849
+ interrupted = false;
4850
+ delete statusPayload.stopped;
4851
+ delete statusPayload.timedOut;
4852
+ statusPayload.error = periodicGitProbeFailure.message;
4853
+ const step = statusPayload.steps[periodicGitProbeFailure.index];
4854
+ if (step) {
4855
+ step.status = "failed";
4856
+ step.error = periodicGitProbeFailure.message;
4857
+ step.exitCode = 1;
4858
+ delete step.stopped;
4859
+ delete step.timedOut;
4860
+ }
4861
+ const result = results[periodicGitProbeFailure.index];
4862
+ if (result) {
4863
+ result.success = false;
4864
+ result.exitCode = 1;
4865
+ result.error = periodicGitProbeFailure.message;
4866
+ result.output = periodicGitProbeFailure.message;
4867
+ result.stopped = false;
4868
+ result.timedOut = false;
4869
+ result.interrupted = false;
4870
+ }
4871
+ }
4872
+
4744
4873
  let summary = results.map((r) => `${r.agent}:\n${r.output || (r.exitCode !== 0 ? r.error : undefined) || "(no output)"}`).join("\n\n");
4745
4874
  let truncated = false;
4746
4875
 
@@ -4806,9 +4935,15 @@ export async function runSubagent(
4806
4935
  clearTimeout(timeoutTimer);
4807
4936
  timeoutTimer = undefined;
4808
4937
  }
4938
+ if (checkpointTimer) {
4939
+ clearTimeout(checkpointTimer);
4940
+ checkpointTimer = undefined;
4941
+ }
4809
4942
  if (!timedOut && !stopped && !interrupted && config.timeoutMs !== undefined && timeoutMessage !== undefined && results.some((result) => result.timedOut === true && result.error?.startsWith(timeoutMessage))) {
4810
4943
  timedOut = true;
4811
4944
  }
4945
+ disposeControlInbox();
4946
+ for (const request of consumeStopRequestPayloads(asyncDir)) stopChildStep(request);
4812
4947
  const signalTerminated = !stopped && !timedOut && !interrupted && results.some((result) => result.exitCode !== 0 && isUnexplainedProcessSignal(omitUndefinedProperties({
4813
4948
  processSignal: result.processSignal,
4814
4949
  interrupted: result.interrupted,
@@ -4823,7 +4958,6 @@ export async function runSubagent(
4823
4958
  });
4824
4959
  statusPayload.state = stopped || signalTerminated ? "stopped" : timedOut || usageBudgetExceeded ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : partialWithEvidence ? "partial" : "failed";
4825
4960
  closeSteerInbox(asyncDir, statusPayload.state, (filePath, payload) => runPersistence.write(filePath, payload));
4826
- disposeControlInbox();
4827
4961
  for (const request of consumeSteerRequests(asyncDir)) deliverSteerRequest(request);
4828
4962
  const effectiveSessionFile = sessionFile ?? latestSessionFile;
4829
4963
  const steeringLifecycle = steeringStatus(statusPayload);
@@ -4832,7 +4966,7 @@ export async function runSubagent(
4832
4966
  for (const target of request.targets) {
4833
4967
  if (target.state !== "scheduled" && target.state !== "routed" && target.state !== "queued") continue;
4834
4968
  changed = true;
4835
- const reason = target.state === "queued" ? "run ended before queued follow-up delivery" : "child terminated before steering delivery";
4969
+ const reason = target.state === "queued" ? unconsumedSteerReason() : "child terminated before steering delivery";
4836
4970
  updateSteeringLifecycleTarget(request.id, target.index, "failed", Date.now(), { reason });
4837
4971
  markSteeringAttention(target.index);
4838
4972
  emitSteeringEvent("subagent.steer.failed", { type: "steer", id: request.id, ts: request.requestedAt, message: reason }, target.index, { reason });
@@ -4859,7 +4993,7 @@ export async function runSubagent(
4859
4993
  statusPayload.activityState = "needs_attention";
4860
4994
  statusPayload.error = partialResult?.error ?? statusPayload.error;
4861
4995
  for (const step of statusPayload.steps) {
4862
- if (step.status === "failed") step.activityState = "needs_attention";
4996
+ if (step.status === "failed" || step.status === "partial") step.activityState = "needs_attention";
4863
4997
  }
4864
4998
  }
4865
4999
  statusPayload.endedAt = runEndedAt;
@@ -4916,14 +5050,14 @@ export async function runSubagent(
4916
5050
  intercomTarget: r.intercomTarget,
4917
5051
  model: r.model,
4918
5052
  thinking: r.thinking,
4919
- attemptedModels: r.attemptedModels,
4920
- modelAttempts: r.modelAttempts,
5053
+ requestedModel: r.requestedModel,
4921
5054
  contextOverflow: r.contextOverflow,
4922
5055
  totalCost: r.totalCost,
4923
5056
  usage: r.usage,
4924
5057
  artifactPaths: r.artifactPaths,
4925
5058
  savedOutputPath: r.savedOutputPath,
4926
5059
  outputSaveError: r.outputSaveError,
5060
+ artifactOutputSaveFailed: r.artifactOutputSaveFailed,
4927
5061
  metadataSaveError: r.metadataSaveError,
4928
5062
  truncated: r.truncated,
4929
5063
  transcriptPath: r.transcriptPath,
@@ -5059,7 +5193,7 @@ export async function runSubagent(
5059
5193
  async function waitForStartupControl(
5060
5194
  controlPath: string,
5061
5195
  token: string,
5062
- action: "ack" | "proceed",
5196
+ action: "ack" | "confirm" | "proceed",
5063
5197
  timeoutMs = 30_000,
5064
5198
  ): Promise<void> {
5065
5199
  const deadline = Date.now() + timeoutMs;
@@ -5073,14 +5207,15 @@ async function waitForStartupControl(
5073
5207
  }
5074
5208
  if (payload.token !== token) throw new Error("Runner startup control token does not match.");
5075
5209
  if (payload.action === action) return;
5076
- if (payload.action !== "ack" && payload.action !== "proceed") throw new Error("Runner startup control action is invalid.");
5210
+ if (payload.action !== "ack" && payload.action !== "confirm" && payload.action !== "proceed") throw new Error("Runner startup control action is invalid.");
5077
5211
  }
5078
5212
  await new Promise((resolve) => setTimeout(resolve, 20));
5079
5213
  }
5080
5214
  throw new Error(`Timed out after ${timeoutMs}ms waiting for runner startup control '${action}'.`);
5081
5215
  }
5082
5216
 
5083
- async function runConfiguredSubagent(config: SubagentRunConfig): Promise<void> {
5217
+ // Both hosts enter here: startup authorization, revival leases and disposal stay shared.
5218
+ export async function runConfiguredSubagent(config: SubagentRunConfig, options?: DefaultChildSessionFactoryOptions): Promise<void> {
5084
5219
  let lease: ReturnType<typeof acquireSessionLease> | undefined;
5085
5220
  let startupCommitted = config.revivalLease === undefined && config.launchBarrierToken === undefined;
5086
5221
  const startupPath = path.join(config.asyncDir, "runner-startup.json");
@@ -5109,6 +5244,8 @@ async function runConfiguredSubagent(config: SubagentRunConfig): Promise<void> {
5109
5244
  writeAtomicJson(startupPath, { state: "ready", token: lease.owner.token, pid: process.pid, owner: lease.owner });
5110
5245
  await waitForStartupControl(startupAckPath, lease.owner.token, "ack");
5111
5246
  writeAtomicJson(startupPath, { state: "acknowledged", token: lease.owner.token, pid: process.pid });
5247
+ await waitForStartupControl(startupAckPath, lease.owner.token, "confirm");
5248
+ writeAtomicJson(startupPath, { state: "confirmed", token: lease.owner.token, pid: process.pid });
5112
5249
  await waitForStartupControl(startupProceedPath, lease.owner.token, "proceed");
5113
5250
  startupCommitted = true;
5114
5251
  for (const controlPath of [startupAckPath, startupProceedPath]) {
@@ -5119,7 +5256,7 @@ async function runConfiguredSubagent(config: SubagentRunConfig): Promise<void> {
5119
5256
  }
5120
5257
  }
5121
5258
  }
5122
- const childSessions = await loadRunnerChildSessionFactory(config);
5259
+ const childSessions = await loadRunnerChildSessionFactory(config, options);
5123
5260
  try {
5124
5261
  await runSubagent(config, childSessions);
5125
5262
  } finally {