pi-subagents 0.67.0 → 0.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/README.md +1 -1
  3. package/docs/agents.md +37 -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 +15 -12
  11. package/docs/watchdog.md +10 -12
  12. package/docs/workflows.md +11 -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 +4 -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 +184 -71
  27. package/src/agents/chain-serializer.ts +5 -0
  28. package/src/agents/runtime-agent-registry.ts +7 -6
  29. package/src/api/preflight.ts +20 -16
  30. package/src/api/required-child-extensions.ts +6 -0
  31. package/src/extension/config.ts +10 -37
  32. package/src/extension/fanout-child.ts +3 -0
  33. package/src/extension/herdr-pi-bridge.ts +160 -0
  34. package/src/extension/index.ts +42 -31
  35. package/src/extension/public-execution.ts +3 -3
  36. package/src/extension/schemas.ts +16 -5
  37. package/src/extension/tool-description.ts +8 -7
  38. package/src/intercom/native-supervisor-channel.ts +22 -18
  39. package/src/policy/authority.ts +4 -0
  40. package/src/profiles/profiles.ts +12 -6
  41. package/src/runs/background/active-run-index.ts +17 -1
  42. package/src/runs/background/async-execution.ts +309 -126
  43. package/src/runs/background/async-job-tracker.ts +8 -6
  44. package/src/runs/background/async-resume.ts +13 -4
  45. package/src/runs/background/async-status.ts +15 -4
  46. package/src/runs/background/auto-drain.ts +20 -10
  47. package/src/runs/background/binary-bootstrap.ts +5 -0
  48. package/src/runs/background/chain-append.ts +1 -1
  49. package/src/runs/background/chain-root-attachment.ts +14 -33
  50. package/src/runs/background/notify.ts +74 -6
  51. package/src/runs/background/result-files.ts +8 -4
  52. package/src/runs/background/result-watcher.ts +19 -2
  53. package/src/runs/background/run-child-session.ts +20 -29
  54. package/src/runs/background/runner-aliases.ts +4 -33
  55. package/src/runs/background/runner-child-launch.ts +4 -1
  56. package/src/runs/background/runner-child-sessions.ts +2 -2
  57. package/src/runs/background/runner-http-dispatcher.ts +119 -0
  58. package/src/runs/background/scheduled-runs.ts +11 -5
  59. package/src/runs/background/stale-run-reconciler.ts +35 -11
  60. package/src/runs/background/subagent-runner.ts +396 -275
  61. package/src/runs/background/subagent-wait.ts +128 -23
  62. package/src/runs/background/wait-completions.ts +75 -27
  63. package/src/runs/background/wait-subscriptions.ts +9 -3
  64. package/src/runs/background/wait-tool.ts +4 -2
  65. package/src/runs/foreground/async-stop-action.ts +93 -3
  66. package/src/runs/foreground/execution.ts +91 -218
  67. package/src/runs/foreground/foreground-history.ts +2 -1
  68. package/src/runs/foreground/subagent-executor.ts +266 -80
  69. package/src/runs/shared/acceptance.ts +34 -10
  70. package/src/runs/shared/async-status-projection.ts +123 -33
  71. package/src/runs/shared/child-launch-plan.ts +15 -3
  72. package/src/runs/shared/child-launch.ts +19 -6
  73. package/src/runs/shared/child-runtime-config.ts +5 -0
  74. package/src/runs/shared/child-session.ts +94 -50
  75. package/src/runs/shared/child-tool-plan.ts +28 -16
  76. package/src/runs/shared/dynamic-fanout.ts +2 -2
  77. package/src/runs/shared/external-cli-contract.ts +11 -1
  78. package/src/runs/shared/external-cli-preflight.ts +6 -2
  79. package/src/runs/shared/herdr-connection.ts +134 -0
  80. package/src/runs/shared/herdr-external-adapters.ts +169 -0
  81. package/src/runs/shared/herdr-machine.ts +279 -0
  82. package/src/runs/shared/herdr-pi-protocol.ts +59 -0
  83. package/src/runs/shared/herdr-placed-run.ts +263 -0
  84. package/src/runs/shared/model-resolution-diagnostic.ts +76 -0
  85. package/src/runs/shared/{model-fallback.ts → model-resolution.ts} +22 -237
  86. package/src/runs/shared/model-scope.ts +1 -1
  87. package/src/runs/shared/nested-events.ts +11 -2
  88. package/src/runs/shared/parallel-utils.ts +7 -2
  89. package/src/runs/shared/pi-spawn.ts +1 -1
  90. package/src/runs/shared/subagent-prompt-runtime.ts +4 -2
  91. package/src/runs/shared/worktree-setup-command.ts +27 -4
  92. package/src/runs/shared/worktree.ts +3 -3
  93. package/src/shared/child-cache-retention.ts +43 -0
  94. package/src/shared/launch-contract.ts +6 -9
  95. package/src/shared/pruned-fork.ts +1 -1
  96. package/src/shared/required-child-extensions.ts +81 -0
  97. package/src/shared/settings.ts +5 -2
  98. package/src/shared/shortcuts.ts +0 -4
  99. package/src/shared/types.ts +70 -29
  100. package/src/slash/slash-commands.ts +0 -6
  101. package/src/slash/subagents-admin.ts +13 -9
  102. package/src/tui/render.ts +20 -10
  103. package/src/watchdog/child-status.ts +28 -36
  104. package/src/watchdog/lsp-diagnostics.ts +1 -1
  105. package/src/watchdog/model-selection.ts +1 -1
  106. package/src/watchdog/register-child.ts +10 -3
  107. package/src/watchdog/register-main.ts +20 -20
  108. package/src/watchdog/render.ts +1 -1
  109. package/src/watchdog/review.ts +14 -30
  110. package/src/watchdog/rules.ts +1 -1
  111. package/src/watchdog/runtime.ts +23 -12
  112. package/src/watchdog/settings.ts +3 -6
  113. package/src/watchdog/types.ts +3 -5
  114. package/src/watchdog/warning-format.ts +1 -1
  115. package/src/workflows/scripted-workflow.ts +42 -3
  116. package/src/workflows/workflow-receipt.ts +21 -3
  117. package/src/workflows/workflow-resources.ts +13 -2
  118. package/src/runs/shared/model-exclusions.ts +0 -374
  119. package/src/runs/shared/readonly-model-continuation.ts +0 -69
  120. 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";
@@ -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,21 @@ 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
+ singleLaunch: for (let attemptIndex = 0; attemptIndex < 2; attemptIndex++) {
1122
+ if (ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break singleLaunch;
1123
+ const expectedModelForVerification = candidate && !step.skipPrimaryModelVerification ? candidate : undefined;
1083
1124
  try {
1084
1125
  assertThinkingWithinCeiling({ model: candidate, configThinking: step.thinking, ceiling: step.thinkingCeiling, agent: step.agent, runId: ctx.id });
1085
1126
  } catch (error) {
1086
1127
  const message = error instanceof Error ? error.message : String(error);
1087
- if (readonlyContinuation) {
1088
- failContinuationLaunch(candidate, error);
1089
- break modelAttemptsLoop;
1090
- }
1091
1128
  return omitUndefinedProperties({ agent: step.agent, output: message, error: message, exitCode: 1, context: step.context, thinkingCeiling: step.thinkingCeiling });
1092
1129
  }
1093
1130
  ctx.onAttemptStart?.(omitUndefinedProperties({
@@ -1124,11 +1161,7 @@ export async function runSingleStepInner(
1124
1161
  childWatchdog,
1125
1162
  watchdogStatus: (event) => watchdogSink?.(event),
1126
1163
  });
1127
- } catch (error) {
1128
- if (!readonlyContinuation) throw error;
1129
- failContinuationLaunch(candidate, error);
1130
- break modelAttemptsLoop;
1131
- }
1164
+ } catch (error) { throw error; }
1132
1165
  if (effectiveStructuredOutput && launch.config.structuredOutput) {
1133
1166
  // The runner reads the value back from the runtime's files after the run.
1134
1167
  launch.config.structuredOutput.capture = createStructuredOutputFileCapture(effectiveStructuredOutput);
@@ -1147,13 +1180,13 @@ export async function runSingleStepInner(
1147
1180
  subagentOnlyExtensions: step.subagentOnlyExtensions,
1148
1181
  fast: step.fast,
1149
1182
  model: step.model,
1150
- modelCandidates: step.modelCandidates,
1151
1183
  mcpDirectTools: step.mcpDirectTools,
1152
1184
  cwd: step.cwd ?? ctx.cwd,
1153
1185
  requireReadTool: Boolean(step.skills?.length),
1154
1186
  structuredOutput: Boolean(effectiveStructuredOutput),
1155
1187
  capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
1156
1188
  inheritedCapabilityCeiling: ctx.inheritedChildRuntime?.capabilityCeiling,
1189
+ requiredExtensions: step.requiredExtensions ?? ctx.inheritedChildRuntime?.requiredExtensions,
1157
1190
  permissionRules: step.permissionRules,
1158
1191
  hostAvailableBuiltins: ctx.hostAvailableBuiltins,
1159
1192
  }));
@@ -1165,7 +1198,7 @@ export async function runSingleStepInner(
1165
1198
  inheritGlobalContext: step.inheritGlobalContext,
1166
1199
  inheritSkills: step.inheritSkills,
1167
1200
  task: step.launchBindingTask ?? task,
1168
- modelCandidates: candidates as string[],
1201
+ model: candidate,
1169
1202
  fast: step.fast,
1170
1203
  thinking: resolveEffectiveThinking(candidate, step.thinking),
1171
1204
  systemPrompt: step.systemPrompt ?? "",
@@ -1184,10 +1217,7 @@ export async function runSingleStepInner(
1184
1217
  const run = await runChildSession(omitUndefinedProperties({
1185
1218
  factory: ctx.childSessions,
1186
1219
  launch,
1187
- collectReadonlyEvidence: true,
1188
- readonlyContinuation,
1189
- canContinue,
1190
- prompt: `Task: ${attemptTask}`,
1220
+ prompt: `Task: ${recoveryTask}`,
1191
1221
  childWatchdog,
1192
1222
  childEventContext: { runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
1193
1223
  appendChildEvent: (event) => appendDiagnosticJsonl(eventsPath, JSON.stringify(event), typeof event.type === "string" ? event.type : undefined),
@@ -1216,6 +1246,13 @@ export async function runSingleStepInner(
1216
1246
  modelResponseAliases: step.modelResponseAliases,
1217
1247
  mutationTools: step.mutationTools,
1218
1248
  }));
1249
+ launched = true;
1250
+ aggregateUsage.input += run.usage.input;
1251
+ aggregateUsage.output += run.usage.output;
1252
+ aggregateUsage.cacheRead += run.usage.cacheRead;
1253
+ aggregateUsage.cacheWrite += run.usage.cacheWrite;
1254
+ aggregateUsage.cost += run.usage.cost;
1255
+ aggregateUsage.turns += run.usage.turns;
1219
1256
  // A parked run still owes completion evidence when it actually finishes.
1220
1257
  // Stopped/timedOut runs already have terminal failures; checking completion evidence there is meaningless.
1221
1258
  const completionDiagnosticsEligible = !run.interrupted && !run.stopped && !run.timedOut;
@@ -1274,7 +1311,8 @@ export async function runSingleStepInner(
1274
1311
  const completionGuardEnabled = isAgentContract(step.agentContract) ? step.completionGuard === true : step.completionGuard !== false;
1275
1312
  const completionToolPlan = resolvedTaskToolPlan;
1276
1313
  const completionTools = completionToolPlan ? (completionToolPlan.explicitToolAllowlist ? completionToolPlan.effectiveToolAllowlist : undefined) : step.tools;
1277
- const mutationEvidence = collectTrackedMutationEvidence(mutationSnapshot, step.cwd ?? ctx.cwd);
1314
+ const remoteGitChanged = run.nativeMachine?.initialGit && run.nativeMachine.finalGit ? run.nativeMachine.initialGit.head !== run.nativeMachine.finalGit.head || run.nativeMachine.initialGit.dirty !== run.nativeMachine.finalGit.dirty : undefined;
1315
+ const mutationEvidence = run.nativeMachine ? { source: "tracked-files" as const, trackedOnly: true as const, changedFiles: [], attemptedMutation: remoteGitChanged === true, ...(remoteGitChanged === undefined ? { unavailable: "Remote Git before/after evidence was incomplete." } : {}) } : collectTrackedMutationEvidence(mutationSnapshot, step.cwd ?? ctx.cwd);
1278
1316
  finalMutationEvidence = mutationEvidence;
1279
1317
  const completionMutationEvidence = ctx.trackedMutationEvidenceForCompletionGuard === false ? undefined : mutationEvidence;
1280
1318
  const completionGuard = completionDiagnosticsEligible && run.exitCode === 0 && !run.error && !structuredError && !hiddenError?.hasError && !midToolExitError && !emptyOutputError && completionGuardEnabled
@@ -1340,15 +1378,6 @@ export async function runSingleStepInner(
1340
1378
  : `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
1341
1379
  : undefined);
1342
1380
  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
1381
  completionGuardTriggeredFinal = completionEvidence.guardTriggered && !underlyingError && !missingRequiredOutputError;
1353
1382
  finalOutputSnapshot = outputSnapshot;
1354
1383
  if (step.toolBudget) {
@@ -1366,11 +1395,14 @@ export async function runSingleStepInner(
1366
1395
  });
1367
1396
  const fileMutationEffect = completionEvidence.fileMutation ?? (missingRequiredOutputAfterMutation ? { status: "observed" as const, expected: completionEvidence.mutationExpected, attempted: true, evidence: mutationEvidence } : undefined);
1368
1397
  finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error, structuredOutput, runtimeAcknowledgedExtensions, ...(step.agentContract ? { agentContract: step.agentContract } : {}), ...(fileMutationEffect || settlementDiagnostic ? { effects: { ...(fileMutationEffect ? { fileMutation: fileMutationEffect } : {}), ...(settlementDiagnostic ? { settlementDiagnostic } : {}) } } : {}) } as RunChildSessionResult;
1369
- const abortRecovery = !attempt.success ? planAbortRecovery({
1398
+ if (run.stopped || run.timedOut || ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break singleLaunch;
1399
+ if (effectiveExitCode === 0 && !error) break singleLaunch;
1400
+ if (completionEvidence.guardTriggered) break singleLaunch;
1401
+ const recovery = planAbortRecovery({
1370
1402
  messages: run.messages,
1371
1403
  error,
1372
1404
  sessionAvailable: Boolean(step.sessionFile && fs.existsSync(step.sessionFile)),
1373
- alreadyResumed: recoveryState !== "unused",
1405
+ alreadyResumed: attemptIndex > 0,
1374
1406
  stopped: run.stopped || ctx.stopSignal?.aborted || ctx.skipAcceptance?.(),
1375
1407
  interrupted: run.interrupted,
1376
1408
  timedOut: run.timedOut || ctx.timeoutSignal?.aborted,
@@ -1380,83 +1412,26 @@ export async function runSingleStepInner(
1380
1412
  acceptanceFailed: false,
1381
1413
  currentTool: run.currentTool,
1382
1414
  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;
1389
- }
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;
1415
+ });
1416
+ if (recovery.action === "resume") {
1417
+ recoveryTask = recovery.prompt;
1418
+ continue singleLaunch;
1401
1419
  }
1402
- if (completionEvidence.guardTriggered) break modelAttemptsLoop;
1403
-
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
- }
1420
+ if (recovery.diagnostic) {
1421
+ finalResult.abortRecoveryDiagnostic = recovery.diagnostic;
1444
1422
  }
1445
1423
 
1446
- const retryableModelFailure = isRetryableModelFailureAttempt({ error, messages: run.messages, toolCount: run.toolCount });
1447
- if (retryableModelFailure) recordRetryableModelFailure(candidate ?? run.model ?? step.model, error);
1448
1424
  if (isContextOverflow(error)) {
1449
1425
  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;
1426
+ break singleLaunch;
1452
1427
  }
1453
- if (!retryableModelFailure || modelIndex === candidates.length - 1) break modelAttemptsLoop;
1454
- attemptNotes.push(formatModelAttemptNote(attempt, candidates[modelIndex + 1]));
1455
- modelIndex += 1;
1428
+ break singleLaunch;
1456
1429
  }
1457
1430
 
1458
1431
  const rawOutput = finalResult?.finalOutput ?? "";
1459
- const outputForPersistence = stripAcceptanceReport(rawOutput);
1432
+ let outputForPersistence = stripAcceptanceReport(rawOutput);
1433
+ if (!outputForPersistence.trim() && finalResult?.structuredOutput !== undefined)
1434
+ outputForPersistence = JSON.stringify(finalResult.structuredOutput, null, 2);
1460
1435
  const resolvedOutput = step.outputPath && finalResult?.exitCode === 0
1461
1436
  ? resolveSingleOutput(step.outputPath, outputForPersistence, finalOutputSnapshot, step.outputClaimPath)
1462
1437
  : { fullOutput: outputForPersistence };
@@ -1469,9 +1444,6 @@ export async function runSingleStepInner(
1469
1444
  const output = stripAcceptanceReport(resolvedOutput.fullOutput);
1470
1445
  const outputReference = resolvedOutput.savedPath ? formatSavedOutputReference(resolvedOutput.savedPath, output) : undefined;
1471
1446
  let outputForSummary = output;
1472
- if (attemptNotes.length > 0) {
1473
- outputForSummary = `${attemptNotes.join("\n")}\n\n${outputForSummary}`.trim();
1474
- }
1475
1447
  if (finalResult?.stopped && !outputForSummary.trim()) {
1476
1448
  outputForSummary = ctx.stopMessage ?? "Subagent stopped by user.";
1477
1449
  }
@@ -1556,7 +1528,7 @@ export async function runSingleStepInner(
1556
1528
  abortRecoveryDiagnostic: effectiveFinalExitCode !== 0 ? finalResult?.abortRecoveryDiagnostic : undefined,
1557
1529
  requiredOutput: effectiveFinalExitCode !== 0 ? finalResult?.effects?.settlementDiagnostic?.requiredOutput : undefined,
1558
1530
  });
1559
- const usage = usageFromAttempts(modelAttempts);
1531
+ const usage = launched ? aggregateUsage : undefined;
1560
1532
 
1561
1533
  const artifactErrors = artifactPaths && ctx.artifactConfig?.enabled !== false
1562
1534
  ? persistStepArtifacts({
@@ -1574,8 +1546,8 @@ export async function runSingleStepInner(
1574
1546
  task: PROMPT_REDACTED,
1575
1547
  exitCode: effectiveFinalExitCode,
1576
1548
  model: finalResult?.model,
1577
- attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
1578
- modelAttempts,
1549
+ nativeMachine: finalResult?.nativeMachine,
1550
+ requestedModel: step.requestedModel,
1579
1551
  usage,
1580
1552
  error: effectiveFinalError,
1581
1553
  acceptance: effectiveAcceptance,
@@ -1604,15 +1576,16 @@ export async function runSingleStepInner(
1604
1576
  sessionFile: step.sessionFile,
1605
1577
  intercomTarget: ctx.childIntercomTarget,
1606
1578
  model: finalResult?.model,
1579
+ nativeMachine: finalResult?.nativeMachine,
1607
1580
  thinking: resolveEffectiveThinking(finalResult?.model, step.thinking),
1608
- attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
1609
- modelAttempts,
1581
+ requestedModel: step.requestedModel,
1610
1582
  contextOverflow: contextOverflow || undefined,
1611
- totalCost: costSummaryFromAttempts(modelAttempts),
1583
+ totalCost: costSummaryFromUsage(usage),
1612
1584
  usage,
1613
1585
  artifactPaths,
1614
1586
  savedOutputPath: finalizedOutput.savedPath,
1615
1587
  outputSaveError: [resolvedOutput.saveError, artifactErrors.outputSaveError].filter(Boolean).join("\n") || undefined,
1588
+ artifactOutputSaveFailed: artifactErrors.outputSaveError ? true : undefined,
1616
1589
  metadataSaveError: artifactErrors.metadataSaveError,
1617
1590
  transcriptPath: transcriptWriter ? artifactPaths?.transcriptPath : undefined,
1618
1591
  transcriptError: transcriptWriter?.getError(),
@@ -1856,8 +1829,12 @@ function missingRequiredOutputAfterUsefulMutation(result: SingleStepResult): boo
1856
1829
  }
1857
1830
 
1858
1831
  function partialExecutionWithUsefulMutation(result: SingleStepResult): boolean {
1832
+ if (result.execution?.status !== "partial") return false;
1833
+ // Pane-native external results are deliberately partial from bounded terminal
1834
+ // evidence alone; Pi mutation-based partials retain their existing behavior.
1835
+ if (result.runner?.type === "external-cli" && result.runner.machine) return true;
1859
1836
  const fileMutation = result.effects?.fileMutation;
1860
- return result.execution?.status === "partial" && (fileMutation?.attempted === true || Boolean(fileMutation?.evidence?.changedFiles.length));
1837
+ return fileMutation?.attempted === true || Boolean(fileMutation?.evidence?.changedFiles.length);
1861
1838
  }
1862
1839
 
1863
1840
  function partialEvidenceResult(result: SingleStepResult): boolean {
@@ -1960,6 +1937,7 @@ export async function runSubagent(
1960
1937
  let currentActivityState: ActivityState | undefined;
1961
1938
  let activityTimer: NodeJS.Timeout | undefined;
1962
1939
  let timeoutTimer: NodeJS.Timeout | undefined;
1940
+ let checkpointTimer: NodeJS.Timeout | undefined;
1963
1941
  let timedOut = false;
1964
1942
  let stopped = false;
1965
1943
  let usageBudgetExceeded = false;
@@ -1968,6 +1946,7 @@ export async function runSubagent(
1968
1946
  const timeoutAbortController = new AbortController();
1969
1947
  const stopAbortController = new AbortController();
1970
1948
  const setupInterruptController = new AbortController();
1949
+ const runStopSignal = AbortSignal.any([timeoutAbortController.signal, stopAbortController.signal]);
1971
1950
  const setupSignal = AbortSignal.any([timeoutAbortController.signal, stopAbortController.signal, setupInterruptController.signal]);
1972
1951
  let previousCumulativeTokens: TokenUsage = { input: 0, output: 0, total: 0 };
1973
1952
  let latestSessionFile: string | undefined;
@@ -2009,7 +1988,7 @@ export async function runSubagent(
2009
1988
  model: task.model,
2010
1989
  ...(task.contextLimit !== undefined ? { contextLimit: task.contextLimit } : {}),
2011
1990
  thinking: task.thinking,
2012
- attemptedModels: task.modelCandidates && task.modelCandidates.length > 0 ? task.modelCandidates : task.model ? [task.model] : undefined,
1991
+ requestedModel: task.requestedModel,
2013
1992
  recentTools: [],
2014
1993
  recentOutput: [],
2015
1994
  }));
@@ -2063,7 +2042,7 @@ export async function runSubagent(
2063
2042
  model: step.model,
2064
2043
  ...(step.contextLimit !== undefined ? { contextLimit: step.contextLimit } : {}),
2065
2044
  thinking: step.thinking,
2066
- attemptedModels: step.modelCandidates && step.modelCandidates.length > 0 ? step.modelCandidates : step.model ? [step.model] : undefined,
2045
+ requestedModel: step.requestedModel,
2067
2046
  recentTools: [],
2068
2047
  recentOutput: [],
2069
2048
  }));
@@ -2290,9 +2269,7 @@ export async function runSubagent(
2290
2269
  sessionFile: step.sessionFile,
2291
2270
  model: step.model,
2292
2271
  thinking: step.thinking,
2293
- attemptedModels: step.attemptedModels,
2294
- modelAttempts: step.modelAttempts,
2295
- usage: usageFromAttempts(step.modelAttempts),
2272
+ requestedModel: step.requestedModel,
2296
2273
  contextOverflow: step.contextOverflow,
2297
2274
  })),
2298
2275
  exitCode: state === "complete" || state === "paused" ? 0 : 1,
@@ -2457,6 +2434,8 @@ export async function runSubagent(
2457
2434
  yield* nestedRuns(child.steps?.flatMap((step) => step.children ?? []));
2458
2435
  }
2459
2436
  };
2437
+ const isNestedControlDescendant = (run: NestedRunSummary): boolean =>
2438
+ !config.nestedSelf || run.path.some((entry) => entry.runId === id);
2460
2439
  const interruptNestedAsyncDescendants = (): void => {
2461
2440
  if (!config.nestedRoute) return;
2462
2441
  let registry: ReturnType<typeof projectNestedEvents>;
@@ -2472,7 +2451,7 @@ export async function runSubagent(
2472
2451
  return;
2473
2452
  }
2474
2453
  for (const run of nestedRuns(registry.children)) {
2475
- if (run.state !== "running" && run.state !== "queued") continue;
2454
+ if (!isNestedControlDescendant(run) || (run.state !== "running" && run.state !== "queued")) continue;
2476
2455
  const nestedAsyncDir = run.asyncDir ?? resolveNestedAsyncDir(config.nestedRoute.rootRunId, run);
2477
2456
  if (!nestedAsyncDir) continue;
2478
2457
  try {
@@ -2503,7 +2482,7 @@ export async function runSubagent(
2503
2482
  return;
2504
2483
  }
2505
2484
  for (const run of nestedRuns(registry.children)) {
2506
- if (run.state !== "running" && run.state !== "queued") continue;
2485
+ if (!isNestedControlDescendant(run) || (run.state !== "running" && run.state !== "queued")) continue;
2507
2486
  const nestedAsyncDir = run.asyncDir ?? resolveNestedAsyncDir(config.nestedRoute.rootRunId, run);
2508
2487
  if (!nestedAsyncDir) continue;
2509
2488
  try {
@@ -2534,7 +2513,7 @@ export async function runSubagent(
2534
2513
  return;
2535
2514
  }
2536
2515
  for (const run of nestedRuns(registry.children)) {
2537
- if (run.state !== "running" && run.state !== "queued") continue;
2516
+ if (!isNestedControlDescendant(run) || (run.state !== "running" && run.state !== "queued")) continue;
2538
2517
  const nestedAsyncDir = run.asyncDir ?? resolveNestedAsyncDir(config.nestedRoute.rootRunId, run);
2539
2518
  if (!nestedAsyncDir) continue;
2540
2519
  try {
@@ -2590,6 +2569,7 @@ export async function runSubagent(
2590
2569
  }
2591
2570
  const appendedSteps = requests.flatMap((request) => request.steps);
2592
2571
  steps.push(...appendedSteps);
2572
+ flatSteps.push(...flattenSteps(appendedSteps));
2593
2573
  const now = Date.now();
2594
2574
  const pendingAppends = countPendingChainAppendRequests(asyncDir);
2595
2575
  const added = appendRunnerStepsToStatus({
@@ -2623,9 +2603,66 @@ export async function runSubagent(
2623
2603
  setOptionalProperty(groupNode, "acceptanceStatus", acceptance?.status ?? groupNode.acceptanceStatus);
2624
2604
  };
2625
2605
 
2606
+ const EXTERNAL_GIT_PROBE_MIN_INTERVAL_MS = 2_000;
2607
+ type ExternalActivityEvidence = {
2608
+ cwd?: string;
2609
+ lastStreamActivityAt?: number;
2610
+ fingerprint?: string;
2611
+ probeInFlight?: Promise<void>;
2612
+ lastProbeStartedAt?: number;
2613
+ };
2614
+ const externalActivityEvidence = new Map<number, ExternalActivityEvidence>();
2615
+ const gitProbesByCwd = new Map<string, Promise<string | undefined>>();
2616
+ let periodicGitProbeFailure: { index: number; error: Error; message: string } | undefined;
2617
+ const reportedGitProbeErrors = new Set<string>();
2618
+ const reportGitProbeError = (externalCwd: string, error: unknown): void => {
2619
+ if (reportedGitProbeErrors.has(externalCwd)) return;
2620
+ reportedGitProbeErrors.add(externalCwd);
2621
+ const detail = error instanceof Error ? error.message : String(error);
2622
+ console.error(`[pi-subagents] Git activity evidence unavailable for '${externalCwd.slice(-300)}'; check the cwd and Git installation: ${detail.slice(0, 500)}`);
2623
+ };
2624
+ const readSharedGitFingerprint = (externalCwd: string): Promise<string | undefined> => {
2625
+ const inFlight = gitProbesByCwd.get(externalCwd);
2626
+ if (inFlight) return inFlight;
2627
+ const probe = readGitFingerprint(externalCwd, runStopSignal, (error) => reportGitProbeError(externalCwd, error)).finally(() => gitProbesByCwd.delete(externalCwd));
2628
+ gitProbesByCwd.set(externalCwd, probe);
2629
+ return probe;
2630
+ };
2631
+ const recordPeriodicGitProbeFailure = (index: number, error: unknown): void => {
2632
+ if (periodicGitProbeFailure) return;
2633
+ const cause = error instanceof Error ? error : new Error(String(error));
2634
+ const message = `PROCESS_TREE_UNVERIFIED: ${cause.message}`;
2635
+ periodicGitProbeFailure = { index, error: cause, message };
2636
+ statusPayload.error = message;
2637
+ const step = statusPayload.steps[index];
2638
+ if (step) step.error = message;
2639
+ statusPayload.lastUpdate = Date.now();
2640
+ writeStatusPayload();
2641
+ appendJsonl(eventsPath, JSON.stringify({
2642
+ type: "subagent.run.process_tree_unverified",
2643
+ ts: statusPayload.lastUpdate,
2644
+ runId: id,
2645
+ index,
2646
+ code: "PROCESS_TREE_UNVERIFIED",
2647
+ message,
2648
+ }));
2649
+ };
2650
+ const prepareExternalActivity = async (index: number, externalCwd: string, signal: AbortSignal): Promise<void> => {
2651
+ if (!controlConfig.enabled) return;
2652
+ const resolvedCwd = path.resolve(externalCwd);
2653
+ const evidence: ExternalActivityEvidence = { cwd: resolvedCwd, lastProbeStartedAt: performance.now() };
2654
+ externalActivityEvidence.set(index, evidence);
2655
+ evidence.fingerprint = await readGitFingerprint(resolvedCwd, signal, (error) => reportGitProbeError(resolvedCwd, error));
2656
+ };
2657
+ const recordExternalStreamActivity = (index: number): void => {
2658
+ const evidence = externalActivityEvidence.get(index) ?? {};
2659
+ externalActivityEvidence.set(index, evidence);
2660
+ evidence.lastStreamActivityAt = Date.now();
2661
+ };
2626
2662
  const stepOutputActivityAt = (index: number): number => {
2627
2663
  const step = statusPayload.steps[index];
2628
2664
  let lastActivityAt = step?.lastActivityAt ?? step?.startedAt ?? overallStartTime;
2665
+ lastActivityAt = Math.max(lastActivityAt, externalActivityEvidence.get(index)?.lastStreamActivityAt ?? 0);
2629
2666
  const outputPath = path.join(asyncDir, `output-${index}.log`);
2630
2667
  try {
2631
2668
  lastActivityAt = Math.max(lastActivityAt, fs.statSync(outputPath).mtimeMs);
@@ -2993,7 +3030,7 @@ export async function runSubagent(
2993
3030
  return;
2994
3031
  }
2995
3032
  if (event.type === "message_end") {
2996
- const next = applyChildWatchdogMessage(step.watchdog, event.message, now);
3033
+ const next = applyChildWatchdogMessage(step.watchdog, event.message);
2997
3034
  if (next) step.watchdog = next;
2998
3035
  if (next && (event.message as { role?: unknown } | undefined)?.role === "custom") {
2999
3036
  statusPayload.lastUpdate = now;
@@ -3134,7 +3171,7 @@ export async function runSubagent(
3134
3171
  // A sibling may keep aggregate attention unchanged; publish this step's transition.
3135
3172
  writeStatusPayload(step.activityState !== previousActivityState);
3136
3173
  };
3137
- const updateRunnerActivityState = (now: number): boolean => {
3174
+ const updateRunnerActivityState = (now: number, skipExternalProbeIndex?: number): boolean => {
3138
3175
  if (!controlConfig.enabled) return false;
3139
3176
  let changed = false;
3140
3177
  let runLastActivityAt = statusPayload.lastActivityAt ?? overallStartTime;
@@ -3151,12 +3188,35 @@ export async function runSubagent(
3151
3188
  config: controlConfig,
3152
3189
  startedAt: step.startedAt ?? overallStartTime,
3153
3190
  lastActivityAt,
3154
- turnCount: step.turnCount,
3191
+ turnCount: flatSteps[index]?.runner?.type === "external-cli" ? 1 : step.turnCount,
3155
3192
  currentTool: step.currentTool,
3156
3193
  thinking: step.thinking,
3157
3194
  now,
3158
3195
  }));
3159
3196
  if (idleState === "needs_attention") {
3197
+ const evidence = externalActivityEvidence.get(index);
3198
+ if (index !== skipExternalProbeIndex && evidence?.cwd && evidence.fingerprint) {
3199
+ if (evidence.probeInFlight) continue;
3200
+ if (performance.now() - (evidence.lastProbeStartedAt ?? 0) >= EXTERNAL_GIT_PROBE_MIN_INTERVAL_MS) {
3201
+ evidence.lastProbeStartedAt = performance.now();
3202
+ const baseline = evidence.fingerprint;
3203
+ evidence.probeInFlight = readSharedGitFingerprint(evidence.cwd).then((fingerprint) => {
3204
+ if (requiredStatusStep(statusPayload, index).status !== "running") return;
3205
+ if (fingerprint && fingerprint !== baseline) {
3206
+ evidence.fingerprint = fingerprint;
3207
+ evidence.lastStreamActivityAt = Date.now();
3208
+ updateRunnerActivityState(Date.now());
3209
+ } else {
3210
+ updateRunnerActivityState(Date.now(), index);
3211
+ }
3212
+ }).catch((error: unknown) => {
3213
+ recordPeriodicGitProbeFailure(index, error);
3214
+ }).finally(() => {
3215
+ delete evidence.probeInFlight;
3216
+ });
3217
+ continue;
3218
+ }
3219
+ }
3160
3220
  const previous = step.activityState;
3161
3221
  step.activityState = "needs_attention";
3162
3222
  if (previous !== "needs_attention") {
@@ -3218,11 +3278,15 @@ export async function runSubagent(
3218
3278
  };
3219
3279
  const finalizeWorktree = async (setup: WorktreeSetup, stepIndex: number, flatStartIndex: number, action: () => void, deadlineAt?: number): Promise<void> => {
3220
3280
  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
- }); }
3281
+ try {
3282
+ await Promise.all([...externalActivityEvidence.values()].map((evidence) => evidence.probeInFlight).filter((probe): probe is Promise<void> => probe !== undefined));
3283
+ if (periodicGitProbeFailure) throw periodicGitProbeFailure.error;
3284
+ await withWorktreeTransaction(() => {
3285
+ if (deadlineAt !== undefined && Date.now() >= deadlineAt) throw new Error("Run deadline expired before worktree cleanup");
3286
+ admitted = true;
3287
+ action();
3288
+ });
3289
+ }
3226
3290
  catch (error) {
3227
3291
  if (admitted) throw error;
3228
3292
  const reason = `Worktree finalization retained; manual reconciliation required: ${error instanceof Error ? error.message : String(error)}`;
@@ -3277,16 +3341,17 @@ export async function runSubagent(
3277
3341
  interruptActiveChildren();
3278
3342
  };
3279
3343
  const stopRunner = () => {
3280
- if (stopped || timedOut || interrupted || statusPayload.state !== "running") return;
3344
+ if (stopped || timedOut || (statusPayload.state !== "running" && statusPayload.state !== "paused")) return;
3281
3345
  stopped = true;
3346
+ interrupted = false;
3282
3347
  const now = Date.now();
3283
3348
  statusPayload.stopped = true;
3284
3349
  statusPayload.error = stopMessage;
3285
3350
  currentActivityState = undefined;
3286
3351
  delete statusPayload.activityState;
3287
3352
  statusPayload.lastUpdate = now;
3288
- for (const step of statusPayload.steps) {
3289
- if (step.status !== "running" && step.status !== "pending") continue;
3353
+ for (const [index, step] of statusPayload.steps.entries()) {
3354
+ if (step.status !== "running" && step.status !== "pending" && step.status !== "paused") continue;
3290
3355
  step.status = "stopped";
3291
3356
  step.error = stopMessage;
3292
3357
  step.exitCode = 1;
@@ -3295,6 +3360,8 @@ export async function runSubagent(
3295
3360
  step.endedAt = now;
3296
3361
  step.durationMs = step.startedAt ? now - step.startedAt : 0;
3297
3362
  step.lastActivityAt = now;
3363
+ const result = results[index];
3364
+ if (result?.interrupted) results[index] = { ...result, output: stopMessage, error: stopMessage, exitCode: 1, success: false, interrupted: false, stopped: true };
3298
3365
  }
3299
3366
  writeStatusPayload();
3300
3367
  appendJsonl(eventsPath, JSON.stringify({
@@ -3366,6 +3433,30 @@ export async function runSubagent(
3366
3433
  const remainingMs = Math.max(0, config.deadlineAt - Date.now());
3367
3434
  timeoutTimer = setTimeout(timeoutRunner, remainingMs);
3368
3435
  timeoutTimer.unref?.();
3436
+ // Route the pre-deadline checkpoint like any external steer so its lifecycle records the receipt.
3437
+ const checkpointBeforeDeadlineMs = config.checkpointBeforeDeadlineMs;
3438
+ const checkpointDelayMs = checkpointBeforeDeadlineMs === undefined || !Number.isInteger(checkpointBeforeDeadlineMs) || checkpointBeforeDeadlineMs <= 0
3439
+ ? undefined
3440
+ : remainingMs - checkpointBeforeDeadlineMs;
3441
+ if (checkpointDelayMs !== undefined && checkpointDelayMs >= 1_000) {
3442
+ const deadlineAt = config.deadlineAt;
3443
+ checkpointTimer = setTimeout(() => {
3444
+ checkpointTimer = undefined;
3445
+ if (timedOut || stopped || interrupted) return;
3446
+ if (!statusPayload.steps.some((step) => step.status === "running")) return;
3447
+ const now = Date.now();
3448
+ const seconds = Math.round(Math.max(0, deadlineAt - now) / 1000);
3449
+ deliverSteerRequest({
3450
+ type: "steer",
3451
+ id: `deadline-checkpoint-${now}`,
3452
+ ts: now,
3453
+ mode: "steer",
3454
+ source: "deadline-checkpoint",
3455
+ message: `Deadline checkpoint from the runner: this run is killed in about ${seconds} seconds. Finish the current tool call only, then stop and reply with a handoff: changed files, build/test state, remaining work, and commit/PR state. Do not start new work.`,
3456
+ });
3457
+ }, checkpointDelayMs);
3458
+ checkpointTimer.unref?.();
3459
+ }
3369
3460
  }
3370
3461
  appendJsonl(
3371
3462
  eventsPath,
@@ -3411,14 +3502,7 @@ export async function runSubagent(
3411
3502
  const thinkingOverride = step.thinkingOverrides?.[itemIndex];
3412
3503
  const model = thinkingOverride ? applyThinkingSuffix(step.parallel.model, thinkingOverride, true) : step.parallel.model;
3413
3504
  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
- }
3505
+ assertThinkingWithinCeiling({ model, configThinking, ceiling: step.parallel.thinkingCeiling, agent: step.parallel.agent, runId: id });
3422
3506
  }
3423
3507
  if (materialized.collectedOnEmpty) await validateDynamicCollection(step.collect.outputSchema, materialized.collectedOnEmpty);
3424
3508
  if (!config.runFanoutBudget) throw new Error("Async runner is missing its run fan-out budget identity.");
@@ -3551,10 +3635,6 @@ export async function runSubagent(
3551
3635
  ...(thinkingOverride ? {
3552
3636
  ...(model ? { model } : {}),
3553
3637
  ...(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
3638
  } : {}),
3559
3639
  structuredOutputSchema: step.parallel.structuredOutputSchema ?? step.parallel.structuredOutput?.schema,
3560
3640
  });
@@ -3581,7 +3661,7 @@ export async function runSubagent(
3581
3661
  ...(task.contextLimit !== undefined ? { contextLimit: task.contextLimit } : {}),
3582
3662
  ...(task.thinking ? { thinking: task.thinking } : {}),
3583
3663
  ...(task.thinkingCeiling ? { thinkingCeiling: task.thinkingCeiling } : {}),
3584
- ...(task.modelCandidates && task.modelCandidates.length > 0 ? { attemptedModels: task.modelCandidates } : task.model ? { attemptedModels: [task.model] } : {}),
3664
+ ...(task.requestedModel ? { requestedModel: task.requestedModel } : {}),
3585
3665
  recentTools: [],
3586
3666
  recentOutput: [],
3587
3667
  });
@@ -3710,6 +3790,8 @@ export async function runSubagent(
3710
3790
  onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking, attempt.contextLimit),
3711
3791
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
3712
3792
  onExternalProcess: (process) => updateExternalProcess(fi, process),
3793
+ prepareExternalActivity: (externalCwd, signal) => prepareExternalActivity(fi, externalCwd, signal),
3794
+ onExternalStreamActivity: () => recordExternalStreamActivity(fi),
3713
3795
  onExternalJob: (externalJob) => updateExternalJob(fi, externalJob),
3714
3796
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(fi),
3715
3797
  usageBudgetExhausted: continuationUsageBudgetExhausted,
@@ -3719,7 +3801,7 @@ export async function runSubagent(
3719
3801
  const taskEndTime = Date.now();
3720
3802
  const childInterrupted = singleResult.interrupted === true;
3721
3803
  const childStopped = singleResult.stopped === true;
3722
- requiredStatusStep(statusPayload, fi).status = stopped || childStopped ? "stopped" : timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
3804
+ requiredStatusStep(statusPayload, fi).status = stopped || childStopped ? "stopped" : timedOut ? "failed" : childInterrupted ? "paused" : singleResult.execution?.status === "partial" ? "partial" : singleResult.exitCode === 0 ? "complete" : "failed";
3723
3805
  requiredStatusStep(statusPayload, fi).endedAt = taskEndTime;
3724
3806
  requiredStatusStep(statusPayload, fi).durationMs = taskEndTime - taskStartTime;
3725
3807
  requiredStatusStep(statusPayload, fi).exitCode = stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
@@ -3732,8 +3814,7 @@ export async function runSubagent(
3732
3814
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "sessionName", singleResult.sessionName);
3733
3815
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "model", singleResult.model);
3734
3816
  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);
3817
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "requestedModel", singleResult.requestedModel);
3737
3818
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "contextOverflow", singleResult.contextOverflow);
3738
3819
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "totalCost", singleResult.totalCost);
3739
3820
  if (singleResult.totalCost) {
@@ -3790,7 +3871,7 @@ export async function runSubagent(
3790
3871
  output: pr.output,
3791
3872
  outputState: pr.outputState,
3792
3873
  error: pr.error,
3793
- success: pr.stopped !== true && pr.interrupted !== true && pr.exitCode === 0,
3874
+ success: pr.stopped !== true && pr.interrupted !== true && pr.exitCode === 0 && pr.execution?.status !== "partial",
3794
3875
  exitCode: pr.interrupted === true ? 0 : pr.exitCode,
3795
3876
  skipped: pr.skipped,
3796
3877
  interrupted: pr.interrupted,
@@ -3802,12 +3883,13 @@ export async function runSubagent(
3802
3883
  intercomTarget: pr.intercomTarget,
3803
3884
  model: pr.model,
3804
3885
  thinking: pr.thinking,
3805
- attemptedModels: pr.attemptedModels,
3806
- modelAttempts: pr.modelAttempts,
3886
+ requestedModel: pr.requestedModel,
3807
3887
  contextOverflow: pr.contextOverflow,
3808
3888
  totalCost: pr.totalCost,
3809
3889
  usage: pr.usage,
3810
3890
  artifactPaths: pr.artifactPaths,
3891
+ outputSaveError: pr.outputSaveError,
3892
+ artifactOutputSaveFailed: pr.artifactOutputSaveFailed,
3811
3893
  transcriptPath: pr.transcriptPath,
3812
3894
  transcriptError: pr.transcriptError,
3813
3895
  effects: pr.effects,
@@ -4122,6 +4204,8 @@ export async function runSubagent(
4122
4204
  onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking, attempt.contextLimit),
4123
4205
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
4124
4206
  onExternalProcess: (process) => updateExternalProcess(fi, process),
4207
+ prepareExternalActivity: (externalCwd, signal) => prepareExternalActivity(fi, externalCwd, signal),
4208
+ onExternalStreamActivity: () => recordExternalStreamActivity(fi),
4125
4209
  onExternalJob: (externalJob) => updateExternalJob(fi, externalJob),
4126
4210
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(fi),
4127
4211
  usageBudgetExhausted: continuationUsageBudgetExhausted,
@@ -4137,7 +4221,7 @@ export async function runSubagent(
4137
4221
  const childInterrupted = singleResult.interrupted === true;
4138
4222
  const childStopped = singleResult.stopped === true;
4139
4223
 
4140
- requiredStatusStep(statusPayload, fi).status = stopped || childStopped ? "stopped" : timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
4224
+ requiredStatusStep(statusPayload, fi).status = stopped || childStopped ? "stopped" : timedOut ? "failed" : childInterrupted ? "paused" : singleResult.execution?.status === "partial" ? "partial" : singleResult.exitCode === 0 ? "complete" : "failed";
4141
4225
  requiredStatusStep(statusPayload, fi).endedAt = taskEndTime;
4142
4226
  requiredStatusStep(statusPayload, fi).durationMs = taskDuration;
4143
4227
  requiredStatusStep(statusPayload, fi).exitCode = stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
@@ -4150,8 +4234,7 @@ export async function runSubagent(
4150
4234
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "sessionName", singleResult.sessionName);
4151
4235
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "model", singleResult.model);
4152
4236
  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);
4237
+ setOptionalProperty(requiredStatusStep(statusPayload, fi), "requestedModel", singleResult.requestedModel);
4155
4238
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "contextOverflow", singleResult.contextOverflow);
4156
4239
  setOptionalProperty(requiredStatusStep(statusPayload, fi), "totalCost", singleResult.totalCost);
4157
4240
  if (singleResult.totalCost) {
@@ -4218,7 +4301,7 @@ export async function runSubagent(
4218
4301
  const sessionTokens = config.sessionDir
4219
4302
  ? parseSessionTokens(path.join(config.sessionDir, `parallel-${t}`))
4220
4303
  : null;
4221
- const fallbackTokens = tokenUsageFromAttempts(parallelResults[t]?.modelAttempts);
4304
+ const fallbackTokens = tokenUsageFromUsage(parallelResults[t]?.usage);
4222
4305
  const observedTokens = requiredStatusStep(statusPayload, fi).tokens;
4223
4306
  const taskTokens = sessionTokens ?? (fallbackTokens
4224
4307
  ? { ...fallbackTokens, ...(observedTokens?.window !== undefined ? { window: observedTokens.window } : {}), ...(observedTokens?.windowPeak !== undefined ? { windowPeak: observedTokens.windowPeak } : {}) }
@@ -4249,7 +4332,7 @@ export async function runSubagent(
4249
4332
  output: pr.output,
4250
4333
  outputState: pr.outputState,
4251
4334
  error: pr.error,
4252
- success: pr.stopped !== true && pr.interrupted !== true && pr.exitCode === 0,
4335
+ success: pr.stopped !== true && pr.interrupted !== true && pr.exitCode === 0 && pr.execution?.status !== "partial",
4253
4336
  exitCode: pr.interrupted === true ? 0 : pr.exitCode,
4254
4337
  skipped: pr.skipped,
4255
4338
  interrupted: pr.interrupted,
@@ -4261,12 +4344,13 @@ export async function runSubagent(
4261
4344
  intercomTarget: pr.intercomTarget,
4262
4345
  model: pr.model,
4263
4346
  thinking: pr.thinking,
4264
- attemptedModels: pr.attemptedModels,
4265
- modelAttempts: pr.modelAttempts,
4347
+ requestedModel: pr.requestedModel,
4266
4348
  contextOverflow: pr.contextOverflow,
4267
4349
  totalCost: pr.totalCost,
4268
4350
  usage: pr.usage,
4269
4351
  artifactPaths: pr.artifactPaths,
4352
+ outputSaveError: pr.outputSaveError,
4353
+ artifactOutputSaveFailed: pr.artifactOutputSaveFailed,
4270
4354
  transcriptPath: pr.transcriptPath,
4271
4355
  transcriptError: pr.transcriptError,
4272
4356
  effects: pr.effects,
@@ -4299,7 +4383,7 @@ export async function runSubagent(
4299
4383
  exitCode: r.exitCode,
4300
4384
  error: r.error,
4301
4385
  model: r.model,
4302
- attemptedModels: r.attemptedModels,
4386
+ requestedModel: r.requestedModel,
4303
4387
  })),
4304
4388
  );
4305
4389
  if (worktreeSetup) {
@@ -4523,6 +4607,8 @@ export async function runSubagent(
4523
4607
  onAttemptStart: (attempt) => updateStepModel(flatIndex, attempt.model, attempt.thinking, attempt.contextLimit),
4524
4608
  onChildEvent: (event) => updateStepFromChildEvent(flatIndex, event),
4525
4609
  onExternalProcess: (process) => updateExternalProcess(flatIndex, process),
4610
+ prepareExternalActivity: (externalCwd, signal) => prepareExternalActivity(flatIndex, externalCwd, signal),
4611
+ onExternalStreamActivity: () => recordExternalStreamActivity(flatIndex),
4526
4612
  onExternalJob: (externalJob) => updateExternalJob(flatIndex, externalJob),
4527
4613
  skipAcceptance: () => timedOut || stopped || childStopRequests.has(flatIndex),
4528
4614
  usageBudgetExhausted: continuationUsageBudgetExhausted,
@@ -4550,19 +4636,20 @@ export async function runSubagent(
4550
4636
  output: stopped || childStopped ? stopMessage : timedOut ? singleResult.output || (timeoutMessage ?? "Subagent timed out.") : singleResult.output,
4551
4637
  outputState: singleResult.outputState,
4552
4638
  error: stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error,
4553
- success: !stopped && !childStopped && !timedOut && singleResult.interrupted !== true && singleResult.exitCode === 0,
4639
+ success: !stopped && !childStopped && !timedOut && singleResult.interrupted !== true && singleResult.exitCode === 0 && singleResult.execution?.status !== "partial",
4554
4640
  exitCode: stopped || childStopped ? 1 : timedOut ? 1 : singleResult.interrupted === true ? 0 : singleResult.exitCode,
4555
4641
  sessionFile: singleResult.sessionFile,
4556
4642
  intercomTarget: singleResult.intercomTarget,
4557
4643
  model: singleResult.model,
4558
4644
  thinking: singleResult.thinking,
4559
- attemptedModels: singleResult.attemptedModels,
4560
- modelAttempts: singleResult.modelAttempts,
4645
+ requestedModel: singleResult.requestedModel,
4561
4646
  contextOverflow: singleResult.contextOverflow,
4562
4647
  totalCost: singleResult.totalCost,
4563
4648
  usage: singleResult.usage,
4564
4649
  artifactPaths: singleResult.artifactPaths,
4565
4650
  savedOutputPath: singleResult.savedOutputPath,
4651
+ outputSaveError: singleResult.outputSaveError,
4652
+ artifactOutputSaveFailed: singleResult.artifactOutputSaveFailed,
4566
4653
  transcriptPath: singleResult.transcriptPath,
4567
4654
  transcriptError: singleResult.transcriptError,
4568
4655
  effects: singleResult.effects,
@@ -4607,7 +4694,7 @@ export async function runSubagent(
4607
4694
  if (cumulativeTokens) {
4608
4695
  previousCumulativeTokens = cumulativeTokens;
4609
4696
  } else {
4610
- const fallbackTokens = tokenUsageFromAttempts(singleResult.modelAttempts);
4697
+ const fallbackTokens = tokenUsageFromUsage(singleResult.usage);
4611
4698
  const observedTokens = requiredStatusStep(statusPayload, flatIndex).tokens;
4612
4699
  stepTokens = fallbackTokens
4613
4700
  ? { ...fallbackTokens, ...(observedTokens?.window !== undefined ? { window: observedTokens.window } : {}), ...(observedTokens?.windowPeak !== undefined ? { windowPeak: observedTokens.windowPeak } : {}) }
@@ -4627,7 +4714,7 @@ export async function runSubagent(
4627
4714
 
4628
4715
  const stepEndTime = Date.now();
4629
4716
  const childInterrupted = singleResult.interrupted === true;
4630
- requiredStatusStep(statusPayload, flatIndex).status = stopped || childStopped ? "stopped" : timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
4717
+ requiredStatusStep(statusPayload, flatIndex).status = stopped || childStopped ? "stopped" : timedOut ? "failed" : childInterrupted ? "paused" : singleResult.execution?.status === "partial" ? "partial" : singleResult.exitCode === 0 ? "complete" : "failed";
4631
4718
  requiredStatusStep(statusPayload, flatIndex).endedAt = stepEndTime;
4632
4719
  requiredStatusStep(statusPayload, flatIndex).durationMs = stepEndTime - stepStartTime;
4633
4720
  requiredStatusStep(statusPayload, flatIndex).exitCode = stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
@@ -4640,8 +4727,7 @@ export async function runSubagent(
4640
4727
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "sessionName", singleResult.sessionName);
4641
4728
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "model", singleResult.model);
4642
4729
  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);
4730
+ setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "requestedModel", singleResult.requestedModel);
4645
4731
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "contextOverflow", singleResult.contextOverflow);
4646
4732
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "totalCost", singleResult.totalCost);
4647
4733
  setOptionalProperty(requiredStatusStep(statusPayload, flatIndex), "error", stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error);
@@ -4756,6 +4842,34 @@ export async function runSubagent(
4756
4842
  }
4757
4843
  }
4758
4844
 
4845
+ await Promise.all([...externalActivityEvidence.values()].map((evidence) => evidence.probeInFlight).filter((probe): probe is Promise<void> => probe !== undefined));
4846
+ if (periodicGitProbeFailure) {
4847
+ stopped = false;
4848
+ timedOut = false;
4849
+ interrupted = false;
4850
+ delete statusPayload.stopped;
4851
+ delete statusPayload.timedOut;
4852
+ statusPayload.error = periodicGitProbeFailure.message;
4853
+ const step = statusPayload.steps[periodicGitProbeFailure.index];
4854
+ if (step) {
4855
+ step.status = "failed";
4856
+ step.error = periodicGitProbeFailure.message;
4857
+ step.exitCode = 1;
4858
+ delete step.stopped;
4859
+ delete step.timedOut;
4860
+ }
4861
+ const result = results[periodicGitProbeFailure.index];
4862
+ if (result) {
4863
+ result.success = false;
4864
+ result.exitCode = 1;
4865
+ result.error = periodicGitProbeFailure.message;
4866
+ result.output = periodicGitProbeFailure.message;
4867
+ result.stopped = false;
4868
+ result.timedOut = false;
4869
+ result.interrupted = false;
4870
+ }
4871
+ }
4872
+
4759
4873
  let summary = results.map((r) => `${r.agent}:\n${r.output || (r.exitCode !== 0 ? r.error : undefined) || "(no output)"}`).join("\n\n");
4760
4874
  let truncated = false;
4761
4875
 
@@ -4821,9 +4935,15 @@ export async function runSubagent(
4821
4935
  clearTimeout(timeoutTimer);
4822
4936
  timeoutTimer = undefined;
4823
4937
  }
4938
+ if (checkpointTimer) {
4939
+ clearTimeout(checkpointTimer);
4940
+ checkpointTimer = undefined;
4941
+ }
4824
4942
  if (!timedOut && !stopped && !interrupted && config.timeoutMs !== undefined && timeoutMessage !== undefined && results.some((result) => result.timedOut === true && result.error?.startsWith(timeoutMessage))) {
4825
4943
  timedOut = true;
4826
4944
  }
4945
+ disposeControlInbox();
4946
+ for (const request of consumeStopRequestPayloads(asyncDir)) stopChildStep(request);
4827
4947
  const signalTerminated = !stopped && !timedOut && !interrupted && results.some((result) => result.exitCode !== 0 && isUnexplainedProcessSignal(omitUndefinedProperties({
4828
4948
  processSignal: result.processSignal,
4829
4949
  interrupted: result.interrupted,
@@ -4838,7 +4958,6 @@ export async function runSubagent(
4838
4958
  });
4839
4959
  statusPayload.state = stopped || signalTerminated ? "stopped" : timedOut || usageBudgetExceeded ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : partialWithEvidence ? "partial" : "failed";
4840
4960
  closeSteerInbox(asyncDir, statusPayload.state, (filePath, payload) => runPersistence.write(filePath, payload));
4841
- disposeControlInbox();
4842
4961
  for (const request of consumeSteerRequests(asyncDir)) deliverSteerRequest(request);
4843
4962
  const effectiveSessionFile = sessionFile ?? latestSessionFile;
4844
4963
  const steeringLifecycle = steeringStatus(statusPayload);
@@ -4874,7 +4993,7 @@ export async function runSubagent(
4874
4993
  statusPayload.activityState = "needs_attention";
4875
4994
  statusPayload.error = partialResult?.error ?? statusPayload.error;
4876
4995
  for (const step of statusPayload.steps) {
4877
- if (step.status === "failed") step.activityState = "needs_attention";
4996
+ if (step.status === "failed" || step.status === "partial") step.activityState = "needs_attention";
4878
4997
  }
4879
4998
  }
4880
4999
  statusPayload.endedAt = runEndedAt;
@@ -4931,14 +5050,14 @@ export async function runSubagent(
4931
5050
  intercomTarget: r.intercomTarget,
4932
5051
  model: r.model,
4933
5052
  thinking: r.thinking,
4934
- attemptedModels: r.attemptedModels,
4935
- modelAttempts: r.modelAttempts,
5053
+ requestedModel: r.requestedModel,
4936
5054
  contextOverflow: r.contextOverflow,
4937
5055
  totalCost: r.totalCost,
4938
5056
  usage: r.usage,
4939
5057
  artifactPaths: r.artifactPaths,
4940
5058
  savedOutputPath: r.savedOutputPath,
4941
5059
  outputSaveError: r.outputSaveError,
5060
+ artifactOutputSaveFailed: r.artifactOutputSaveFailed,
4942
5061
  metadataSaveError: r.metadataSaveError,
4943
5062
  truncated: r.truncated,
4944
5063
  transcriptPath: r.transcriptPath,
@@ -5074,7 +5193,7 @@ export async function runSubagent(
5074
5193
  async function waitForStartupControl(
5075
5194
  controlPath: string,
5076
5195
  token: string,
5077
- action: "ack" | "proceed",
5196
+ action: "ack" | "confirm" | "proceed",
5078
5197
  timeoutMs = 30_000,
5079
5198
  ): Promise<void> {
5080
5199
  const deadline = Date.now() + timeoutMs;
@@ -5088,7 +5207,7 @@ async function waitForStartupControl(
5088
5207
  }
5089
5208
  if (payload.token !== token) throw new Error("Runner startup control token does not match.");
5090
5209
  if (payload.action === action) return;
5091
- if (payload.action !== "ack" && payload.action !== "proceed") throw new Error("Runner startup control action is invalid.");
5210
+ if (payload.action !== "ack" && payload.action !== "confirm" && payload.action !== "proceed") throw new Error("Runner startup control action is invalid.");
5092
5211
  }
5093
5212
  await new Promise((resolve) => setTimeout(resolve, 20));
5094
5213
  }
@@ -5125,6 +5244,8 @@ export async function runConfiguredSubagent(config: SubagentRunConfig, options?:
5125
5244
  writeAtomicJson(startupPath, { state: "ready", token: lease.owner.token, pid: process.pid, owner: lease.owner });
5126
5245
  await waitForStartupControl(startupAckPath, lease.owner.token, "ack");
5127
5246
  writeAtomicJson(startupPath, { state: "acknowledged", token: lease.owner.token, pid: process.pid });
5247
+ await waitForStartupControl(startupAckPath, lease.owner.token, "confirm");
5248
+ writeAtomicJson(startupPath, { state: "confirmed", token: lease.owner.token, pid: process.pid });
5128
5249
  await waitForStartupControl(startupProceedPath, lease.owner.token, "proceed");
5129
5250
  startupCommitted = true;
5130
5251
  for (const controlPath of [startupAckPath, startupProceedPath]) {