pi-subagents 0.67.0 → 0.69.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 (122) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +1 -1
  3. package/docs/agents.md +41 -12
  4. package/docs/configuration.md +61 -19
  5. package/docs/extension-api.md +5 -1
  6. package/docs/missions.md +2 -2
  7. package/docs/models.md +11 -79
  8. package/docs/observability.md +18 -8
  9. package/docs/standalone-background.md +13 -3
  10. package/docs/tool-reference.md +38 -14
  11. package/docs/watchdog.md +10 -12
  12. package/docs/workflows.md +59 -1
  13. package/index.ts +5 -2
  14. package/package.json +4 -2
  15. package/runner-peer-loader.mjs +24 -0
  16. package/runner-peer-preload.mjs +25 -11
  17. package/skills/pi-subagents/SKILL.md +18 -21
  18. package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
  19. package/skills/pi-subagents/references/execution-controls.md +6 -4
  20. package/skills/pi-subagents/references/management-authoring-rpc.md +0 -1
  21. package/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
  22. package/skills/pi-subagents/references/prompting-and-roles.md +16 -12
  23. package/skills/pi-subagents/references/review-and-validation.md +3 -3
  24. package/src/agents/agent-management.ts +57 -58
  25. package/src/agents/agent-serializer.ts +4 -3
  26. package/src/agents/agents.ts +185 -72
  27. package/src/agents/chain-serializer.ts +5 -0
  28. package/src/agents/runtime-agent-registry.ts +7 -6
  29. package/src/agents/skills.ts +1 -1
  30. package/src/api/preflight.ts +20 -16
  31. package/src/api/required-child-extensions.ts +6 -0
  32. package/src/extension/config.ts +10 -37
  33. package/src/extension/fanout-child.ts +3 -0
  34. package/src/extension/herdr-pi-bridge.ts +160 -0
  35. package/src/extension/index.ts +42 -31
  36. package/src/extension/public-execution.ts +3 -3
  37. package/src/extension/schemas.ts +23 -6
  38. package/src/extension/tool-description.ts +8 -7
  39. package/src/inspectors/ghostty/plugin.ts +13 -1
  40. package/src/intercom/native-supervisor-channel.ts +22 -18
  41. package/src/policy/authority.ts +4 -0
  42. package/src/profiles/profiles.ts +12 -6
  43. package/src/runs/background/active-run-index.ts +17 -1
  44. package/src/runs/background/async-execution.ts +309 -126
  45. package/src/runs/background/async-job-tracker.ts +8 -6
  46. package/src/runs/background/async-resume.ts +13 -4
  47. package/src/runs/background/async-status.ts +15 -4
  48. package/src/runs/background/auto-drain.ts +20 -10
  49. package/src/runs/background/binary-bootstrap.ts +5 -0
  50. package/src/runs/background/chain-append.ts +1 -1
  51. package/src/runs/background/chain-root-attachment.ts +14 -33
  52. package/src/runs/background/notify.ts +74 -6
  53. package/src/runs/background/result-files.ts +8 -4
  54. package/src/runs/background/result-watcher.ts +19 -2
  55. package/src/runs/background/run-child-session.ts +20 -29
  56. package/src/runs/background/runner-aliases.ts +4 -33
  57. package/src/runs/background/runner-child-launch.ts +4 -1
  58. package/src/runs/background/runner-child-sessions.ts +2 -2
  59. package/src/runs/background/runner-http-dispatcher.ts +119 -0
  60. package/src/runs/background/scheduled-runs.ts +11 -5
  61. package/src/runs/background/stale-run-reconciler.ts +35 -11
  62. package/src/runs/background/subagent-runner.ts +413 -276
  63. package/src/runs/background/subagent-wait.ts +128 -23
  64. package/src/runs/background/wait-completions.ts +75 -27
  65. package/src/runs/background/wait-subscriptions.ts +9 -3
  66. package/src/runs/background/wait-tool.ts +4 -2
  67. package/src/runs/foreground/async-stop-action.ts +93 -3
  68. package/src/runs/foreground/execution.ts +115 -219
  69. package/src/runs/foreground/foreground-history.ts +2 -1
  70. package/src/runs/foreground/subagent-executor.ts +281 -80
  71. package/src/runs/shared/acceptance.ts +194 -37
  72. package/src/runs/shared/async-status-projection.ts +123 -33
  73. package/src/runs/shared/child-launch-plan.ts +15 -3
  74. package/src/runs/shared/child-launch.ts +19 -6
  75. package/src/runs/shared/child-runtime-config.ts +5 -0
  76. package/src/runs/shared/child-session.ts +94 -50
  77. package/src/runs/shared/child-tool-plan.ts +28 -16
  78. package/src/runs/shared/dynamic-fanout.ts +2 -2
  79. package/src/runs/shared/external-cli-contract.ts +11 -1
  80. package/src/runs/shared/external-cli-preflight.ts +6 -2
  81. package/src/runs/shared/herdr-connection.ts +134 -0
  82. package/src/runs/shared/herdr-external-adapters.ts +169 -0
  83. package/src/runs/shared/herdr-machine.ts +279 -0
  84. package/src/runs/shared/herdr-pi-protocol.ts +59 -0
  85. package/src/runs/shared/herdr-placed-run.ts +263 -0
  86. package/src/runs/shared/model-resolution-diagnostic.ts +76 -0
  87. package/src/runs/shared/{model-fallback.ts → model-resolution.ts} +22 -237
  88. package/src/runs/shared/model-scope.ts +1 -1
  89. package/src/runs/shared/nested-events.ts +11 -2
  90. package/src/runs/shared/parallel-utils.ts +7 -2
  91. package/src/runs/shared/pi-spawn.ts +1 -1
  92. package/src/runs/shared/subagent-prompt-runtime.ts +4 -2
  93. package/src/runs/shared/worktree-setup-command.ts +27 -4
  94. package/src/runs/shared/worktree.ts +30 -8
  95. package/src/shared/child-cache-retention.ts +43 -0
  96. package/src/shared/launch-contract.ts +6 -9
  97. package/src/shared/pruned-fork.ts +1 -1
  98. package/src/shared/required-child-extensions.ts +81 -0
  99. package/src/shared/settings.ts +5 -2
  100. package/src/shared/shortcuts.ts +0 -4
  101. package/src/shared/types.ts +81 -29
  102. package/src/slash/slash-commands.ts +0 -6
  103. package/src/slash/subagents-admin.ts +13 -9
  104. package/src/tui/render.ts +20 -10
  105. package/src/watchdog/child-status.ts +28 -36
  106. package/src/watchdog/lsp-diagnostics.ts +1 -1
  107. package/src/watchdog/model-selection.ts +1 -1
  108. package/src/watchdog/register-child.ts +10 -3
  109. package/src/watchdog/register-main.ts +20 -20
  110. package/src/watchdog/render.ts +1 -1
  111. package/src/watchdog/review.ts +14 -30
  112. package/src/watchdog/rules.ts +1 -1
  113. package/src/watchdog/runtime.ts +23 -12
  114. package/src/watchdog/settings.ts +3 -6
  115. package/src/watchdog/types.ts +3 -5
  116. package/src/watchdog/warning-format.ts +1 -1
  117. package/src/workflows/scripted-workflow.ts +68 -7
  118. package/src/workflows/workflow-receipt.ts +21 -3
  119. package/src/workflows/workflow-resources.ts +13 -2
  120. package/src/runs/shared/model-exclusions.ts +0 -374
  121. package/src/runs/shared/readonly-model-continuation.ts +0 -69
  122. package/src/runs/shared/readonly-session-evidence.ts +0 -307
@@ -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,
@@ -97,9 +85,7 @@ 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
87
  import type { ChildSessionFactory, DefaultChildSessionFactoryOptions } from "../shared/child-session.ts";
100
- import { getSettledReadonlyChild, runChildSession, type ChildEvent, type RunChildSessionInput, type RunChildSessionResult, type SteerDelivery, 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";
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
103
  import { createSteeringStatus, recordSteeringRequest, steeringStatus, terminalSteeringNoticeState, unconsumedSteerReason, updateSteeringTarget } from "./steering.ts";
118
- import { PROMPT_REDACTED, detectSubagentError, extractTextFromContent, extractToolArgsPreview, formatEmptyTerminalAssistantResponseError, getFinalOutput, hasEmptyTerminalAssistantResponse, readStatus } from "../../shared/utils.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";
@@ -150,7 +136,7 @@ import { assertThinkingWithinCeiling } from "../../shared/thinking-ceiling.ts";
150
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
- import { acceptanceFailureMessage, aggregateAcceptanceReport, buildSkippedAcceptanceLedger, evaluateAcceptance, formatAcceptancePrompt, resolveAcceptanceReportMode, resolveEffectiveAcceptance, stripAcceptanceReport } from "../shared/acceptance.ts";
139
+ import { acceptanceFailureMessage, aggregateAcceptanceReport, buildSkippedAcceptanceLedger, captureStagedIndexBaseline, evaluateAcceptance, formatAcceptancePrompt, resolveAcceptanceReportMode, resolveEffectiveAcceptance, stripAcceptanceReport, typedVerifyOutput } from "../shared/acceptance.ts";
154
140
  import { attachContractProjections, isAgentContract } from "../shared/agent-contract.ts";
155
141
  import { waitForImportedAsyncRoot } from "./chain-root-attachment.ts";
156
142
  import { appendRunnerStepsToStatus, consumeChainAppendRequests, countPendingChainAppendRequests, statusStepDescription } from "./chain-append.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";
@@ -224,6 +212,8 @@ export 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;
@@ -272,14 +262,15 @@ interface StepResult {
272
262
  sessionFile?: string;
273
263
  intercomTarget?: string;
274
264
  model?: string;
265
+ nativeMachine?: import("../../shared/types.ts").SingleResult["nativeMachine"];
275
266
  thinking?: string;
276
- attemptedModels?: string[];
277
- modelAttempts?: ModelAttempt[];
267
+ requestedModel?: string;
278
268
  /** True when the dispatch failed because the input exceeded the model's context window. */
279
269
  contextOverflow?: boolean;
280
270
  totalCost?: CostSummary;
281
271
  artifactPaths?: ArtifactPaths;
282
272
  outputSaveError?: string;
273
+ artifactOutputSaveFailed?: true;
283
274
  metadataSaveError?: string;
284
275
  truncated?: boolean;
285
276
  transcriptPath?: string;
@@ -422,50 +413,22 @@ function emptyUsage(): Usage {
422
413
  return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
423
414
  }
424
415
 
425
- function tokenUsageFromAttempts(attempts: ModelAttempt[] | undefined): TokenUsage | null {
426
- if (!attempts || attempts.length === 0) return null;
427
- let input = 0;
428
- let output = 0;
429
- for (const attempt of attempts) {
430
- input += attempt.usage?.input ?? 0;
431
- output += attempt.usage?.output ?? 0;
432
- }
416
+ function tokenUsageFromUsage(usage: Usage | undefined): TokenUsage | null {
417
+ const input = usage?.input ?? 0;
418
+ const output = usage?.output ?? 0;
433
419
  const total = input + output;
434
420
  return total > 0 ? { input, output, total } : null;
435
421
  }
436
422
 
437
- function costSummaryFromAttempts(attempts: ModelAttempt[] | undefined): CostSummary | undefined {
438
- if (!attempts || attempts.length === 0) return undefined;
439
- let inputTokens = 0;
440
- let outputTokens = 0;
441
- let costUsd = 0;
442
- for (const attempt of attempts) {
443
- inputTokens += attempt.usage?.input ?? 0;
444
- outputTokens += attempt.usage?.output ?? 0;
445
- costUsd += attempt.usage?.cost ?? 0;
446
- }
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;
447
427
  return inputTokens > 0 || outputTokens > 0 || costUsd > 0
448
428
  ? { inputTokens, outputTokens, costUsd }
449
429
  : undefined;
450
430
  }
451
431
 
452
- function usageFromAttempts(attempts: ModelAttempt[] | undefined): Usage | undefined {
453
- if (!attempts || attempts.length === 0) return undefined;
454
- const usage = emptyUsage();
455
- for (const attempt of attempts) {
456
- if (!attempt.usage) continue;
457
- usage.input += attempt.usage.input;
458
- usage.output += attempt.usage.output;
459
- usage.cacheRead += attempt.usage.cacheRead;
460
- usage.cacheWrite += attempt.usage.cacheWrite;
461
- usage.cost += attempt.usage.cost;
462
- usage.turns += attempt.usage.turns;
463
- }
464
- return usage.input !== 0 || usage.output !== 0 || usage.cacheRead !== 0 || usage.cacheWrite !== 0 || usage.cost !== 0 || usage.turns !== 0
465
- ? usage
466
- : undefined;
467
- }
468
-
469
432
  function appendRecentStepOutput(step: RunnerStatusStep, lines: string[]): void {
470
433
  const nonEmpty = lines.filter((line) => line.trim());
471
434
  if (nonEmpty.length === 0) return;
@@ -659,6 +622,38 @@ function writeRunLog(
659
622
  write(logPath, lines.join("\n"));
660
623
  }
661
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
+
662
657
  /** Context for running a single step */
663
658
  interface SingleStepContext {
664
659
  previousOutput: string;
@@ -703,6 +698,8 @@ interface SingleStepContext {
703
698
  onAttemptStart?: (attempt: { model?: string; thinking?: string; contextLimit?: number }) => void;
704
699
  onChildEvent?: (event: ChildEvent) => void;
705
700
  onExternalProcess?: (process: ExternalProcessStatus) => void;
701
+ prepareExternalActivity?: (cwd: string, signal: AbortSignal) => Promise<void>;
702
+ onExternalStreamActivity?: () => void;
706
703
  onExternalJob?: (status: ExternalJobStatus) => void;
707
704
  skipAcceptance?: () => boolean;
708
705
  /** Authoritative owner decision after event delivery; undefined includes incomplete run-wide usage. */
@@ -714,7 +711,26 @@ interface SingleStepContext {
714
711
  orcaProgressTab?: OrcaProgressTab;
715
712
  }
716
713
 
717
- /** 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
+
718
734
  export async function runSingleStepInner(
719
735
  step: SubagentStep,
720
736
  ctx: SingleStepContext,
@@ -773,8 +789,7 @@ export async function runSingleStepInner(
773
789
  sessionFile: imported.sessionFile,
774
790
  intercomTarget: imported.intercomTarget,
775
791
  model: imported.model,
776
- attemptedModels: imported.attemptedModels,
777
- modelAttempts: imported.modelAttempts,
792
+ requestedModel: imported.requestedModel,
778
793
  contextOverflow: imported.contextOverflow,
779
794
  totalCost: imported.totalCost,
780
795
  usage: imported.usage,
@@ -808,13 +823,13 @@ export async function runSingleStepInner(
808
823
  subagentOnlyExtensions: step.subagentOnlyExtensions,
809
824
  fast: step.fast,
810
825
  model: step.model,
811
- modelCandidates: step.modelCandidates,
812
826
  mcpDirectTools: step.mcpDirectTools,
813
827
  cwd: step.cwd ?? ctx.cwd,
814
828
  requireReadTool: Boolean(step.skills?.length),
815
829
  structuredOutput: Boolean(effectiveStructuredOutput),
816
830
  capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
817
831
  inheritedCapabilityCeiling: ctx.inheritedChildRuntime?.capabilityCeiling,
832
+ requiredExtensions: step.requiredExtensions ?? ctx.inheritedChildRuntime?.requiredExtensions,
818
833
  permissionRules: step.permissionRules,
819
834
  hostAvailableBuiltins: ctx.hostAvailableBuiltins,
820
835
  }));
@@ -874,7 +889,53 @@ export async function runSingleStepInner(
874
889
  transcriptWriter?.writeInitialUserMessage(`${PROMPT_REDACTED}; live Prompt Audit only.`);
875
890
 
876
891
  if (step.runner?.type === "external-cli") {
877
- 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
+ }
878
939
  const adapterLaunch = step.runner.adapter === "codex-exec" || step.runner.adapter === "codex-exec-writer"
879
940
  ? resolveCodexExecLaunch({ adapter: step.runner.adapter, command: step.runner.command, asyncDir: path.dirname(ctx.outputFile), stepIndex: ctx.flatIndex })
880
941
  : step.runner.adapter === "claude-code" || step.runner.adapter === "claude-code-writer"
@@ -882,9 +943,13 @@ export async function runSingleStepInner(
882
943
  : step.runner.adapter === "cursor-agent" || step.runner.adapter === "cursor-agent-writer"
883
944
  ? resolveCursorAgentLaunch({ adapter: step.runner.adapter, command: step.runner.command, cwd: externalCwd, asyncDir: path.dirname(ctx.outputFile), stepIndex: ctx.flatIndex })
884
945
  : undefined;
885
- const runner = resolveExternalCliRunnerStatus({ ...step.runner, ...(adapterLaunch ? { args: adapterLaunch.args } : {}) });
946
+ const runner = resolveExternalCliRunnerStatus({ ...step.runner, ...(adapterLaunch ? { args: adapterLaunch.args } : {}), ...(step.machine ? { machine: step.machine } : {}) });
886
947
  const outputSnapshot = captureSingleOutputSnapshot(step.outputPath);
887
- 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({
888
953
  command: adapterLaunch?.command ?? runner.command,
889
954
  args: adapterLaunch?.args ?? runner.args,
890
955
  cwd: externalCwd,
@@ -902,9 +967,13 @@ export async function runSingleStepInner(
902
967
  timeoutMessage: ctx.timeoutMessage,
903
968
  stopMessage: ctx.stopMessage,
904
969
  onProcess: ctx.onExternalProcess,
905
- onStdout: (chunk) => ctx.orcaProgressTab?.append(chunk.toString("utf-8")),
906
- onStderr: (chunk) => ctx.orcaProgressTab?.append(chunk.toString("utf-8")),
907
- }));
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;
908
977
  try { fs.writeFileSync(ctx.outputFile, external.output, "utf-8"); } catch { /* Observability output is best-effort. */ }
909
978
  const resolvedOutput = step.outputPath && external.exitCode === 0
910
979
  ? resolveSingleOutput(step.outputPath, external.output, outputSnapshot, step.outputClaimPath)
@@ -928,7 +997,7 @@ export async function runSingleStepInner(
928
997
  artifactPaths,
929
998
  artifactConfig: ctx.artifactConfig,
930
999
  output: formatOutputArtifactContent(omitUndefinedProperties({ output: resolvedOutput.fullOutput, error: external.error, metadataPath: ctx.artifactConfig?.includeMetadata === false ? undefined : artifactPaths.metadataPath })),
931
- 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() },
932
1001
  })
933
1002
  : {};
934
1003
  return omitUndefinedProperties({
@@ -944,9 +1013,10 @@ export async function runSingleStepInner(
944
1013
  processSignal: external.processSignal,
945
1014
  artifactPaths,
946
1015
  outputSaveError: [resolvedOutput.saveError, artifactErrors.outputSaveError].filter(Boolean).join("\n") || undefined,
1016
+ artifactOutputSaveFailed: artifactErrors.outputSaveError ? true : undefined,
947
1017
  metadataSaveError: artifactErrors.metadataSaveError,
948
1018
  runner,
949
- externalProcess: external.externalProcess,
1019
+ externalProcess: externalProcess,
950
1020
  });
951
1021
  }
952
1022
 
@@ -1013,6 +1083,7 @@ export async function runSingleStepInner(
1013
1083
  stopped: external.stopped,
1014
1084
  artifactPaths,
1015
1085
  outputSaveError: [resolvedOutput.saveError, artifactErrors.outputSaveError].filter(Boolean).join("\n") || undefined,
1086
+ artifactOutputSaveFailed: artifactErrors.outputSaveError ? true : undefined,
1016
1087
  metadataSaveError: artifactErrors.metadataSaveError,
1017
1088
  runner,
1018
1089
  externalJob: external.externalJob,
@@ -1026,16 +1097,9 @@ export async function runSingleStepInner(
1026
1097
  alignForkedSessionCwd(step.sessionFile, effectiveCwd);
1027
1098
  }
1028
1099
 
1029
- const candidates = step.modelCandidates !== undefined
1030
- ? step.modelCandidates.length > 0 ? step.modelCandidates : [undefined]
1031
- : step.model
1032
- ? [step.model]
1033
- : [undefined];
1034
- const attemptedModels: string[] = [];
1100
+ const candidate = step.model;
1035
1101
  let capabilityAudit: import("../shared/capability-ceiling.ts").SubagentCapabilityAudit | undefined;
1036
1102
  let launchResolvedExtensions = step.launchResolvedExtensions;
1037
- const modelAttempts: ModelAttempt[] = [];
1038
- const attemptNotes: string[] = [];
1039
1103
  let finalRequiredOutputMissing: boolean | undefined;
1040
1104
  const eventsPath = path.join(path.dirname(ctx.outputFile), "events.jsonl");
1041
1105
  let finalResult: RunChildSessionResult | undefined;
@@ -1046,48 +1110,22 @@ export async function runSingleStepInner(
1046
1110
  let toolBudget = step.toolBudget ? initialToolBudgetState(step.toolBudget) : undefined;
1047
1111
  let toolBudgetBlocked = false;
1048
1112
  let actualLaunchContractDigest = step.launchContractDigest;
1049
- 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);
1050
1114
  let finalMutationEvidence = collectTrackedMutationEvidence(mutationSnapshot, step.cwd ?? ctx.cwd);
1051
1115
 
1052
- let modelIndex = 0;
1053
1116
  let contextOverflow = false;
1054
1117
  let launchWarningsEmitted = false;
1055
- let recoveryState: LogicalRecoveryState = "unused";
1056
- let readonlyContinuation: RunChildSessionInput["readonlyContinuation"];
1057
- const continuationBudget = () => {
1058
- if (step.toolBudget) return "tool-budget-configured" as const;
1059
- // Refresh the authoritative run ledger, already fed synchronously by onChildEvent.
1060
- const exhausted = ctx.usageBudgetExhausted?.();
1061
- if (exhausted === true) return "exhausted" as const;
1062
- if (!ctx.usageBudget) return "unconfigured" as const;
1063
- if (ctx.usageBudget.costUsd || !ctx.onChildEvent || exhausted !== false) return "unknown" as const;
1064
- return "available" as const;
1065
- };
1066
- const lifecycleAllowsContinuation = () => !ctx.timeoutSignal?.aborted && !ctx.stopSignal?.aborted
1067
- && !ctx.skipAcceptance?.() && (ctx.deadlineAt === undefined || Date.now() < ctx.deadlineAt);
1068
- const canContinue = () => lifecycleAllowsContinuation() && ["available", "unconfigured"].includes(continuationBudget());
1069
- const failContinuationLaunch = (candidate: string | undefined, error: unknown) => {
1070
- const message = error instanceof Error ? error.message : String(error);
1071
- modelAttempts.push({ model: candidate ?? "default", success: false, exitCode: 1, error: message });
1072
- if (candidate) attemptedModels.push(candidate);
1073
- if (finalResult) finalResult = { ...finalResult, exitCode: 1, error: message };
1074
- };
1075
- let nextAttemptTask = task;
1076
- modelAttemptsLoop: while (modelIndex < candidates.length) {
1077
- if (ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break;
1078
- if (readonlyContinuation && !canContinue()) break;
1079
- const recoveringAbort = recoveryState === "abort-recovery";
1080
- const attemptTask = nextAttemptTask;
1081
- const candidate = candidates[modelIndex];
1082
- const expectedModelForVerification = candidate && !(step.skipPrimaryModelVerification && modelIndex === 0) ? candidate : undefined;
1118
+ const aggregateUsage = emptyUsage();
1119
+ let launched = false;
1120
+ let recoveryTask = task;
1121
+ let stagedIndexBaseline: string | undefined;
1122
+ singleLaunch: for (let attemptIndex = 0; attemptIndex < 2; attemptIndex++) {
1123
+ if (ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break singleLaunch;
1124
+ const expectedModelForVerification = candidate && !step.skipPrimaryModelVerification ? candidate : undefined;
1083
1125
  try {
1084
1126
  assertThinkingWithinCeiling({ model: candidate, configThinking: step.thinking, ceiling: step.thinkingCeiling, agent: step.agent, runId: ctx.id });
1085
1127
  } catch (error) {
1086
1128
  const message = error instanceof Error ? error.message : String(error);
1087
- if (readonlyContinuation) {
1088
- failContinuationLaunch(candidate, error);
1089
- break modelAttemptsLoop;
1090
- }
1091
1129
  return omitUndefinedProperties({ agent: step.agent, output: message, error: message, exitCode: 1, context: step.context, thinkingCeiling: step.thinkingCeiling });
1092
1130
  }
1093
1131
  ctx.onAttemptStart?.(omitUndefinedProperties({
@@ -1124,11 +1162,7 @@ export async function runSingleStepInner(
1124
1162
  childWatchdog,
1125
1163
  watchdogStatus: (event) => watchdogSink?.(event),
1126
1164
  });
1127
- } catch (error) {
1128
- if (!readonlyContinuation) throw error;
1129
- failContinuationLaunch(candidate, error);
1130
- break modelAttemptsLoop;
1131
- }
1165
+ } catch (error) { throw error; }
1132
1166
  if (effectiveStructuredOutput && launch.config.structuredOutput) {
1133
1167
  // The runner reads the value back from the runtime's files after the run.
1134
1168
  launch.config.structuredOutput.capture = createStructuredOutputFileCapture(effectiveStructuredOutput);
@@ -1147,13 +1181,13 @@ export async function runSingleStepInner(
1147
1181
  subagentOnlyExtensions: step.subagentOnlyExtensions,
1148
1182
  fast: step.fast,
1149
1183
  model: step.model,
1150
- modelCandidates: step.modelCandidates,
1151
1184
  mcpDirectTools: step.mcpDirectTools,
1152
1185
  cwd: step.cwd ?? ctx.cwd,
1153
1186
  requireReadTool: Boolean(step.skills?.length),
1154
1187
  structuredOutput: Boolean(effectiveStructuredOutput),
1155
1188
  capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
1156
1189
  inheritedCapabilityCeiling: ctx.inheritedChildRuntime?.capabilityCeiling,
1190
+ requiredExtensions: step.requiredExtensions ?? ctx.inheritedChildRuntime?.requiredExtensions,
1157
1191
  permissionRules: step.permissionRules,
1158
1192
  hostAvailableBuiltins: ctx.hostAvailableBuiltins,
1159
1193
  }));
@@ -1165,7 +1199,7 @@ export async function runSingleStepInner(
1165
1199
  inheritGlobalContext: step.inheritGlobalContext,
1166
1200
  inheritSkills: step.inheritSkills,
1167
1201
  task: step.launchBindingTask ?? task,
1168
- modelCandidates: candidates as string[],
1202
+ model: candidate,
1169
1203
  fast: step.fast,
1170
1204
  thinking: resolveEffectiveThinking(candidate, step.thinking),
1171
1205
  systemPrompt: step.systemPrompt ?? "",
@@ -1181,13 +1215,18 @@ export async function runSingleStepInner(
1181
1215
  // Each attempt rewrites the step output log; synchronous appends keep a
1182
1216
  // retried attempt from interleaving with the previous attempt's flush.
1183
1217
  fs.writeFileSync(ctx.outputFile, "", "utf-8");
1218
+ if (step.effectiveAcceptance?.preserveStagedIndex && stagedIndexBaseline === undefined) {
1219
+ try {
1220
+ stagedIndexBaseline = captureStagedIndexBaseline(step.cwd ?? ctx.cwd);
1221
+ } catch (error) {
1222
+ const message = error instanceof Error ? error.message : String(error);
1223
+ return { agent: step.agent, output: message, error: message, exitCode: 1, context: step.context };
1224
+ }
1225
+ }
1184
1226
  const run = await runChildSession(omitUndefinedProperties({
1185
1227
  factory: ctx.childSessions,
1186
1228
  launch,
1187
- collectReadonlyEvidence: true,
1188
- readonlyContinuation,
1189
- canContinue,
1190
- prompt: `Task: ${attemptTask}`,
1229
+ prompt: `Task: ${recoveryTask}`,
1191
1230
  childWatchdog,
1192
1231
  childEventContext: { runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
1193
1232
  appendChildEvent: (event) => appendDiagnosticJsonl(eventsPath, JSON.stringify(event), typeof event.type === "string" ? event.type : undefined),
@@ -1216,6 +1255,13 @@ export async function runSingleStepInner(
1216
1255
  modelResponseAliases: step.modelResponseAliases,
1217
1256
  mutationTools: step.mutationTools,
1218
1257
  }));
1258
+ launched = true;
1259
+ aggregateUsage.input += run.usage.input;
1260
+ aggregateUsage.output += run.usage.output;
1261
+ aggregateUsage.cacheRead += run.usage.cacheRead;
1262
+ aggregateUsage.cacheWrite += run.usage.cacheWrite;
1263
+ aggregateUsage.cost += run.usage.cost;
1264
+ aggregateUsage.turns += run.usage.turns;
1219
1265
  // A parked run still owes completion evidence when it actually finishes.
1220
1266
  // Stopped/timedOut runs already have terminal failures; checking completion evidence there is meaningless.
1221
1267
  const completionDiagnosticsEligible = !run.interrupted && !run.stopped && !run.timedOut;
@@ -1274,7 +1320,8 @@ export async function runSingleStepInner(
1274
1320
  const completionGuardEnabled = isAgentContract(step.agentContract) ? step.completionGuard === true : step.completionGuard !== false;
1275
1321
  const completionToolPlan = resolvedTaskToolPlan;
1276
1322
  const completionTools = completionToolPlan ? (completionToolPlan.explicitToolAllowlist ? completionToolPlan.effectiveToolAllowlist : undefined) : step.tools;
1277
- const mutationEvidence = collectTrackedMutationEvidence(mutationSnapshot, step.cwd ?? ctx.cwd);
1323
+ 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;
1324
+ 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);
1278
1325
  finalMutationEvidence = mutationEvidence;
1279
1326
  const completionMutationEvidence = ctx.trackedMutationEvidenceForCompletionGuard === false ? undefined : mutationEvidence;
1280
1327
  const completionGuard = completionDiagnosticsEligible && run.exitCode === 0 && !run.error && !structuredError && !hiddenError?.hasError && !midToolExitError && !emptyOutputError && completionGuardEnabled
@@ -1340,15 +1387,6 @@ export async function runSingleStepInner(
1340
1387
  : `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
1341
1388
  : undefined);
1342
1389
  const error = underlyingError ?? missingRequiredOutputError ?? completionEvidence.legacyFailureError;
1343
- const attempt: ModelAttempt = omitUndefinedProperties({
1344
- model: candidate ?? run.model ?? step.model ?? "default",
1345
- success: effectiveExitCode === 0 && !error,
1346
- exitCode: effectiveExitCode,
1347
- error,
1348
- usage: run.usage,
1349
- });
1350
- modelAttempts.push(attempt);
1351
- if (!recoveringAbort && candidate) attemptedModels.push(candidate);
1352
1390
  completionGuardTriggeredFinal = completionEvidence.guardTriggered && !underlyingError && !missingRequiredOutputError;
1353
1391
  finalOutputSnapshot = outputSnapshot;
1354
1392
  if (step.toolBudget) {
@@ -1366,11 +1404,14 @@ export async function runSingleStepInner(
1366
1404
  });
1367
1405
  const fileMutationEffect = completionEvidence.fileMutation ?? (missingRequiredOutputAfterMutation ? { status: "observed" as const, expected: completionEvidence.mutationExpected, attempted: true, evidence: mutationEvidence } : undefined);
1368
1406
  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;
1369
- const abortRecovery = !attempt.success ? planAbortRecovery({
1407
+ if (run.stopped || run.timedOut || ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break singleLaunch;
1408
+ if (effectiveExitCode === 0 && !error) break singleLaunch;
1409
+ if (completionEvidence.guardTriggered) break singleLaunch;
1410
+ const recovery = planAbortRecovery({
1370
1411
  messages: run.messages,
1371
1412
  error,
1372
1413
  sessionAvailable: Boolean(step.sessionFile && fs.existsSync(step.sessionFile)),
1373
- alreadyResumed: recoveryState !== "unused",
1414
+ alreadyResumed: attemptIndex > 0,
1374
1415
  stopped: run.stopped || ctx.stopSignal?.aborted || ctx.skipAcceptance?.(),
1375
1416
  interrupted: run.interrupted,
1376
1417
  timedOut: run.timedOut || ctx.timeoutSignal?.aborted,
@@ -1380,83 +1421,26 @@ export async function runSingleStepInner(
1380
1421
  acceptanceFailed: false,
1381
1422
  currentTool: run.currentTool,
1382
1423
  afterCompactionSettlement: run.afterCompactionSettlement,
1383
- }) : undefined;
1384
- if (abortRecovery?.action === "settle" && abortRecovery.diagnostic) {
1385
- attempt.error = attempt.error
1386
- ? `${abortRecovery.diagnostic}\n${attempt.error.slice(0, 8_000)}`
1387
- : abortRecovery.diagnostic;
1388
- if (finalResult) finalResult.abortRecoveryDiagnostic = abortRecovery.diagnostic;
1424
+ });
1425
+ if (recovery.action === "resume") {
1426
+ recoveryTask = recovery.prompt;
1427
+ continue singleLaunch;
1389
1428
  }
1390
- if (run.stopped || run.timedOut || ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break modelAttemptsLoop;
1391
- if (abortRecovery?.action === "settle" && abortRecovery.diagnostic) break modelAttemptsLoop;
1392
- if (attempt.success) break modelAttemptsLoop;
1393
- // The shared token is consumed before sibling creation; no third dispatch of any kind.
1394
- if (recoveryState === "readonly-continuation") break modelAttemptsLoop;
1395
- if (recoveringAbort) break modelAttemptsLoop;
1396
- if (abortRecovery?.action === "resume") {
1397
- recoveryState = "abort-recovery";
1398
- nextAttemptTask = abortRecovery.prompt;
1399
- attemptNotes.push("[abort-recovery] provider/transport abort after useful progress; resuming the retained child session once.");
1400
- continue;
1429
+ if (recovery.diagnostic) {
1430
+ finalResult.abortRecoveryDiagnostic = recovery.diagnostic;
1401
1431
  }
1402
- if (completionEvidence.guardTriggered) break modelAttemptsLoop;
1403
1432
 
1404
- const source = getSettledReadonlyChild(run);
1405
- if (source) {
1406
- const captured = launch.capture.completionIntentContext?.();
1407
- const sourceModel = captured?.model;
1408
- const retained = getReadonlySessionEvidence(source)!;
1409
- // Modality/capacity assessment, not another history/provenance validator.
1410
- // Images and unknown content have no certified token bound in this host slice.
1411
- const textOnly = (JSON.parse(retained.contextJson) as Message[]).every((message) => typeof message.content === "string"
1412
- || (Array.isArray(message.content) && message.content.every((block) => ["text", "thinking", "toolCall"].includes(block.type))));
1413
- const retainedBytes = Buffer.byteLength(retained.contextJson, "utf8");
1414
- const resolvedCandidates = candidates.map((reference, index) => {
1415
- // Only exact registry identities qualify; aliases/default guesses remain ineligible.
1416
- const base = reference ? splitKnownThinkingSuffix(reference).baseModel : "";
1417
- const slash = base.indexOf("/");
1418
- const model = slash > 0 ? captured?.modelRegistry.find(base.slice(0, slash), base.slice(slash + 1)) : undefined;
1419
- // Reserve the source window for runtime/system/tool overhead, plus a conservative
1420
- // byte bound for the complete retained text and new prompt; never assume a fit
1421
- // merely because a 429 request was sent. Equal/smaller windows remain denied.
1422
- const compatible = textOnly && model && sourceModel && model.input?.includes("text")
1423
- && Number.isFinite(sourceModel.contextWindow) && sourceModel.contextWindow > 0
1424
- && model.contextWindow >= sourceModel.contextWindow + retainedBytes + Buffer.byteLength(READONLY_CONTINUATION_PROMPT, "utf8")
1425
- && model.maxTokens > 0 && model.maxTokens <= sourceModel.maxTokens;
1426
- return { resolved: model ? { provider: model.provider, model: model.id, api: model.api } : undefined,
1427
- tried: index <= modelIndex, compatibility: compatible ? "compatible" as const : "unknown" as const };
1428
- });
1429
- const plan = planReadonlyModelContinuation({ source, recoveryState, candidates: resolvedCandidates, currentIndex: modelIndex,
1430
- lifecycleAllowsContinuation: lifecycleAllowsContinuation() && !run.interrupted && !run.stopped && !run.timedOut,
1431
- effectsAllowContinuation: !run.currentTool && !run.observedMutationAttempt && !mutationEvidence.attemptedMutation
1432
- && !structuredError && !effectiveStructuredOutput && !missingRequiredOutputError && !toolAvailabilityError
1433
- && !completionEvidence.guardTriggered && !midToolExitError && !hiddenError?.hasError,
1434
- budget: continuationBudget(), knownContextOverflow: isContextOverflow(error) });
1435
- if (plan.kind === "continue" && canContinue()) {
1436
- recoveryState = plan.recoveryState;
1437
- const selected = resolvedCandidates[plan.candidateIndex]!.resolved!;
1438
- readonlyContinuation = { source, expected: plan.expected, modelId: `${selected.provider}/${selected.model}` };
1439
- nextAttemptTask = plan.prompt;
1440
- modelIndex = plan.candidateIndex;
1441
- attemptNotes.push(`[readonly-continuation] ${attempt.model} returned HTTP 429 after read-only progress; continuing the retained session once with ${candidates[modelIndex]}.`);
1442
- continue;
1443
- }
1444
- }
1445
-
1446
- const retryableModelFailure = isRetryableModelFailureAttempt({ error, messages: run.messages, toolCount: run.toolCount });
1447
- if (retryableModelFailure) recordRetryableModelFailure(candidate ?? run.model ?? step.model, error);
1448
1433
  if (isContextOverflow(error)) {
1449
1434
  contextOverflow = true;
1450
- 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.`);
1451
- break modelAttemptsLoop;
1435
+ break singleLaunch;
1452
1436
  }
1453
- if (!retryableModelFailure || modelIndex === candidates.length - 1) break modelAttemptsLoop;
1454
- attemptNotes.push(formatModelAttemptNote(attempt, candidates[modelIndex + 1]));
1455
- modelIndex += 1;
1437
+ break singleLaunch;
1456
1438
  }
1457
1439
 
1458
1440
  const rawOutput = finalResult?.finalOutput ?? "";
1459
- const outputForPersistence = stripAcceptanceReport(rawOutput);
1441
+ let outputForPersistence = stripAcceptanceReport(rawOutput);
1442
+ if (!outputForPersistence.trim() && finalResult?.structuredOutput !== undefined)
1443
+ outputForPersistence = JSON.stringify(finalResult.structuredOutput, null, 2);
1460
1444
  const resolvedOutput = step.outputPath && finalResult?.exitCode === 0
1461
1445
  ? resolveSingleOutput(step.outputPath, outputForPersistence, finalOutputSnapshot, step.outputClaimPath)
1462
1446
  : { fullOutput: outputForPersistence };
@@ -1469,9 +1453,6 @@ export async function runSingleStepInner(
1469
1453
  const output = stripAcceptanceReport(resolvedOutput.fullOutput);
1470
1454
  const outputReference = resolvedOutput.savedPath ? formatSavedOutputReference(resolvedOutput.savedPath, output) : undefined;
1471
1455
  let outputForSummary = output;
1472
- if (attemptNotes.length > 0) {
1473
- outputForSummary = `${attemptNotes.join("\n")}\n\n${outputForSummary}`.trim();
1474
- }
1475
1456
  if (finalResult?.stopped && !outputForSummary.trim()) {
1476
1457
  outputForSummary = ctx.stopMessage ?? "Subagent stopped by user.";
1477
1458
  }
@@ -1522,6 +1503,7 @@ export async function runSingleStepInner(
1522
1503
  ? { content: childWrittenOutput, path: step.outputPath, authoritative: step.outputMode === "file-only", durable: resolvedOutput.savedPath !== undefined }
1523
1504
  : undefined,
1524
1505
  cwd: step.cwd ?? ctx.cwd,
1506
+ stagedIndexBaseline,
1525
1507
  signal: combinedAbortSignal([ctx.timeoutSignal, ctx.stopSignal]),
1526
1508
  abortMessage: ctx.stopSignal?.aborted ? ctx.stopMessage ?? "Subagent stopped by user." : ctx.timeoutMessage ?? "Subagent timed out.",
1527
1509
  reportOptional: isAgentContract(step.agentContract),
@@ -1542,6 +1524,12 @@ export async function runSingleStepInner(
1542
1524
  const acceptanceFailure = effectiveAcceptance ? acceptanceFailureMessage(effectiveAcceptance) : undefined;
1543
1525
  const acceptanceCanFailRun = acceptanceFailure && effectiveAcceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted && !timedOutAfterAcceptance && !stoppedAfterAcceptance && !isAgentContract(step.agentContract);
1544
1526
  const effectiveFinalExitCode = timedOutAfterAcceptance || stoppedAfterAcceptance ? 1 : acceptanceCanFailRun ? 1 : finalResult?.exitCode ?? 1;
1527
+ // A passing typed gate supplies the structured output for runs that have no
1528
+ // outputSchema of their own; preflight rejects the combination.
1529
+ const typedGate = typedVerifyOutput(effectiveAcceptance);
1530
+ if (typedGate && finalResult && finalResult.structuredOutput === undefined && effectiveFinalExitCode === 0) {
1531
+ finalResult = { ...finalResult, structuredOutput: typedGate.value };
1532
+ }
1545
1533
  const intercomDetachReceipt = finalResult?.finalOutput === INTERCOM_DETACH_RECEIPT;
1546
1534
  const baseFinalError = stoppedAfterAcceptance
1547
1535
  ? ctx.stopMessage ?? "Subagent stopped by user."
@@ -1556,7 +1544,7 @@ export async function runSingleStepInner(
1556
1544
  abortRecoveryDiagnostic: effectiveFinalExitCode !== 0 ? finalResult?.abortRecoveryDiagnostic : undefined,
1557
1545
  requiredOutput: effectiveFinalExitCode !== 0 ? finalResult?.effects?.settlementDiagnostic?.requiredOutput : undefined,
1558
1546
  });
1559
- const usage = usageFromAttempts(modelAttempts);
1547
+ const usage = launched ? aggregateUsage : undefined;
1560
1548
 
1561
1549
  const artifactErrors = artifactPaths && ctx.artifactConfig?.enabled !== false
1562
1550
  ? persistStepArtifacts({
@@ -1574,8 +1562,8 @@ export async function runSingleStepInner(
1574
1562
  task: PROMPT_REDACTED,
1575
1563
  exitCode: effectiveFinalExitCode,
1576
1564
  model: finalResult?.model,
1577
- attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
1578
- modelAttempts,
1565
+ nativeMachine: finalResult?.nativeMachine,
1566
+ requestedModel: step.requestedModel,
1579
1567
  usage,
1580
1568
  error: effectiveFinalError,
1581
1569
  acceptance: effectiveAcceptance,
@@ -1604,15 +1592,16 @@ export async function runSingleStepInner(
1604
1592
  sessionFile: step.sessionFile,
1605
1593
  intercomTarget: ctx.childIntercomTarget,
1606
1594
  model: finalResult?.model,
1595
+ nativeMachine: finalResult?.nativeMachine,
1607
1596
  thinking: resolveEffectiveThinking(finalResult?.model, step.thinking),
1608
- attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
1609
- modelAttempts,
1597
+ requestedModel: step.requestedModel,
1610
1598
  contextOverflow: contextOverflow || undefined,
1611
- totalCost: costSummaryFromAttempts(modelAttempts),
1599
+ totalCost: costSummaryFromUsage(usage),
1612
1600
  usage,
1613
1601
  artifactPaths,
1614
1602
  savedOutputPath: finalizedOutput.savedPath,
1615
1603
  outputSaveError: [resolvedOutput.saveError, artifactErrors.outputSaveError].filter(Boolean).join("\n") || undefined,
1604
+ artifactOutputSaveFailed: artifactErrors.outputSaveError ? true : undefined,
1616
1605
  metadataSaveError: artifactErrors.metadataSaveError,
1617
1606
  transcriptPath: transcriptWriter ? artifactPaths?.transcriptPath : undefined,
1618
1607
  transcriptError: transcriptWriter?.getError(),
@@ -1856,8 +1845,12 @@ function missingRequiredOutputAfterUsefulMutation(result: SingleStepResult): boo
1856
1845
  }
1857
1846
 
1858
1847
  function partialExecutionWithUsefulMutation(result: SingleStepResult): boolean {
1848
+ if (result.execution?.status !== "partial") return false;
1849
+ // Pane-native external results are deliberately partial from bounded terminal
1850
+ // evidence alone; Pi mutation-based partials retain their existing behavior.
1851
+ if (result.runner?.type === "external-cli" && result.runner.machine) return true;
1859
1852
  const fileMutation = result.effects?.fileMutation;
1860
- return result.execution?.status === "partial" && (fileMutation?.attempted === true || Boolean(fileMutation?.evidence?.changedFiles.length));
1853
+ return fileMutation?.attempted === true || Boolean(fileMutation?.evidence?.changedFiles.length);
1861
1854
  }
1862
1855
 
1863
1856
  function partialEvidenceResult(result: SingleStepResult): boolean {
@@ -1960,6 +1953,7 @@ export async function runSubagent(
1960
1953
  let currentActivityState: ActivityState | undefined;
1961
1954
  let activityTimer: NodeJS.Timeout | undefined;
1962
1955
  let timeoutTimer: NodeJS.Timeout | undefined;
1956
+ let checkpointTimer: NodeJS.Timeout | undefined;
1963
1957
  let timedOut = false;
1964
1958
  let stopped = false;
1965
1959
  let usageBudgetExceeded = false;
@@ -1968,6 +1962,7 @@ export async function runSubagent(
1968
1962
  const timeoutAbortController = new AbortController();
1969
1963
  const stopAbortController = new AbortController();
1970
1964
  const setupInterruptController = new AbortController();
1965
+ const runStopSignal = AbortSignal.any([timeoutAbortController.signal, stopAbortController.signal]);
1971
1966
  const setupSignal = AbortSignal.any([timeoutAbortController.signal, stopAbortController.signal, setupInterruptController.signal]);
1972
1967
  let previousCumulativeTokens: TokenUsage = { input: 0, output: 0, total: 0 };
1973
1968
  let latestSessionFile: string | undefined;
@@ -2009,7 +2004,7 @@ export async function runSubagent(
2009
2004
  model: task.model,
2010
2005
  ...(task.contextLimit !== undefined ? { contextLimit: task.contextLimit } : {}),
2011
2006
  thinking: task.thinking,
2012
- attemptedModels: task.modelCandidates && task.modelCandidates.length > 0 ? task.modelCandidates : task.model ? [task.model] : undefined,
2007
+ requestedModel: task.requestedModel,
2013
2008
  recentTools: [],
2014
2009
  recentOutput: [],
2015
2010
  }));
@@ -2063,7 +2058,7 @@ export async function runSubagent(
2063
2058
  model: step.model,
2064
2059
  ...(step.contextLimit !== undefined ? { contextLimit: step.contextLimit } : {}),
2065
2060
  thinking: step.thinking,
2066
- attemptedModels: step.modelCandidates && step.modelCandidates.length > 0 ? step.modelCandidates : step.model ? [step.model] : undefined,
2061
+ requestedModel: step.requestedModel,
2067
2062
  recentTools: [],
2068
2063
  recentOutput: [],
2069
2064
  }));
@@ -2290,9 +2285,7 @@ export async function runSubagent(
2290
2285
  sessionFile: step.sessionFile,
2291
2286
  model: step.model,
2292
2287
  thinking: step.thinking,
2293
- attemptedModels: step.attemptedModels,
2294
- modelAttempts: step.modelAttempts,
2295
- usage: usageFromAttempts(step.modelAttempts),
2288
+ requestedModel: step.requestedModel,
2296
2289
  contextOverflow: step.contextOverflow,
2297
2290
  })),
2298
2291
  exitCode: state === "complete" || state === "paused" ? 0 : 1,
@@ -2457,6 +2450,8 @@ export async function runSubagent(
2457
2450
  yield* nestedRuns(child.steps?.flatMap((step) => step.children ?? []));
2458
2451
  }
2459
2452
  };
2453
+ const isNestedControlDescendant = (run: NestedRunSummary): boolean =>
2454
+ !config.nestedSelf || run.path.some((entry) => entry.runId === id);
2460
2455
  const interruptNestedAsyncDescendants = (): void => {
2461
2456
  if (!config.nestedRoute) return;
2462
2457
  let registry: ReturnType<typeof projectNestedEvents>;
@@ -2472,7 +2467,7 @@ export async function runSubagent(
2472
2467
  return;
2473
2468
  }
2474
2469
  for (const run of nestedRuns(registry.children)) {
2475
- if (run.state !== "running" && run.state !== "queued") continue;
2470
+ if (!isNestedControlDescendant(run) || (run.state !== "running" && run.state !== "queued")) continue;
2476
2471
  const nestedAsyncDir = run.asyncDir ?? resolveNestedAsyncDir(config.nestedRoute.rootRunId, run);
2477
2472
  if (!nestedAsyncDir) continue;
2478
2473
  try {
@@ -2503,7 +2498,7 @@ export async function runSubagent(
2503
2498
  return;
2504
2499
  }
2505
2500
  for (const run of nestedRuns(registry.children)) {
2506
- if (run.state !== "running" && run.state !== "queued") continue;
2501
+ if (!isNestedControlDescendant(run) || (run.state !== "running" && run.state !== "queued")) continue;
2507
2502
  const nestedAsyncDir = run.asyncDir ?? resolveNestedAsyncDir(config.nestedRoute.rootRunId, run);
2508
2503
  if (!nestedAsyncDir) continue;
2509
2504
  try {
@@ -2534,7 +2529,7 @@ export async function runSubagent(
2534
2529
  return;
2535
2530
  }
2536
2531
  for (const run of nestedRuns(registry.children)) {
2537
- if (run.state !== "running" && run.state !== "queued") continue;
2532
+ if (!isNestedControlDescendant(run) || (run.state !== "running" && run.state !== "queued")) continue;
2538
2533
  const nestedAsyncDir = run.asyncDir ?? resolveNestedAsyncDir(config.nestedRoute.rootRunId, run);
2539
2534
  if (!nestedAsyncDir) continue;
2540
2535
  try {
@@ -2590,6 +2585,7 @@ export async function runSubagent(
2590
2585
  }
2591
2586
  const appendedSteps = requests.flatMap((request) => request.steps);
2592
2587
  steps.push(...appendedSteps);
2588
+ flatSteps.push(...flattenSteps(appendedSteps));
2593
2589
  const now = Date.now();
2594
2590
  const pendingAppends = countPendingChainAppendRequests(asyncDir);
2595
2591
  const added = appendRunnerStepsToStatus({
@@ -2623,9 +2619,66 @@ export async function runSubagent(
2623
2619
  setOptionalProperty(groupNode, "acceptanceStatus", acceptance?.status ?? groupNode.acceptanceStatus);
2624
2620
  };
2625
2621
 
2622
+ const EXTERNAL_GIT_PROBE_MIN_INTERVAL_MS = 2_000;
2623
+ type ExternalActivityEvidence = {
2624
+ cwd?: string;
2625
+ lastStreamActivityAt?: number;
2626
+ fingerprint?: string;
2627
+ probeInFlight?: Promise<void>;
2628
+ lastProbeStartedAt?: number;
2629
+ };
2630
+ const externalActivityEvidence = new Map<number, ExternalActivityEvidence>();
2631
+ const gitProbesByCwd = new Map<string, Promise<string | undefined>>();
2632
+ let periodicGitProbeFailure: { index: number; error: Error; message: string } | undefined;
2633
+ const reportedGitProbeErrors = new Set<string>();
2634
+ const reportGitProbeError = (externalCwd: string, error: unknown): void => {
2635
+ if (reportedGitProbeErrors.has(externalCwd)) return;
2636
+ reportedGitProbeErrors.add(externalCwd);
2637
+ const detail = error instanceof Error ? error.message : String(error);
2638
+ console.error(`[pi-subagents] Git activity evidence unavailable for '${externalCwd.slice(-300)}'; check the cwd and Git installation: ${detail.slice(0, 500)}`);
2639
+ };
2640
+ const readSharedGitFingerprint = (externalCwd: string): Promise<string | undefined> => {
2641
+ const inFlight = gitProbesByCwd.get(externalCwd);
2642
+ if (inFlight) return inFlight;
2643
+ const probe = readGitFingerprint(externalCwd, runStopSignal, (error) => reportGitProbeError(externalCwd, error)).finally(() => gitProbesByCwd.delete(externalCwd));
2644
+ gitProbesByCwd.set(externalCwd, probe);
2645
+ return probe;
2646
+ };
2647
+ const recordPeriodicGitProbeFailure = (index: number, error: unknown): void => {
2648
+ if (periodicGitProbeFailure) return;
2649
+ const cause = error instanceof Error ? error : new Error(String(error));
2650
+ const message = `PROCESS_TREE_UNVERIFIED: ${cause.message}`;
2651
+ periodicGitProbeFailure = { index, error: cause, message };
2652
+ statusPayload.error = message;
2653
+ const step = statusPayload.steps[index];
2654
+ if (step) step.error = message;
2655
+ statusPayload.lastUpdate = Date.now();
2656
+ writeStatusPayload();
2657
+ appendJsonl(eventsPath, JSON.stringify({
2658
+ type: "subagent.run.process_tree_unverified",
2659
+ ts: statusPayload.lastUpdate,
2660
+ runId: id,
2661
+ index,
2662
+ code: "PROCESS_TREE_UNVERIFIED",
2663
+ message,
2664
+ }));
2665
+ };
2666
+ const prepareExternalActivity = async (index: number, externalCwd: string, signal: AbortSignal): Promise<void> => {
2667
+ if (!controlConfig.enabled) return;
2668
+ const resolvedCwd = path.resolve(externalCwd);
2669
+ const evidence: ExternalActivityEvidence = { cwd: resolvedCwd, lastProbeStartedAt: performance.now() };
2670
+ externalActivityEvidence.set(index, evidence);
2671
+ evidence.fingerprint = await readGitFingerprint(resolvedCwd, signal, (error) => reportGitProbeError(resolvedCwd, error));
2672
+ };
2673
+ const recordExternalStreamActivity = (index: number): void => {
2674
+ const evidence = externalActivityEvidence.get(index) ?? {};
2675
+ externalActivityEvidence.set(index, evidence);
2676
+ evidence.lastStreamActivityAt = Date.now();
2677
+ };
2626
2678
  const stepOutputActivityAt = (index: number): number => {
2627
2679
  const step = statusPayload.steps[index];
2628
2680
  let lastActivityAt = step?.lastActivityAt ?? step?.startedAt ?? overallStartTime;
2681
+ lastActivityAt = Math.max(lastActivityAt, externalActivityEvidence.get(index)?.lastStreamActivityAt ?? 0);
2629
2682
  const outputPath = path.join(asyncDir, `output-${index}.log`);
2630
2683
  try {
2631
2684
  lastActivityAt = Math.max(lastActivityAt, fs.statSync(outputPath).mtimeMs);
@@ -2993,7 +3046,7 @@ export async function runSubagent(
2993
3046
  return;
2994
3047
  }
2995
3048
  if (event.type === "message_end") {
2996
- const next = applyChildWatchdogMessage(step.watchdog, event.message, now);
3049
+ const next = applyChildWatchdogMessage(step.watchdog, event.message);
2997
3050
  if (next) step.watchdog = next;
2998
3051
  if (next && (event.message as { role?: unknown } | undefined)?.role === "custom") {
2999
3052
  statusPayload.lastUpdate = now;
@@ -3134,7 +3187,7 @@ export async function runSubagent(
3134
3187
  // A sibling may keep aggregate attention unchanged; publish this step's transition.
3135
3188
  writeStatusPayload(step.activityState !== previousActivityState);
3136
3189
  };
3137
- const updateRunnerActivityState = (now: number): boolean => {
3190
+ const updateRunnerActivityState = (now: number, skipExternalProbeIndex?: number): boolean => {
3138
3191
  if (!controlConfig.enabled) return false;
3139
3192
  let changed = false;
3140
3193
  let runLastActivityAt = statusPayload.lastActivityAt ?? overallStartTime;
@@ -3151,12 +3204,35 @@ export async function runSubagent(
3151
3204
  config: controlConfig,
3152
3205
  startedAt: step.startedAt ?? overallStartTime,
3153
3206
  lastActivityAt,
3154
- turnCount: step.turnCount,
3207
+ turnCount: flatSteps[index]?.runner?.type === "external-cli" ? 1 : step.turnCount,
3155
3208
  currentTool: step.currentTool,
3156
3209
  thinking: step.thinking,
3157
3210
  now,
3158
3211
  }));
3159
3212
  if (idleState === "needs_attention") {
3213
+ const evidence = externalActivityEvidence.get(index);
3214
+ if (index !== skipExternalProbeIndex && evidence?.cwd && evidence.fingerprint) {
3215
+ if (evidence.probeInFlight) continue;
3216
+ if (performance.now() - (evidence.lastProbeStartedAt ?? 0) >= EXTERNAL_GIT_PROBE_MIN_INTERVAL_MS) {
3217
+ evidence.lastProbeStartedAt = performance.now();
3218
+ const baseline = evidence.fingerprint;
3219
+ evidence.probeInFlight = readSharedGitFingerprint(evidence.cwd).then((fingerprint) => {
3220
+ if (requiredStatusStep(statusPayload, index).status !== "running") return;
3221
+ if (fingerprint && fingerprint !== baseline) {
3222
+ evidence.fingerprint = fingerprint;
3223
+ evidence.lastStreamActivityAt = Date.now();
3224
+ updateRunnerActivityState(Date.now());
3225
+ } else {
3226
+ updateRunnerActivityState(Date.now(), index);
3227
+ }
3228
+ }).catch((error: unknown) => {
3229
+ recordPeriodicGitProbeFailure(index, error);
3230
+ }).finally(() => {
3231
+ delete evidence.probeInFlight;
3232
+ });
3233
+ continue;
3234
+ }
3235
+ }
3160
3236
  const previous = step.activityState;
3161
3237
  step.activityState = "needs_attention";
3162
3238
  if (previous !== "needs_attention") {
@@ -3218,11 +3294,15 @@ export async function runSubagent(
3218
3294
  };
3219
3295
  const finalizeWorktree = async (setup: WorktreeSetup, stepIndex: number, flatStartIndex: number, action: () => void, deadlineAt?: number): Promise<void> => {
3220
3296
  let admitted = false;
3221
- try { await withWorktreeTransaction(() => {
3222
- if (deadlineAt !== undefined && Date.now() >= deadlineAt) throw new Error("Run deadline expired before worktree cleanup");
3223
- admitted = true;
3224
- action();
3225
- }); }
3297
+ try {
3298
+ await Promise.all([...externalActivityEvidence.values()].map((evidence) => evidence.probeInFlight).filter((probe): probe is Promise<void> => probe !== undefined));
3299
+ if (periodicGitProbeFailure) throw periodicGitProbeFailure.error;
3300
+ await withWorktreeTransaction(() => {
3301
+ if (deadlineAt !== undefined && Date.now() >= deadlineAt) throw new Error("Run deadline expired before worktree cleanup");
3302
+ admitted = true;
3303
+ action();
3304
+ });
3305
+ }
3226
3306
  catch (error) {
3227
3307
  if (admitted) throw error;
3228
3308
  const reason = `Worktree finalization retained; manual reconciliation required: ${error instanceof Error ? error.message : String(error)}`;
@@ -3277,16 +3357,17 @@ export async function runSubagent(
3277
3357
  interruptActiveChildren();
3278
3358
  };
3279
3359
  const stopRunner = () => {
3280
- if (stopped || timedOut || interrupted || statusPayload.state !== "running") return;
3360
+ if (stopped || timedOut || (statusPayload.state !== "running" && statusPayload.state !== "paused")) return;
3281
3361
  stopped = true;
3362
+ interrupted = false;
3282
3363
  const now = Date.now();
3283
3364
  statusPayload.stopped = true;
3284
3365
  statusPayload.error = stopMessage;
3285
3366
  currentActivityState = undefined;
3286
3367
  delete statusPayload.activityState;
3287
3368
  statusPayload.lastUpdate = now;
3288
- for (const step of statusPayload.steps) {
3289
- if (step.status !== "running" && step.status !== "pending") continue;
3369
+ for (const [index, step] of statusPayload.steps.entries()) {
3370
+ if (step.status !== "running" && step.status !== "pending" && step.status !== "paused") continue;
3290
3371
  step.status = "stopped";
3291
3372
  step.error = stopMessage;
3292
3373
  step.exitCode = 1;
@@ -3295,6 +3376,8 @@ export async function runSubagent(
3295
3376
  step.endedAt = now;
3296
3377
  step.durationMs = step.startedAt ? now - step.startedAt : 0;
3297
3378
  step.lastActivityAt = now;
3379
+ const result = results[index];
3380
+ if (result?.interrupted) results[index] = { ...result, output: stopMessage, error: stopMessage, exitCode: 1, success: false, interrupted: false, stopped: true };
3298
3381
  }
3299
3382
  writeStatusPayload();
3300
3383
  appendJsonl(eventsPath, JSON.stringify({
@@ -3366,6 +3449,30 @@ export async function runSubagent(
3366
3449
  const remainingMs = Math.max(0, config.deadlineAt - Date.now());
3367
3450
  timeoutTimer = setTimeout(timeoutRunner, remainingMs);
3368
3451
  timeoutTimer.unref?.();
3452
+ // Route the pre-deadline checkpoint like any external steer so its lifecycle records the receipt.
3453
+ const checkpointBeforeDeadlineMs = config.checkpointBeforeDeadlineMs;
3454
+ const checkpointDelayMs = checkpointBeforeDeadlineMs === undefined || !Number.isInteger(checkpointBeforeDeadlineMs) || checkpointBeforeDeadlineMs <= 0
3455
+ ? undefined
3456
+ : remainingMs - checkpointBeforeDeadlineMs;
3457
+ if (checkpointDelayMs !== undefined && checkpointDelayMs >= 1_000) {
3458
+ const deadlineAt = config.deadlineAt;
3459
+ checkpointTimer = setTimeout(() => {
3460
+ checkpointTimer = undefined;
3461
+ if (timedOut || stopped || interrupted) return;
3462
+ if (!statusPayload.steps.some((step) => step.status === "running")) return;
3463
+ const now = Date.now();
3464
+ const seconds = Math.round(Math.max(0, deadlineAt - now) / 1000);
3465
+ deliverSteerRequest({
3466
+ type: "steer",
3467
+ id: `deadline-checkpoint-${now}`,
3468
+ ts: now,
3469
+ mode: "steer",
3470
+ source: "deadline-checkpoint",
3471
+ 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.`,
3472
+ });
3473
+ }, checkpointDelayMs);
3474
+ checkpointTimer.unref?.();
3475
+ }
3369
3476
  }
3370
3477
  appendJsonl(
3371
3478
  eventsPath,
@@ -3411,14 +3518,7 @@ export async function runSubagent(
3411
3518
  const thinkingOverride = step.thinkingOverrides?.[itemIndex];
3412
3519
  const model = thinkingOverride ? applyThinkingSuffix(step.parallel.model, thinkingOverride, true) : step.parallel.model;
3413
3520
  const configThinking = thinkingOverride ? thinkingOverride : step.parallel.thinking;
3414
- const candidates = step.parallel.modelCandidates !== undefined
3415
- ? step.parallel.modelCandidates.length > 0
3416
- ? step.parallel.modelCandidates.map((candidate) => thinkingOverride ? applyThinkingSuffix(candidate, thinkingOverride, true) ?? candidate : candidate)
3417
- : [undefined]
3418
- : model ? [model] : [undefined];
3419
- for (const candidate of candidates) {
3420
- assertThinkingWithinCeiling({ model: candidate, configThinking, ceiling: step.parallel.thinkingCeiling, agent: step.parallel.agent, runId: id });
3421
- }
3521
+ assertThinkingWithinCeiling({ model, configThinking, ceiling: step.parallel.thinkingCeiling, agent: step.parallel.agent, runId: id });
3422
3522
  }
3423
3523
  if (materialized.collectedOnEmpty) await validateDynamicCollection(step.collect.outputSchema, materialized.collectedOnEmpty);
3424
3524
  if (!config.runFanoutBudget) throw new Error("Async runner is missing its run fan-out budget identity.");
@@ -3551,10 +3651,6 @@ export async function runSubagent(
3551
3651
  ...(thinkingOverride ? {
3552
3652
  ...(model ? { model } : {}),
3553
3653
  ...(thinking ? { thinking } : {}),
3554
- ...(step.parallel.modelCandidates ? { modelCandidates: step.parallel.modelCandidates.flatMap((candidate) => {
3555
- const resolved = applyThinkingSuffix(candidate, thinkingOverride, true);
3556
- return resolved ? [resolved] : [];
3557
- }) } : {}),
3558
3654
  } : {}),
3559
3655
  structuredOutputSchema: step.parallel.structuredOutputSchema ?? step.parallel.structuredOutput?.schema,
3560
3656
  });
@@ -3581,7 +3677,7 @@ export async function runSubagent(
3581
3677
  ...(task.contextLimit !== undefined ? { contextLimit: task.contextLimit } : {}),
3582
3678
  ...(task.thinking ? { thinking: task.thinking } : {}),
3583
3679
  ...(task.thinkingCeiling ? { thinkingCeiling: task.thinkingCeiling } : {}),
3584
- ...(task.modelCandidates && task.modelCandidates.length > 0 ? { attemptedModels: task.modelCandidates } : task.model ? { attemptedModels: [task.model] } : {}),
3680
+ ...(task.requestedModel ? { requestedModel: task.requestedModel } : {}),
3585
3681
  recentTools: [],
3586
3682
  recentOutput: [],
3587
3683
  });
@@ -3710,6 +3806,8 @@ export async function runSubagent(
3710
3806
  onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking, attempt.contextLimit),
3711
3807
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
3712
3808
  onExternalProcess: (process) => updateExternalProcess(fi, process),
3809
+ prepareExternalActivity: (externalCwd, signal) => prepareExternalActivity(fi, externalCwd, signal),
3810
+ onExternalStreamActivity: () => recordExternalStreamActivity(fi),
3713
3811
  onExternalJob: (externalJob) => updateExternalJob(fi, externalJob),
3714
3812
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(fi),
3715
3813
  usageBudgetExhausted: continuationUsageBudgetExhausted,
@@ -3719,7 +3817,7 @@ export async function runSubagent(
3719
3817
  const taskEndTime = Date.now();
3720
3818
  const childInterrupted = singleResult.interrupted === true;
3721
3819
  const childStopped = singleResult.stopped === true;
3722
- requiredStatusStep(statusPayload, fi).status = stopped || childStopped ? "stopped" : timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
3820
+ requiredStatusStep(statusPayload, fi).status = stopped || childStopped ? "stopped" : timedOut ? "failed" : childInterrupted ? "paused" : singleResult.execution?.status === "partial" ? "partial" : singleResult.exitCode === 0 ? "complete" : "failed";
3723
3821
  requiredStatusStep(statusPayload, fi).endedAt = taskEndTime;
3724
3822
  requiredStatusStep(statusPayload, fi).durationMs = taskEndTime - taskStartTime;
3725
3823
  requiredStatusStep(statusPayload, fi).exitCode = stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
@@ -3732,8 +3830,7 @@ export async function runSubagent(
3732
3830
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "sessionName", singleResult.sessionName);
3733
3831
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "model", singleResult.model);
3734
3832
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "thinking", resolveEffectiveThinking(singleResult.model, requiredStatusStep(statusPayload, fi).thinking));
3735
- setOptionalProperty(requiredStatusStep(statusPayload, fi), "attemptedModels", singleResult.attemptedModels);
3736
- setOptionalProperty(requiredStatusStep(statusPayload, fi), "modelAttempts", singleResult.modelAttempts);
3833
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "requestedModel", singleResult.requestedModel);
3737
3834
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "contextOverflow", singleResult.contextOverflow);
3738
3835
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "totalCost", singleResult.totalCost);
3739
3836
  if (singleResult.totalCost) {
@@ -3790,7 +3887,7 @@ export async function runSubagent(
3790
3887
  output: pr.output,
3791
3888
  outputState: pr.outputState,
3792
3889
  error: pr.error,
3793
- success: pr.stopped !== true && pr.interrupted !== true && pr.exitCode === 0,
3890
+ success: pr.stopped !== true && pr.interrupted !== true && pr.exitCode === 0 && pr.execution?.status !== "partial",
3794
3891
  exitCode: pr.interrupted === true ? 0 : pr.exitCode,
3795
3892
  skipped: pr.skipped,
3796
3893
  interrupted: pr.interrupted,
@@ -3802,12 +3899,13 @@ export async function runSubagent(
3802
3899
  intercomTarget: pr.intercomTarget,
3803
3900
  model: pr.model,
3804
3901
  thinking: pr.thinking,
3805
- attemptedModels: pr.attemptedModels,
3806
- modelAttempts: pr.modelAttempts,
3902
+ requestedModel: pr.requestedModel,
3807
3903
  contextOverflow: pr.contextOverflow,
3808
3904
  totalCost: pr.totalCost,
3809
3905
  usage: pr.usage,
3810
3906
  artifactPaths: pr.artifactPaths,
3907
+ outputSaveError: pr.outputSaveError,
3908
+ artifactOutputSaveFailed: pr.artifactOutputSaveFailed,
3811
3909
  transcriptPath: pr.transcriptPath,
3812
3910
  transcriptError: pr.transcriptError,
3813
3911
  effects: pr.effects,
@@ -4122,6 +4220,8 @@ export async function runSubagent(
4122
4220
  onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking, attempt.contextLimit),
4123
4221
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
4124
4222
  onExternalProcess: (process) => updateExternalProcess(fi, process),
4223
+ prepareExternalActivity: (externalCwd, signal) => prepareExternalActivity(fi, externalCwd, signal),
4224
+ onExternalStreamActivity: () => recordExternalStreamActivity(fi),
4125
4225
  onExternalJob: (externalJob) => updateExternalJob(fi, externalJob),
4126
4226
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(fi),
4127
4227
  usageBudgetExhausted: continuationUsageBudgetExhausted,
@@ -4137,7 +4237,7 @@ export async function runSubagent(
4137
4237
  const childInterrupted = singleResult.interrupted === true;
4138
4238
  const childStopped = singleResult.stopped === true;
4139
4239
 
4140
- requiredStatusStep(statusPayload, fi).status = stopped || childStopped ? "stopped" : timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
4240
+ requiredStatusStep(statusPayload, fi).status = stopped || childStopped ? "stopped" : timedOut ? "failed" : childInterrupted ? "paused" : singleResult.execution?.status === "partial" ? "partial" : singleResult.exitCode === 0 ? "complete" : "failed";
4141
4241
  requiredStatusStep(statusPayload, fi).endedAt = taskEndTime;
4142
4242
  requiredStatusStep(statusPayload, fi).durationMs = taskDuration;
4143
4243
  requiredStatusStep(statusPayload, fi).exitCode = stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
@@ -4150,8 +4250,7 @@ export async function runSubagent(
4150
4250
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "sessionName", singleResult.sessionName);
4151
4251
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "model", singleResult.model);
4152
4252
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "thinking", resolveEffectiveThinking(singleResult.model, requiredStatusStep(statusPayload, fi).thinking));
4153
- setOptionalProperty(requiredStatusStep(statusPayload, fi), "attemptedModels", singleResult.attemptedModels);
4154
- setOptionalProperty(requiredStatusStep(statusPayload, fi), "modelAttempts", singleResult.modelAttempts);
4253
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "requestedModel", singleResult.requestedModel);
4155
4254
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "contextOverflow", singleResult.contextOverflow);
4156
4255
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "totalCost", singleResult.totalCost);
4157
4256
  if (singleResult.totalCost) {
@@ -4218,7 +4317,7 @@ export async function runSubagent(
4218
4317
  const sessionTokens = config.sessionDir
4219
4318
  ? parseSessionTokens(path.join(config.sessionDir, `parallel-${t}`))
4220
4319
  : null;
4221
- const fallbackTokens = tokenUsageFromAttempts(parallelResults[t]?.modelAttempts);
4320
+ const fallbackTokens = tokenUsageFromUsage(parallelResults[t]?.usage);
4222
4321
  const observedTokens = requiredStatusStep(statusPayload, fi).tokens;
4223
4322
  const taskTokens = sessionTokens ?? (fallbackTokens
4224
4323
  ? { ...fallbackTokens, ...(observedTokens?.window !== undefined ? { window: observedTokens.window } : {}), ...(observedTokens?.windowPeak !== undefined ? { windowPeak: observedTokens.windowPeak } : {}) }
@@ -4249,7 +4348,7 @@ export async function runSubagent(
4249
4348
  output: pr.output,
4250
4349
  outputState: pr.outputState,
4251
4350
  error: pr.error,
4252
- success: pr.stopped !== true && pr.interrupted !== true && pr.exitCode === 0,
4351
+ success: pr.stopped !== true && pr.interrupted !== true && pr.exitCode === 0 && pr.execution?.status !== "partial",
4253
4352
  exitCode: pr.interrupted === true ? 0 : pr.exitCode,
4254
4353
  skipped: pr.skipped,
4255
4354
  interrupted: pr.interrupted,
@@ -4261,12 +4360,13 @@ export async function runSubagent(
4261
4360
  intercomTarget: pr.intercomTarget,
4262
4361
  model: pr.model,
4263
4362
  thinking: pr.thinking,
4264
- attemptedModels: pr.attemptedModels,
4265
- modelAttempts: pr.modelAttempts,
4363
+ requestedModel: pr.requestedModel,
4266
4364
  contextOverflow: pr.contextOverflow,
4267
4365
  totalCost: pr.totalCost,
4268
4366
  usage: pr.usage,
4269
4367
  artifactPaths: pr.artifactPaths,
4368
+ outputSaveError: pr.outputSaveError,
4369
+ artifactOutputSaveFailed: pr.artifactOutputSaveFailed,
4270
4370
  transcriptPath: pr.transcriptPath,
4271
4371
  transcriptError: pr.transcriptError,
4272
4372
  effects: pr.effects,
@@ -4299,7 +4399,7 @@ export async function runSubagent(
4299
4399
  exitCode: r.exitCode,
4300
4400
  error: r.error,
4301
4401
  model: r.model,
4302
- attemptedModels: r.attemptedModels,
4402
+ requestedModel: r.requestedModel,
4303
4403
  })),
4304
4404
  );
4305
4405
  if (worktreeSetup) {
@@ -4523,6 +4623,8 @@ export async function runSubagent(
4523
4623
  onAttemptStart: (attempt) => updateStepModel(flatIndex, attempt.model, attempt.thinking, attempt.contextLimit),
4524
4624
  onChildEvent: (event) => updateStepFromChildEvent(flatIndex, event),
4525
4625
  onExternalProcess: (process) => updateExternalProcess(flatIndex, process),
4626
+ prepareExternalActivity: (externalCwd, signal) => prepareExternalActivity(flatIndex, externalCwd, signal),
4627
+ onExternalStreamActivity: () => recordExternalStreamActivity(flatIndex),
4526
4628
  onExternalJob: (externalJob) => updateExternalJob(flatIndex, externalJob),
4527
4629
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(flatIndex),
4528
4630
  usageBudgetExhausted: continuationUsageBudgetExhausted,
@@ -4550,19 +4652,20 @@ export async function runSubagent(
4550
4652
  output: stopped || childStopped ? stopMessage : timedOut ? singleResult.output || (timeoutMessage ?? "Subagent timed out.") : singleResult.output,
4551
4653
  outputState: singleResult.outputState,
4552
4654
  error: stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error,
4553
- success: !stopped && !childStopped && !timedOut && singleResult.interrupted !== true && singleResult.exitCode === 0,
4655
+ success: !stopped && !childStopped && !timedOut && singleResult.interrupted !== true && singleResult.exitCode === 0 && singleResult.execution?.status !== "partial",
4554
4656
  exitCode: stopped || childStopped ? 1 : timedOut ? 1 : singleResult.interrupted === true ? 0 : singleResult.exitCode,
4555
4657
  sessionFile: singleResult.sessionFile,
4556
4658
  intercomTarget: singleResult.intercomTarget,
4557
4659
  model: singleResult.model,
4558
4660
  thinking: singleResult.thinking,
4559
- attemptedModels: singleResult.attemptedModels,
4560
- modelAttempts: singleResult.modelAttempts,
4661
+ requestedModel: singleResult.requestedModel,
4561
4662
  contextOverflow: singleResult.contextOverflow,
4562
4663
  totalCost: singleResult.totalCost,
4563
4664
  usage: singleResult.usage,
4564
4665
  artifactPaths: singleResult.artifactPaths,
4565
4666
  savedOutputPath: singleResult.savedOutputPath,
4667
+ outputSaveError: singleResult.outputSaveError,
4668
+ artifactOutputSaveFailed: singleResult.artifactOutputSaveFailed,
4566
4669
  transcriptPath: singleResult.transcriptPath,
4567
4670
  transcriptError: singleResult.transcriptError,
4568
4671
  effects: singleResult.effects,
@@ -4607,7 +4710,7 @@ export async function runSubagent(
4607
4710
  if (cumulativeTokens) {
4608
4711
  previousCumulativeTokens = cumulativeTokens;
4609
4712
  } else {
4610
- const fallbackTokens = tokenUsageFromAttempts(singleResult.modelAttempts);
4713
+ const fallbackTokens = tokenUsageFromUsage(singleResult.usage);
4611
4714
  const observedTokens = requiredStatusStep(statusPayload, flatIndex).tokens;
4612
4715
  stepTokens = fallbackTokens
4613
4716
  ? { ...fallbackTokens, ...(observedTokens?.window !== undefined ? { window: observedTokens.window } : {}), ...(observedTokens?.windowPeak !== undefined ? { windowPeak: observedTokens.windowPeak } : {}) }
@@ -4627,7 +4730,7 @@ export async function runSubagent(
4627
4730
 
4628
4731
  const stepEndTime = Date.now();
4629
4732
  const childInterrupted = singleResult.interrupted === true;
4630
- requiredStatusStep(statusPayload, flatIndex).status = stopped || childStopped ? "stopped" : timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
4733
+ requiredStatusStep(statusPayload, flatIndex).status = stopped || childStopped ? "stopped" : timedOut ? "failed" : childInterrupted ? "paused" : singleResult.execution?.status === "partial" ? "partial" : singleResult.exitCode === 0 ? "complete" : "failed";
4631
4734
  requiredStatusStep(statusPayload, flatIndex).endedAt = stepEndTime;
4632
4735
  requiredStatusStep(statusPayload, flatIndex).durationMs = stepEndTime - stepStartTime;
4633
4736
  requiredStatusStep(statusPayload, flatIndex).exitCode = stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
@@ -4640,8 +4743,7 @@ export async function runSubagent(
4640
4743
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "sessionName", singleResult.sessionName);
4641
4744
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "model", singleResult.model);
4642
4745
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "thinking", resolveEffectiveThinking(singleResult.model, requiredStatusStep(statusPayload, flatIndex).thinking));
4643
- setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "attemptedModels", singleResult.attemptedModels);
4644
- setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "modelAttempts", singleResult.modelAttempts);
4746
+ setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "requestedModel", singleResult.requestedModel);
4645
4747
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "contextOverflow", singleResult.contextOverflow);
4646
4748
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "totalCost", singleResult.totalCost);
4647
4749
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "error", stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error);
@@ -4756,6 +4858,34 @@ export async function runSubagent(
4756
4858
  }
4757
4859
  }
4758
4860
 
4861
+ await Promise.all([...externalActivityEvidence.values()].map((evidence) => evidence.probeInFlight).filter((probe): probe is Promise<void> => probe !== undefined));
4862
+ if (periodicGitProbeFailure) {
4863
+ stopped = false;
4864
+ timedOut = false;
4865
+ interrupted = false;
4866
+ delete statusPayload.stopped;
4867
+ delete statusPayload.timedOut;
4868
+ statusPayload.error = periodicGitProbeFailure.message;
4869
+ const step = statusPayload.steps[periodicGitProbeFailure.index];
4870
+ if (step) {
4871
+ step.status = "failed";
4872
+ step.error = periodicGitProbeFailure.message;
4873
+ step.exitCode = 1;
4874
+ delete step.stopped;
4875
+ delete step.timedOut;
4876
+ }
4877
+ const result = results[periodicGitProbeFailure.index];
4878
+ if (result) {
4879
+ result.success = false;
4880
+ result.exitCode = 1;
4881
+ result.error = periodicGitProbeFailure.message;
4882
+ result.output = periodicGitProbeFailure.message;
4883
+ result.stopped = false;
4884
+ result.timedOut = false;
4885
+ result.interrupted = false;
4886
+ }
4887
+ }
4888
+
4759
4889
  let summary = results.map((r) => `${r.agent}:\n${r.output || (r.exitCode !== 0 ? r.error : undefined) || "(no output)"}`).join("\n\n");
4760
4890
  let truncated = false;
4761
4891
 
@@ -4821,9 +4951,15 @@ export async function runSubagent(
4821
4951
  clearTimeout(timeoutTimer);
4822
4952
  timeoutTimer = undefined;
4823
4953
  }
4954
+ if (checkpointTimer) {
4955
+ clearTimeout(checkpointTimer);
4956
+ checkpointTimer = undefined;
4957
+ }
4824
4958
  if (!timedOut && !stopped && !interrupted && config.timeoutMs !== undefined && timeoutMessage !== undefined && results.some((result) => result.timedOut === true && result.error?.startsWith(timeoutMessage))) {
4825
4959
  timedOut = true;
4826
4960
  }
4961
+ disposeControlInbox();
4962
+ for (const request of consumeStopRequestPayloads(asyncDir)) stopChildStep(request);
4827
4963
  const signalTerminated = !stopped && !timedOut && !interrupted && results.some((result) => result.exitCode !== 0 && isUnexplainedProcessSignal(omitUndefinedProperties({
4828
4964
  processSignal: result.processSignal,
4829
4965
  interrupted: result.interrupted,
@@ -4838,7 +4974,6 @@ export async function runSubagent(
4838
4974
  });
4839
4975
  statusPayload.state = stopped || signalTerminated ? "stopped" : timedOut || usageBudgetExceeded ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : partialWithEvidence ? "partial" : "failed";
4840
4976
  closeSteerInbox(asyncDir, statusPayload.state, (filePath, payload) => runPersistence.write(filePath, payload));
4841
- disposeControlInbox();
4842
4977
  for (const request of consumeSteerRequests(asyncDir)) deliverSteerRequest(request);
4843
4978
  const effectiveSessionFile = sessionFile ?? latestSessionFile;
4844
4979
  const steeringLifecycle = steeringStatus(statusPayload);
@@ -4874,7 +5009,7 @@ export async function runSubagent(
4874
5009
  statusPayload.activityState = "needs_attention";
4875
5010
  statusPayload.error = partialResult?.error ?? statusPayload.error;
4876
5011
  for (const step of statusPayload.steps) {
4877
- if (step.status === "failed") step.activityState = "needs_attention";
5012
+ if (step.status === "failed" || step.status === "partial") step.activityState = "needs_attention";
4878
5013
  }
4879
5014
  }
4880
5015
  statusPayload.endedAt = runEndedAt;
@@ -4931,14 +5066,14 @@ export async function runSubagent(
4931
5066
  intercomTarget: r.intercomTarget,
4932
5067
  model: r.model,
4933
5068
  thinking: r.thinking,
4934
- attemptedModels: r.attemptedModels,
4935
- modelAttempts: r.modelAttempts,
5069
+ requestedModel: r.requestedModel,
4936
5070
  contextOverflow: r.contextOverflow,
4937
5071
  totalCost: r.totalCost,
4938
5072
  usage: r.usage,
4939
5073
  artifactPaths: r.artifactPaths,
4940
5074
  savedOutputPath: r.savedOutputPath,
4941
5075
  outputSaveError: r.outputSaveError,
5076
+ artifactOutputSaveFailed: r.artifactOutputSaveFailed,
4942
5077
  metadataSaveError: r.metadataSaveError,
4943
5078
  truncated: r.truncated,
4944
5079
  transcriptPath: r.transcriptPath,
@@ -5074,7 +5209,7 @@ export async function runSubagent(
5074
5209
  async function waitForStartupControl(
5075
5210
  controlPath: string,
5076
5211
  token: string,
5077
- action: "ack" | "proceed",
5212
+ action: "ack" | "confirm" | "proceed",
5078
5213
  timeoutMs = 30_000,
5079
5214
  ): Promise<void> {
5080
5215
  const deadline = Date.now() + timeoutMs;
@@ -5088,7 +5223,7 @@ async function waitForStartupControl(
5088
5223
  }
5089
5224
  if (payload.token !== token) throw new Error("Runner startup control token does not match.");
5090
5225
  if (payload.action === action) return;
5091
- if (payload.action !== "ack" && payload.action !== "proceed") throw new Error("Runner startup control action is invalid.");
5226
+ if (payload.action !== "ack" && payload.action !== "confirm" && payload.action !== "proceed") throw new Error("Runner startup control action is invalid.");
5092
5227
  }
5093
5228
  await new Promise((resolve) => setTimeout(resolve, 20));
5094
5229
  }
@@ -5125,6 +5260,8 @@ export async function runConfiguredSubagent(config: SubagentRunConfig, options?:
5125
5260
  writeAtomicJson(startupPath, { state: "ready", token: lease.owner.token, pid: process.pid, owner: lease.owner });
5126
5261
  await waitForStartupControl(startupAckPath, lease.owner.token, "ack");
5127
5262
  writeAtomicJson(startupPath, { state: "acknowledged", token: lease.owner.token, pid: process.pid });
5263
+ await waitForStartupControl(startupAckPath, lease.owner.token, "confirm");
5264
+ writeAtomicJson(startupPath, { state: "confirmed", token: lease.owner.token, pid: process.pid });
5128
5265
  await waitForStartupControl(startupProceedPath, lease.owner.token, "proceed");
5129
5266
  startupCommitted = true;
5130
5267
  for (const controlPath of [startupAckPath, startupProceedPath]) {