pi-subagents 0.67.0 → 0.69.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +1 -1
  3. package/docs/agents.md +41 -12
  4. package/docs/configuration.md +61 -19
  5. package/docs/extension-api.md +5 -1
  6. package/docs/missions.md +2 -2
  7. package/docs/models.md +11 -79
  8. package/docs/observability.md +18 -8
  9. package/docs/standalone-background.md +13 -3
  10. package/docs/tool-reference.md +38 -14
  11. package/docs/watchdog.md +10 -12
  12. package/docs/workflows.md +59 -1
  13. package/index.ts +5 -2
  14. package/package.json +4 -2
  15. package/runner-peer-loader.mjs +24 -0
  16. package/runner-peer-preload.mjs +25 -11
  17. package/skills/pi-subagents/SKILL.md +18 -21
  18. package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
  19. package/skills/pi-subagents/references/execution-controls.md +6 -4
  20. package/skills/pi-subagents/references/management-authoring-rpc.md +0 -1
  21. package/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
  22. package/skills/pi-subagents/references/prompting-and-roles.md +16 -12
  23. package/skills/pi-subagents/references/review-and-validation.md +3 -3
  24. package/src/agents/agent-management.ts +57 -58
  25. package/src/agents/agent-serializer.ts +4 -3
  26. package/src/agents/agents.ts +185 -72
  27. package/src/agents/chain-serializer.ts +5 -0
  28. package/src/agents/runtime-agent-registry.ts +7 -6
  29. package/src/agents/skills.ts +1 -1
  30. package/src/api/preflight.ts +20 -16
  31. package/src/api/required-child-extensions.ts +6 -0
  32. package/src/extension/config.ts +10 -37
  33. package/src/extension/fanout-child.ts +3 -0
  34. package/src/extension/herdr-pi-bridge.ts +160 -0
  35. package/src/extension/index.ts +42 -31
  36. package/src/extension/public-execution.ts +3 -3
  37. package/src/extension/schemas.ts +23 -6
  38. package/src/extension/tool-description.ts +8 -7
  39. package/src/inspectors/ghostty/plugin.ts +13 -1
  40. package/src/intercom/native-supervisor-channel.ts +22 -18
  41. package/src/policy/authority.ts +4 -0
  42. package/src/profiles/profiles.ts +12 -6
  43. package/src/runs/background/active-run-index.ts +17 -1
  44. package/src/runs/background/async-execution.ts +309 -126
  45. package/src/runs/background/async-job-tracker.ts +8 -6
  46. package/src/runs/background/async-resume.ts +13 -4
  47. package/src/runs/background/async-status.ts +15 -4
  48. package/src/runs/background/auto-drain.ts +20 -10
  49. package/src/runs/background/binary-bootstrap.ts +5 -0
  50. package/src/runs/background/chain-append.ts +1 -1
  51. package/src/runs/background/chain-root-attachment.ts +14 -33
  52. package/src/runs/background/notify.ts +74 -6
  53. package/src/runs/background/result-files.ts +8 -4
  54. package/src/runs/background/result-watcher.ts +19 -2
  55. package/src/runs/background/run-child-session.ts +20 -29
  56. package/src/runs/background/runner-aliases.ts +4 -33
  57. package/src/runs/background/runner-child-launch.ts +4 -1
  58. package/src/runs/background/runner-child-sessions.ts +2 -2
  59. package/src/runs/background/runner-http-dispatcher.ts +119 -0
  60. package/src/runs/background/scheduled-runs.ts +11 -5
  61. package/src/runs/background/stale-run-reconciler.ts +35 -11
  62. package/src/runs/background/subagent-runner.ts +413 -276
  63. package/src/runs/background/subagent-wait.ts +128 -23
  64. package/src/runs/background/wait-completions.ts +75 -27
  65. package/src/runs/background/wait-subscriptions.ts +9 -3
  66. package/src/runs/background/wait-tool.ts +4 -2
  67. package/src/runs/foreground/async-stop-action.ts +93 -3
  68. package/src/runs/foreground/execution.ts +115 -219
  69. package/src/runs/foreground/foreground-history.ts +2 -1
  70. package/src/runs/foreground/subagent-executor.ts +281 -80
  71. package/src/runs/shared/acceptance.ts +194 -37
  72. package/src/runs/shared/async-status-projection.ts +123 -33
  73. package/src/runs/shared/child-launch-plan.ts +15 -3
  74. package/src/runs/shared/child-launch.ts +19 -6
  75. package/src/runs/shared/child-runtime-config.ts +5 -0
  76. package/src/runs/shared/child-session.ts +94 -50
  77. package/src/runs/shared/child-tool-plan.ts +28 -16
  78. package/src/runs/shared/dynamic-fanout.ts +2 -2
  79. package/src/runs/shared/external-cli-contract.ts +11 -1
  80. package/src/runs/shared/external-cli-preflight.ts +6 -2
  81. package/src/runs/shared/herdr-connection.ts +134 -0
  82. package/src/runs/shared/herdr-external-adapters.ts +169 -0
  83. package/src/runs/shared/herdr-machine.ts +279 -0
  84. package/src/runs/shared/herdr-pi-protocol.ts +59 -0
  85. package/src/runs/shared/herdr-placed-run.ts +263 -0
  86. package/src/runs/shared/model-resolution-diagnostic.ts +76 -0
  87. package/src/runs/shared/{model-fallback.ts → model-resolution.ts} +22 -237
  88. package/src/runs/shared/model-scope.ts +1 -1
  89. package/src/runs/shared/nested-events.ts +11 -2
  90. package/src/runs/shared/parallel-utils.ts +7 -2
  91. package/src/runs/shared/pi-spawn.ts +1 -1
  92. package/src/runs/shared/subagent-prompt-runtime.ts +4 -2
  93. package/src/runs/shared/worktree-setup-command.ts +27 -4
  94. package/src/runs/shared/worktree.ts +30 -8
  95. package/src/shared/child-cache-retention.ts +43 -0
  96. package/src/shared/launch-contract.ts +6 -9
  97. package/src/shared/pruned-fork.ts +1 -1
  98. package/src/shared/required-child-extensions.ts +81 -0
  99. package/src/shared/settings.ts +5 -2
  100. package/src/shared/shortcuts.ts +0 -4
  101. package/src/shared/types.ts +81 -29
  102. package/src/slash/slash-commands.ts +0 -6
  103. package/src/slash/subagents-admin.ts +13 -9
  104. package/src/tui/render.ts +20 -10
  105. package/src/watchdog/child-status.ts +28 -36
  106. package/src/watchdog/lsp-diagnostics.ts +1 -1
  107. package/src/watchdog/model-selection.ts +1 -1
  108. package/src/watchdog/register-child.ts +10 -3
  109. package/src/watchdog/register-main.ts +20 -20
  110. package/src/watchdog/render.ts +1 -1
  111. package/src/watchdog/review.ts +14 -30
  112. package/src/watchdog/rules.ts +1 -1
  113. package/src/watchdog/runtime.ts +23 -12
  114. package/src/watchdog/settings.ts +3 -6
  115. package/src/watchdog/types.ts +3 -5
  116. package/src/watchdog/warning-format.ts +1 -1
  117. package/src/workflows/scripted-workflow.ts +68 -7
  118. package/src/workflows/workflow-receipt.ts +21 -3
  119. package/src/workflows/workflow-resources.ts +13 -2
  120. package/src/runs/shared/model-exclusions.ts +0 -374
  121. package/src/runs/shared/readonly-model-continuation.ts +0 -69
  122. package/src/runs/shared/readonly-session-evidence.ts +0 -307
@@ -2,18 +2,20 @@
2
2
  * Async execution logic for subagent tool
3
3
  */
4
4
 
5
- import { spawn } from "node:child_process";
5
+ import { spawn, type ChildProcess } from "node:child_process";
6
6
  import { randomUUID } from "node:crypto";
7
7
  import * as fs from "node:fs";
8
8
  import * as path from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
- import { createRequire } from "node:module";
10
+ import * as nodeModule from "node:module";
11
11
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
12
  import { discoverAgents, formatUnknownAgentError, unknownAgentDiagnosticContext, type AgentConfig, type UnknownAgentDiagnosticContext } from "../../agents/agents.ts";
13
13
  import { createAtomicJsonWriter, writePrivateAtomicJson } from "../../shared/atomic-json.ts";
14
+ import { childCacheRetentionEnv } from "../../shared/child-cache-retention.ts";
14
15
  import { buildEffectiveSystemPrompt } from "../shared/effective-system-prompt.ts";
15
16
  import { currentCompletionOwnerId } from "../../shared/completion-owner.ts";
16
- import { planChildLaunch, resolveStepBehavior, suppressProgressForReadOnlyTask, type ResolvedStepBehavior } from "../shared/child-launch-plan.ts";
17
+ import { planChildLaunch, projectChainOutputSchemas, resolveStepBehavior, suppressProgressForReadOnlyTask, type ResolvedStepBehavior } from "../shared/child-launch-plan.ts";
18
+ import { formatHerdrMachineRunnerUnsupported, resolveHerdrMachinePlacement } from "../shared/herdr-machine.ts";
17
19
  import { applyThinkingSuffix, getHostBuiltinToolNames, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/child-tool-plan.ts";
18
20
  import { injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
19
21
  import { applyWatchdogLaunchRules, sendRuleViolationWarning } from "../../watchdog/rules.ts";
@@ -27,7 +29,7 @@ import { resolveNodeExecutable } from "../../shared/node-executable.ts";
27
29
  import { backgroundProcessOptions } from "../shared/background-process-options.ts";
28
30
  import { normalizeSkillInput, resolveSkillsWithFallback } from "../../agents/skills.ts";
29
31
  import { PI_CODING_AGENT_PACKAGE_ROOT_ENV, PROMPT_REDACTED, resolveChildCwd } from "../../shared/utils.ts";
30
- import { buildModelCandidates, resolveEffectiveSubagentModel, resolveModelOrigin, resolveSubagentModelOverride, type AvailableModelInfo, type ModelOrigin, type ParentModel } from "../shared/model-fallback.ts";
32
+ import { resolveEffectiveSubagentModel, resolveModelOrigin, resolveModelSelection, resolveSubagentModelOverride, type AvailableModelInfo, type ModelOrigin, type ParentModel } from "../shared/model-resolution.ts";
31
33
  import { resolveToolTimeoutMs, toolTimeoutFromEnv } from "../shared/tool-timeout.ts";
32
34
  import { resolveModelScopesForAgent, type ModelScopeConfig } from "../shared/model-scope.ts";
33
35
  import { findModelInfo, resolveEffectiveThinking } from "../../shared/model-info.ts";
@@ -47,6 +49,7 @@ import {
47
49
  type ArtifactConfig,
48
50
  type Details,
49
51
  type IntercomBridgeConfig,
52
+ type HerdrMachineReference,
50
53
  type JsonSchemaObject,
51
54
  type MaxOutputConfig,
52
55
  type NestedRouteInfo,
@@ -70,6 +73,7 @@ import type { ChildRuntimeConfig } from "../shared/child-runtime-config.ts";
70
73
  import { childSessionFactoryModule } from "../shared/child-session.ts";
71
74
  import { inheritedChildRuntime } from "../shared/child-launch.ts";
72
75
  import { resultFilePath } from "./result-files.ts";
76
+ import { updateActiveRunIndex } from "./active-run-index.ts";
73
77
  import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
74
78
  import { usageBudgetState } from "../shared/usage-budget.ts";
75
79
  import type { ImportedAsyncRoot } from "./chain-root-attachment.ts";
@@ -83,9 +87,25 @@ import { resolveLaunchBinding } from "../../shared/launch-contract.ts";
83
87
  import { resolvePermissionRules, type PermissionConfig } from "../shared/permissions.ts";
84
88
  import { normalizeExtensionBindings, omitExtensionBindingsEnv, type ExtensionBindings } from "../shared/extension-bindings.ts";
85
89
  import { assertWorkflowLaneKey, normalizeWorkflowLaneMetadata } from "../shared/lane-metadata.ts";
90
+ import { resolveRequiredChildExtensions, type RequiredChildExtensionSnapshot } from "../../shared/required-child-extensions.ts";
86
91
 
87
- const require = createRequire(import.meta.url);
88
- const piPackageRoot = resolvePiPackageRoot() ?? resolveInstalledPiPackageRoot();
92
+ const require = nodeModule.createRequire(import.meta.url);
93
+ const piPackageRoot = resolveAsyncPiPackageRoot();
94
+
95
+ /**
96
+ * The detached runner resolves the same host package the foreground
97
+ * `resolvePiCliScript` path resolves, so an explicit
98
+ * `PI_SUBAGENTS_PI_CODING_AGENT_PACKAGE_ROOT` override must be honored here
99
+ * too. Precedence mirrors the foreground resolver: argv-based discovery wins
100
+ * first (the foreground returns the argv script before any candidate), the
101
+ * environment override is consulted when that discovery cannot identify the
102
+ * host (wrapper installs, non-standard layouts), and the
103
+ * package-manager entry is last. Without the override, such hosts fail
104
+ * closed with "neither is available" while foreground children launch fine.
105
+ */
106
+ function resolveAsyncPiPackageRoot(env: NodeJS.ProcessEnv = process.env): string | undefined {
107
+ return resolvePiPackageRoot() || env[PI_CODING_AGENT_PACKAGE_ROOT_ENV]?.trim() || resolveInstalledPiPackageRoot();
108
+ }
89
109
 
90
110
  function resolveJitiCliFromPackageJson(packageJsonPath: string): string | undefined {
91
111
  if (!fs.existsSync(packageJsonPath)) return undefined;
@@ -109,12 +129,12 @@ function resolveJitiCliPath(): string | undefined {
109
129
  const candidates: Array<() => string | undefined> = [
110
130
  () => require.resolve("jiti/package.json"),
111
131
  () => piPackageRoot
112
- ? createRequire(path.join(piPackageRoot, "package.json")).resolve("jiti/package.json")
132
+ ? nodeModule.createRequire(path.join(piPackageRoot, "package.json")).resolve("jiti/package.json")
113
133
  : undefined,
114
134
  () => {
115
135
  if (!process.argv[1]) return undefined;
116
136
  const piEntry = fs.realpathSync(process.argv[1]);
117
- return createRequire(piEntry).resolve("jiti/package.json");
137
+ return nodeModule.createRequire(piEntry).resolve("jiti/package.json");
118
138
  },
119
139
  () => piPackageRoot ? path.join(piPackageRoot, "node_modules", "jiti", "package.json") : undefined,
120
140
  ];
@@ -132,6 +152,17 @@ function resolveJitiCliPath(): string | undefined {
132
152
  }
133
153
 
134
154
  const jitiCliPath = resolveJitiCliPath();
155
+ const asyncRunnerSourcePath = path.join(
156
+ path.dirname(fileURLToPath(import.meta.url)),
157
+ `subagent-runner${path.extname(fileURLToPath(import.meta.url))}`,
158
+ );
159
+ const sourceUnderNodeModules = asyncRunnerSourcePath.split(path.sep).some((segment) => segment.toLowerCase() === "node_modules");
160
+ function supportsNativeRunner(nodeExecutable: string): boolean {
161
+ return Boolean(process.features.typescript)
162
+ && typeof nodeModule.registerHooks === "function"
163
+ && nodeExecutable === process.execPath
164
+ && !sourceUnderNodeModules;
165
+ }
135
166
 
136
167
  interface AsyncExecutionContext {
137
168
  pi: ExtensionAPI;
@@ -168,6 +199,9 @@ interface AsyncChainParams {
168
199
  availableModels?: AvailableModelInfo[];
169
200
  cwd?: string;
170
201
  maxOutput?: MaxOutputConfig;
202
+ machine?: string;
203
+ /** Launch cwd as typed when a machine is set: a path on that machine, never resolved locally. */
204
+ machineCwd?: string;
171
205
  artifactsDir?: string;
172
206
  artifactConfig: ArtifactConfig;
173
207
  shareEnabled: boolean;
@@ -223,9 +257,13 @@ interface AsyncSingleParams {
223
257
  agentConfig: AgentConfig;
224
258
  /** Agent contract before per-run bridge injection, used only for recovery persistence. */
225
259
  recoveryAgentConfig?: AgentConfig;
260
+ requiredExtensions?: RequiredChildExtensionSnapshot;
226
261
  ctx: AsyncExecutionContext;
227
262
  cwd?: string;
228
263
  requestedCwd?: string;
264
+ machine?: string;
265
+ /** Launch cwd as typed when a machine is set: a path on that machine, never resolved locally. */
266
+ machineCwd?: string;
229
267
  maxOutput?: MaxOutputConfig;
230
268
  artifactsDir?: string;
231
269
  artifactConfig: ArtifactConfig;
@@ -269,6 +307,8 @@ interface AsyncSingleParams {
269
307
  absoluteDeadlineAt?: number;
270
308
  /** Optional per-call hard toolTimeoutMs override (highest precedence). */
271
309
  toolTimeoutMs?: number;
310
+ /** Steer the child to checkpoint and stop this many ms before the run deadline (resolved call param ?? config). */
311
+ checkpointBeforeDeadlineMs?: number;
272
312
  toolBudget?: ResolvedToolBudget | ToolBudgetConfig;
273
313
  usageBudget?: UsageBudgetConfig;
274
314
  configToolBudget?: ResolvedToolBudget;
@@ -312,6 +352,8 @@ export interface AsyncRunnerStepBuildParams {
312
352
  ctx: AsyncExecutionContext;
313
353
  availableModels?: AvailableModelInfo[];
314
354
  cwd?: string;
355
+ machine?: string;
356
+ machineCwd?: string;
315
357
  chainSkills?: string[];
316
358
  sessionFilesByFlatIndex?: (string | undefined)[];
317
359
  thinkingOverridesByFlatIndex?: (AgentConfig["thinking"] | undefined)[];
@@ -368,11 +410,9 @@ export function formatAsyncStartedMessage(headline: string, interactive: boolean
368
410
  return [headline, "", ...guidance].join("\n");
369
411
  }
370
412
 
371
- /**
372
- * Check if jiti is available for async execution
373
- */
413
+ /** Check whether detached async execution has a supported runtime. */
374
414
  export function isAsyncAvailable(): boolean {
375
- return jitiCliPath !== undefined;
415
+ return resolveBunPiExecutable() !== undefined || supportsNativeRunner(resolveNodeExecutable()) || jitiCliPath !== undefined;
376
416
  }
377
417
 
378
418
  export function resolveAsyncRunnerLogPaths(cfg: object): { stdoutPath: string; stderrPath: string } | undefined {
@@ -399,24 +439,14 @@ function closeFd(fd: number | undefined): void {
399
439
  * Spawn the async runner process
400
440
  */
401
441
  const RUNNER_STARTUP_TIMEOUT_MS = 10_000;
402
- const RUNNER_STARTUP_WAIT_BUFFER = typeof SharedArrayBuffer !== "undefined" ? new SharedArrayBuffer(4) : undefined;
403
- const RUNNER_STARTUP_WAIT_VIEW = RUNNER_STARTUP_WAIT_BUFFER ? new Int32Array(RUNNER_STARTUP_WAIT_BUFFER) : undefined;
404
-
405
- type RunnerStartupState = "ready" | "acknowledged";
442
+ type RunnerStartupState = "ready" | "acknowledged" | "confirmed";
406
443
 
407
444
  type RunnerStartupWaitResult =
408
445
  | { ok: true; token: string }
409
446
  | { ok: false; error: string; startupDidNotProceed?: boolean };
410
447
 
411
- function waitForStartupInterval(delayMs = 20): void {
412
- if (RUNNER_STARTUP_WAIT_VIEW) {
413
- Atomics.wait(RUNNER_STARTUP_WAIT_VIEW, 0, 0, delayMs);
414
- return;
415
- }
416
- const waitUntil = Date.now() + delayMs;
417
- while (Date.now() < waitUntil) {
418
- // Startup handshakes are synchronous so resume rejects before reporting a run as started.
419
- }
448
+ function waitForStartupInterval(delayMs = 20): Promise<void> {
449
+ return new Promise((resolve) => setTimeout(resolve, delayMs));
420
450
  }
421
451
 
422
452
  function readRunnerStartup(startupPath: string, expectedState: RunnerStartupState, expectedToken?: string): RunnerStartupWaitResult | undefined {
@@ -434,13 +464,32 @@ function readRunnerStartup(startupPath: string, expectedState: RunnerStartupStat
434
464
  }
435
465
  }
436
466
 
437
- function waitForRunnerStartup(startupPath: string, expectedState: RunnerStartupState, timeoutMs: number, expectedToken?: string): RunnerStartupWaitResult {
467
+ async function waitForRunnerStartup(startupPath: string, expectedState: RunnerStartupState, timeoutMs: number, expectedToken?: string, processClosed?: Promise<{ exitCode: number | null; signal: NodeJS.Signals | null }>): Promise<RunnerStartupWaitResult> {
468
+ const confirmOpen = async (result: RunnerStartupWaitResult): Promise<RunnerStartupWaitResult> => {
469
+ if (!processClosed || result.ok === false) return result;
470
+ const outcome = await Promise.race([processClosed.then((exit) => ({ exit })), new Promise<undefined>((resolve) => setImmediate(() => resolve(undefined)))]);
471
+ if (!outcome) return result;
472
+ const finalResult = readRunnerStartup(startupPath, expectedState, expectedToken);
473
+ if (finalResult?.ok === false) return finalResult;
474
+ const { exitCode, signal } = outcome.exit;
475
+ return { ok: false, error: `Async runner exited before startup state '${expectedState}' (exit code ${exitCode ?? "unknown"}, signal ${signal ?? "none"}).`, startupDidNotProceed: true };
476
+ };
438
477
  const deadline = Date.now() + timeoutMs;
439
- for (;;) {
478
+ while (Date.now() <= deadline) {
440
479
  const result = readRunnerStartup(startupPath, expectedState, expectedToken);
441
- if (result) return result;
442
- if (Date.now() >= deadline) break;
443
- waitForStartupInterval(Math.min(20, Math.max(1, deadline - Date.now())));
480
+ if (result) return await confirmOpen(result);
481
+ const delay = waitForStartupInterval(Math.min(20, Math.max(1, deadline - Date.now())));
482
+ if (processClosed) {
483
+ const outcome = await Promise.race([delay.then(() => undefined), processClosed.then((exit) => ({ exit }))]);
484
+ if (outcome) {
485
+ const finalResult = readRunnerStartup(startupPath, expectedState, expectedToken);
486
+ if (finalResult?.ok === false) return finalResult;
487
+ const { exitCode, signal } = outcome.exit;
488
+ return { ok: false, error: `Async runner exited before startup state '${expectedState}' (exit code ${exitCode ?? "unknown"}, signal ${signal ?? "none"}).`, startupDidNotProceed: true };
489
+ }
490
+ } else {
491
+ await delay;
492
+ }
444
493
  }
445
494
  const finalResult = readRunnerStartup(startupPath, expectedState, expectedToken);
446
495
  if (finalResult) return finalResult;
@@ -449,7 +498,7 @@ function waitForRunnerStartup(startupPath: string, expectedState: RunnerStartupS
449
498
 
450
499
  const writePrivateStartupControlJson = createAtomicJsonWriter({ mode: 0o600, ignoreCleanupErrorAfterSuccess: true });
451
500
 
452
- function writeRunnerStartupControl(filePath: string, payload: { action: "ack" | "proceed"; token: string }): void {
501
+ function writeRunnerStartupControl(filePath: string, payload: { action: "ack" | "confirm" | "proceed"; token: string }): void {
453
502
  // Delegate to the shared atomic JSON writer (temp file + rename, retrying
454
503
  // transient Windows EPERM/EBUSY/EACCES locks and cleaning up the temp file
455
504
  // on failure), so the startup handshake gets the same locking resilience as
@@ -467,6 +516,13 @@ function runnerIsAlive(pid: number): boolean {
467
516
  }
468
517
  }
469
518
 
519
+ function waitForStartupIntervalSync(delayMs = 20): void {
520
+ const waitUntil = Date.now() + delayMs;
521
+ while (Date.now() < waitUntil) {
522
+ // Pre-handshake failures happen before the runner can be observed asynchronously.
523
+ }
524
+ }
525
+
470
526
  function terminateRunnerBeforeProceed(pid: number): boolean {
471
527
  for (const signal of ["SIGTERM", "SIGKILL"] as const) {
472
528
  if (!runnerIsAlive(pid)) return true;
@@ -476,11 +532,88 @@ function terminateRunnerBeforeProceed(pid: number): boolean {
476
532
  if (!runnerIsAlive(pid)) return true;
477
533
  }
478
534
  const deadline = Date.now() + 1000;
479
- while (runnerIsAlive(pid) && Date.now() < deadline) waitForStartupInterval();
535
+ while (runnerIsAlive(pid) && Date.now() < deadline) waitForStartupIntervalSync();
480
536
  }
481
537
  return !runnerIsAlive(pid);
482
538
  }
483
539
 
540
+ async function terminateRunnerBeforeProceedAsync(proc: ChildProcess, processClosed: Promise<{ exitCode: number | null; signal: NodeJS.Signals | null }>): Promise<boolean> {
541
+ for (const signal of ["SIGTERM", "SIGKILL"] as const) {
542
+ if (proc.pid === undefined || !runnerIsAlive(proc.pid)) return true;
543
+ try { process.kill(proc.pid, signal); } catch {
544
+ if (proc.pid === undefined || !runnerIsAlive(proc.pid)) return true;
545
+ }
546
+ const exited = await Promise.race([processClosed.then(() => true), waitForStartupInterval(1000).then(() => false)]);
547
+ if (exited || proc.pid === undefined || !runnerIsAlive(proc.pid)) return true;
548
+ }
549
+ return proc.pid === undefined || !runnerIsAlive(proc.pid);
550
+ }
551
+
552
+ async function completeRunnerStartupHandshake(
553
+ startupPath: string,
554
+ startupAckPath: string,
555
+ startupProceedPath: string,
556
+ proc: ChildProcess,
557
+ processClosed: Promise<{ exitCode: number | null; signal: NodeJS.Signals | null }>,
558
+ getObservedProcessExit: () => { exitCode: number | null; signal: NodeJS.Signals | null } | undefined,
559
+ runnerProcessInstanceId: string,
560
+ persistStartupFailure: (message: string) => void,
561
+ ): Promise<SpawnRunnerResult> {
562
+ try {
563
+ const ready = await waitForRunnerStartup(startupPath, "ready", RUNNER_STARTUP_TIMEOUT_MS, undefined, processClosed);
564
+ if (ready.ok === false) {
565
+ persistStartupFailure(ready.error);
566
+ const terminationObserved = await terminateRunnerBeforeProceedAsync(proc, processClosed);
567
+ return { pid: proc.pid, runnerProcessInstanceId, error: ready.error, terminationObserved, startupDidNotProceed: true };
568
+ }
569
+ try { writeRunnerStartupControl(startupAckPath, { action: "ack", token: ready.token }); } catch (error) {
570
+ const message = `Failed to acknowledge async runner startup: ${error instanceof Error ? error.message : String(error)}`;
571
+ persistStartupFailure(message);
572
+ const terminationObserved = await terminateRunnerBeforeProceedAsync(proc, processClosed);
573
+ return { pid: proc.pid, runnerProcessInstanceId, error: message, terminationObserved, startupDidNotProceed: true };
574
+ }
575
+ const acknowledged = await waitForRunnerStartup(startupPath, "acknowledged", RUNNER_STARTUP_TIMEOUT_MS, ready.token, processClosed);
576
+ if (acknowledged.ok === false) {
577
+ persistStartupFailure(acknowledged.error);
578
+ const terminationObserved = await terminateRunnerBeforeProceedAsync(proc, processClosed);
579
+ return { pid: proc.pid, runnerProcessInstanceId, error: acknowledged.error, terminationObserved, startupDidNotProceed: true };
580
+ }
581
+ try { writeRunnerStartupControl(startupAckPath, { action: "confirm", token: ready.token }); } catch (error) {
582
+ const message = `Failed to confirm async runner startup: ${error instanceof Error ? error.message : String(error)}`;
583
+ persistStartupFailure(message);
584
+ const terminationObserved = await terminateRunnerBeforeProceedAsync(proc, processClosed);
585
+ return { pid: proc.pid, runnerProcessInstanceId, error: message, terminationObserved, startupDidNotProceed: true };
586
+ }
587
+ const confirmed = await waitForRunnerStartup(startupPath, "confirmed", RUNNER_STARTUP_TIMEOUT_MS, ready.token, processClosed);
588
+ if (confirmed.ok === false) {
589
+ persistStartupFailure(confirmed.error);
590
+ const terminationObserved = await terminateRunnerBeforeProceedAsync(proc, processClosed);
591
+ return { pid: proc.pid, runnerProcessInstanceId, error: confirmed.error, terminationObserved, startupDidNotProceed: true };
592
+ }
593
+ const closedAtCommit = await Promise.race([
594
+ processClosed.then((exit) => exit),
595
+ new Promise<undefined>((resolve) => setImmediate(() => resolve(undefined))),
596
+ ]);
597
+ const observedExit = closedAtCommit ?? getObservedProcessExit();
598
+ if (observedExit) {
599
+ const message = `Async runner exited after startup state 'acknowledged' before proceed (exit code ${observedExit.exitCode ?? "unknown"}, signal ${observedExit.signal ?? "none"}).`;
600
+ persistStartupFailure(message);
601
+ const terminationObserved = await terminateRunnerBeforeProceedAsync(proc, processClosed);
602
+ return { pid: proc.pid, runnerProcessInstanceId, error: message, terminationObserved, startupDidNotProceed: true };
603
+ }
604
+ try { writeRunnerStartupControl(startupProceedPath, { action: "proceed", token: ready.token }); } catch (error) {
605
+ const message = `Failed to authorize async runner startup: ${error instanceof Error ? error.message : String(error)}`;
606
+ persistStartupFailure(message);
607
+ const terminationObserved = await terminateRunnerBeforeProceedAsync(proc, processClosed);
608
+ return { pid: proc.pid, runnerProcessInstanceId, error: message, terminationObserved, startupDidNotProceed: true };
609
+ }
610
+ try { fs.rmSync(startupPath, { force: true }); } catch {}
611
+ return { pid: proc.pid, runnerProcessInstanceId };
612
+ } finally {
613
+ await new Promise<void>((resolve) => setImmediate(resolve));
614
+ }
615
+ }
616
+
484
617
  function persistPreProceedStartupFailure(asyncDir: string, runId: string, runnerProcessInstanceId: string, sessionId: string | undefined, completionOwnerId: string | undefined, message: string): void {
485
618
  const now = Date.now();
486
619
  try {
@@ -489,6 +622,8 @@ function persistPreProceedStartupFailure(asyncDir: string, runId: string, runner
489
622
  try {
490
623
  status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as Partial<AsyncStatus>;
491
624
  } catch {}
625
+ const existingProcessTerminal = status.processTerminal?.state === "observed" || status.processTerminal?.state === "unknown"
626
+ ? status.processTerminal : undefined;
492
627
  writePrivateAtomicJson(statusPath, {
493
628
  ...status,
494
629
  runId,
@@ -497,7 +632,7 @@ function persistPreProceedStartupFailure(asyncDir: string, runId: string, runner
497
632
  state: "failed",
498
633
  lastUpdate: now,
499
634
  error: message,
500
- processTerminal: {
635
+ processTerminal: existingProcessTerminal ?? {
501
636
  version: 1,
502
637
  state: "not-started",
503
638
  runId,
@@ -537,17 +672,27 @@ export function emitProcessTerminalEvent(ctx: AsyncExecutionContext, proof: unkn
537
672
  }
538
673
  }
539
674
 
540
- function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Omit<AsyncStatus, "pid" | "processTerminal">, initialStatusPath: string, onProcessTerminal?: (proof: unknown) => void, onBeforeProceed?: (runnerProcessInstanceId: string) => void, requestedCwd = cwd): SpawnRunnerResult {
675
+ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Omit<AsyncStatus, "pid" | "processTerminal">, initialStatusPath: string, onProcessTerminal?: (proof: unknown) => void, onBeforeProceed?: (runnerProcessInstanceId: string) => void, requestedCwd = cwd): SpawnRunnerResult | Promise<SpawnRunnerResult> {
541
676
  const cwdError = preflightLaunchCwd(requestedCwd, cwd);
542
677
  if (cwdError) return { error: cwdError };
543
678
 
544
679
  // The compiled host exposes its SDK only through Pi's extension loader.
545
680
  const binaryHost = resolveBunPiExecutable();
546
- const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
547
- const bootstrap = path.join(path.dirname(runner), "binary-bootstrap.ts");
681
+ const nodeExecutable = resolveNodeExecutable();
682
+ const configuredExtensions = (cfg as { steps?: Array<{ extensions?: unknown[]; subagentOnlyExtensions?: unknown[]; requiredExtensions?: unknown[] }> }).steps?.some(
683
+ (step) => (step.extensions?.length ?? 0) > 0 || (step.subagentOnlyExtensions?.length ?? 0) > 0 || (step.requiredExtensions?.length ?? 0) > 0,
684
+ ) ?? false;
685
+ // Keep provider/tool extensions in Jiti's host-alias boundary; the native preload
686
+ // only certifies the runner's imports, not a separately loaded extension graph.
687
+ const nativeRunnerSupported = supportsNativeRunner(nodeExecutable) && !configuredExtensions;
688
+ const runner = asyncRunnerSourcePath;
689
+ const runnerIsJavaScript = path.extname(runner) === ".js";
690
+ const bootstrap = path.join(path.dirname(runner), `binary-bootstrap${path.extname(fileURLToPath(import.meta.url))}`);
548
691
  if (binaryHost && !fs.existsSync(bootstrap)) return { error: `Background runner bootstrap not found: ${bootstrap}` };
549
- if (!binaryHost && !jitiCliPath) {
550
- return { error: "upstream jiti for TypeScript execution could not be found; ensure package dependencies are installed" };
692
+ if (!binaryHost && !runnerIsJavaScript && !nativeRunnerSupported && !jitiCliPath) {
693
+ return { error: sourceUnderNodeModules
694
+ ? "Background runner source is installed under node_modules, so upstream jiti is required for TypeScript execution."
695
+ : "Background runner requires enabled native TypeScript with synchronous module hooks, or installed upstream jiti for TypeScript execution." };
551
696
  }
552
697
  if (!binaryHost && !piPackageRoot) {
553
698
  return { error: `Background children require a supported standalone Pi host or the installed npm package (${PI_CODING_AGENT_PACKAGE}); neither is available.` };
@@ -564,7 +709,7 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Om
564
709
  const launchBarrierToken = hasRevivalLease ? undefined : runnerProcessInstanceId;
565
710
  const launchConfig = { ...cfg, runnerProcessInstanceId, ...(launchBarrierToken ? { launchBarrierToken } : {}) };
566
711
  writePrivateAtomicJson(cfgPath, launchConfig);
567
- const command = binaryHost ?? resolveNodeExecutable();
712
+ const command = binaryHost ?? nodeExecutable;
568
713
  const launchForStartup = launchConfig as typeof launchConfig & { asyncDir?: unknown; id?: unknown; sessionId?: unknown; completionOwnerId?: unknown; revivalLease?: unknown };
569
714
  const launchAsyncDir = typeof launchForStartup.asyncDir === "string" ? launchForStartup.asyncDir : undefined;
570
715
  const launchRunId = typeof launchForStartup.id === "string" ? launchForStartup.id : suffix;
@@ -595,27 +740,44 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Om
595
740
  : [];
596
741
  const args = binaryHost
597
742
  ? ["--no-extensions", "--no-skills", "--no-prompt-templates", "--no-session", "--mode", "rpc", "--extension", bootstrap]
598
- : [...preload, jitiCliPath!, runner, cfgPath];
743
+ : runnerIsJavaScript
744
+ ? [...preload, runner, cfgPath]
745
+ : nativeRunnerSupported
746
+ ? [...preload, "--experimental-strip-types", runner, cfgPath]
747
+ : [...preload, jitiCliPath!, runner, cfgPath];
599
748
  const proc = spawn(command, args, {
600
749
  cwd,
601
750
  ...backgroundProcessOptions(),
602
751
  stdio: ["ignore", stdoutFd ?? "ignore", stderrFd ?? "ignore"],
603
752
  env: {
604
753
  ...omitExtensionBindingsEnv(process.env),
754
+ // Unset leaves the inherited parent value in place. See childCacheRetention.
755
+ ...childCacheRetentionEnv(),
605
756
  [PI_CODING_AGENT_PACKAGE_ROOT_ENV]: binaryHost ? undefined : piPackageRoot,
606
757
  // npm must override inherited bundled layouts (#2071); binaries retain release assets.
607
758
  PI_PACKAGE_DIR: binaryHost ? process.env.PI_PACKAGE_DIR : piPackageRoot,
608
759
  [JITI_ALIAS_ENV]: binaryHost ? undefined : JSON.stringify(hostPeerAliases.aliases),
760
+ PI_ASYNC_NATIVE_RUNNER: !binaryHost && (runnerIsJavaScript || nativeRunnerSupported) ? "1" : "0",
761
+ PI_ASYNC_COMPILED_RUNNER: !binaryHost && runnerIsJavaScript ? "1" : "0",
609
762
  PI_SUBAGENT_RUNNER_CONFIG: binaryHost ? cfgPath : undefined,
610
763
  },
611
764
  });
765
+ let observedProcessExit: { exitCode: number | null; signal: NodeJS.Signals | null } | undefined;
766
+ proc.once("exit", (exitCode, signal) => { observedProcessExit = { exitCode, signal }; });
767
+ const processClosed = new Promise<{ exitCode: number | null; signal: NodeJS.Signals | null }>((resolve) => {
768
+ proc.once("close", (exitCode, signal) => {
769
+ observedProcessExit ??= { exitCode, signal };
770
+ resolve({ exitCode, signal });
771
+ });
772
+ });
612
773
  closeFd(stdoutFd);
613
774
  closeFd(stderrFd);
614
775
  proc.on("error", (error) => {
615
776
  console.error(`[pi-subagents] async spawn failed: ${error.message}`);
616
777
  });
617
778
  proc.once("close", (exitCode, signal) => {
618
- const launch = launchConfig as { asyncDir?: unknown; id?: unknown; nestedRoute?: NestedRouteInfo; nestedSelf?: { parentRunId: string; parentStepIndex?: number; depth: number; path?: Array<{ runId: string; stepIndex?: number; agent?: string }> } };
779
+ const finalize = () => {
780
+ const launch = launchConfig as { asyncDir?: unknown; id?: unknown; nestedRoute?: NestedRouteInfo; nestedSelf?: { parentRunId: string; parentStepIndex?: number; depth: number; path?: Array<{ runId: string; stepIndex?: number; agent?: string }> } };
619
781
  const asyncDir = launch.asyncDir;
620
782
  const runId = launch.id;
621
783
  if (typeof asyncDir !== "string" || typeof runId !== "string") return;
@@ -663,6 +825,9 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Om
663
825
  }
664
826
  }
665
827
  onProcessTerminal?.(persisted);
828
+ };
829
+ if (startupPath) setImmediate(finalize);
830
+ else finalize();
666
831
  });
667
832
  if (typeof proc.pid !== "number") {
668
833
  return { error: `async runner did not produce a pid for cwd: ${cwd}` };
@@ -673,6 +838,8 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Om
673
838
  pid: proc.pid,
674
839
  processTerminal: { version: 1, state: "pending", runId: initialStatus.runId, runnerProcessInstanceId },
675
840
  });
841
+ // Aggregate waits must see the launch before the runner's first status update.
842
+ updateActiveRunIndex(path.dirname(initialStatusPath), initialStatus.state, initialStatus.toolCallId);
676
843
  } catch (error) {
677
844
  const message = `Failed to persist initial async status: ${error instanceof Error ? error.message : String(error)}`;
678
845
  if (launchAsyncDir) persistPreProceedStartupFailure(launchAsyncDir, launchRunId, runnerProcessInstanceId, launchSessionId, launchCompletionOwnerId, message);
@@ -711,39 +878,7 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Om
711
878
  const persistStartupFailure = (message: string) => {
712
879
  if (launchAsyncDir) persistPreProceedStartupFailure(launchAsyncDir, launchRunId, runnerProcessInstanceId, launchSessionId, launchCompletionOwnerId, message);
713
880
  };
714
- const ready = waitForRunnerStartup(startupPath, "ready", RUNNER_STARTUP_TIMEOUT_MS);
715
- if (ready.ok === false) {
716
- persistStartupFailure(ready.error);
717
- const terminationObserved = terminateRunnerBeforeProceed(proc.pid);
718
- return { pid: proc.pid, runnerProcessInstanceId, error: ready.error, terminationObserved, startupDidNotProceed: ready.startupDidNotProceed };
719
- }
720
- try {
721
- writeRunnerStartupControl(startupAckPath, { action: "ack", token: ready.token });
722
- } catch (error) {
723
- const message = `Failed to acknowledge async runner startup: ${error instanceof Error ? error.message : String(error)}`;
724
- persistStartupFailure(message);
725
- const terminationObserved = terminateRunnerBeforeProceed(proc.pid);
726
- return { pid: proc.pid, runnerProcessInstanceId, error: message, terminationObserved, startupDidNotProceed: true };
727
- }
728
- const acknowledged = waitForRunnerStartup(startupPath, "acknowledged", RUNNER_STARTUP_TIMEOUT_MS, ready.token);
729
- if (acknowledged.ok === false) {
730
- persistStartupFailure(acknowledged.error);
731
- const terminationObserved = terminateRunnerBeforeProceed(proc.pid);
732
- return { pid: proc.pid, runnerProcessInstanceId, error: acknowledged.error, terminationObserved, startupDidNotProceed: acknowledged.startupDidNotProceed };
733
- }
734
- try {
735
- writeRunnerStartupControl(startupProceedPath, { action: "proceed", token: ready.token });
736
- } catch (error) {
737
- const message = `Failed to authorize async runner startup: ${error instanceof Error ? error.message : String(error)}`;
738
- persistStartupFailure(message);
739
- const terminationObserved = terminateRunnerBeforeProceed(proc.pid);
740
- return { pid: proc.pid, runnerProcessInstanceId, error: message, terminationObserved, startupDidNotProceed: true };
741
- }
742
- try {
743
- fs.rmSync(startupPath, { force: true });
744
- } catch {
745
- // Proceed is the commit point; handshake cleanup cannot turn a running revival into a start error.
746
- }
881
+ return completeRunnerStartupHandshake(startupPath, startupAckPath, startupProceedPath, proc, processClosed, () => observedProcessExit ?? (proc.exitCode !== null || proc.signalCode !== null ? { exitCode: proc.exitCode, signal: proc.signalCode } : undefined), runnerProcessInstanceId, persistStartupFailure);
747
882
  }
748
883
  return { pid: proc.pid, runnerProcessInstanceId };
749
884
  } catch (error) {
@@ -785,6 +920,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
785
920
  const chainSkills = params.chainSkills ?? [];
786
921
  const availableModels = params.availableModels;
787
922
  const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
923
+ const launchMachine = params.machine;
788
924
  let managedWorktreeProvider: "native" | "worktrunk" | undefined;
789
925
  try {
790
926
  if (chain.some((step) => "worktree" in step && step.worktree === true)) {
@@ -819,8 +955,6 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
819
955
  if (error instanceof ChainOutputValidationError) return { error: error.message };
820
956
  throw error;
821
957
  }
822
- const workflowGraph = buildWorkflowGraphSnapshot({ runId: id, mode: resultMode, steps: graphChain });
823
-
824
958
  const diagnosticContext = params.unknownAgentDiagnosticContext
825
959
  ?? unknownAgentDiagnosticContext(discoverAgents(path.resolve(runnerCwd), "both"));
826
960
  for (const s of chain) {
@@ -833,6 +967,8 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
833
967
  if (!agents.find((x) => x.name === agentName)) return { error: formatUnknownAgentError(agentName, diagnosticContext) };
834
968
  }
835
969
  }
970
+ const graphSteps = projectChainOutputSchemas(graphChain, agents) as ChainStep[];
971
+ const workflowGraph = buildWorkflowGraphSnapshot({ runId: id, mode: resultMode, steps: graphSteps });
836
972
 
837
973
  let progressInstructionCreated = false;
838
974
  const buildStepOverrides = (s: SequentialStep): StepOverrides => {
@@ -845,16 +981,32 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
845
981
  ...(stepSkillInput !== undefined ? { skills: stepSkillInput } : {}),
846
982
  ...(s.model !== undefined ? { model: s.model } : {}),
847
983
  ...(s.fast !== undefined ? { fast: s.fast } : {}),
984
+ ...(s.outputSchema !== undefined ? { outputSchema: s.outputSchema } : {}),
848
985
  };
849
986
  };
850
987
  const buildSeqStep = (s: SequentialStep, sessionFile?: string, behaviorCwd?: string, progressPrecreated = false, resolvedBehavior?: ResolvedStepBehavior, flatIndex?: number, parallelOutputNamespace?: { stepIndex: number; taskIndex?: number }, runFanoutPath?: string) => {
851
988
  const a = agents.find((x) => x.name === s.agent)!;
989
+ const effectiveBehavior = resolvedBehavior ?? suppressProgressForReadOnlyTask(resolveStepBehavior(a, buildStepOverrides(s), chainSkills), s.task, originalTask);
990
+ const requestedMachine = s.machine ?? launchMachine ?? a.machine;
852
991
  const externalRunner = a.runner?.type === "external-cli" || a.runner?.type === "external-job";
853
992
  const externalRunnerType = a.runner?.type;
993
+ const machineUnsupported = formatHerdrMachineRunnerUnsupported({ machine: requestedMachine, agentName: a.name, runnerType: a.runner?.type, adapter: a.runner?.type === "external-cli" ? a.runner.adapter : undefined, worktree: s.worktree });
994
+ if (machineUnsupported) throw new AsyncStartValidationError(machineUnsupported);
995
+ let machine: HerdrMachineReference | undefined;
996
+ let machineEnv: Record<string, string> | undefined;
997
+ if (requestedMachine) {
998
+ try {
999
+ const placement = resolveHerdrMachinePlacement({ machine: requestedMachine, cwd: runnerCwd, stepCwd: s.cwd ?? params.machineCwd });
1000
+ machine = placement.machine;
1001
+ machineEnv = placement.env;
1002
+ } catch (error) {
1003
+ throw new AsyncStartValidationError(error instanceof Error ? error.message : String(error));
1004
+ }
1005
+ }
854
1006
  if (externalRunner) {
855
1007
  const unsupported: string[] = [];
856
1008
  if (s.model !== undefined) unsupported.push("model override");
857
- if (s.outputSchema !== undefined) unsupported.push("structured output");
1009
+ if (effectiveBehavior.outputSchema !== undefined) unsupported.push("structured output");
858
1010
  if (s.acceptance !== undefined || params.agentContract !== undefined || s.agentContract !== undefined) unsupported.push("acceptance/agent contract");
859
1011
  if (s.toolBudget !== undefined || params.toolBudget !== undefined || a.toolBudget !== undefined || params.configToolBudget !== undefined) unsupported.push("tool budget");
860
1012
  if ((s.fast ?? params.fast ?? a.fast) === true) unsupported.push("fast mode");
@@ -885,10 +1037,11 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
885
1037
  runtimeCwd: ctx.cwd,
886
1038
  stepCwdInput: s.cwd,
887
1039
  behaviorCwd,
1040
+ ...(machine ? { machineCwd: machine.cwd } : {}),
888
1041
  chainSkills,
889
1042
  outputBaseDir,
890
1043
  parallelOutputNamespace,
891
- resolvedBehavior,
1044
+ resolvedBehavior: effectiveBehavior,
892
1045
  });
893
1046
  const { stepCwd, instructionCwd, readExistenceCwd, behavior, namespaceOutputPath, outputPath, skillNames } = launchPlan;
894
1047
  const { resolved: resolvedSkills, missing: missingSkills } = resolveSkillsWithFallback(
@@ -944,39 +1097,40 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
944
1097
  }
945
1098
  const agentContract = s.agentContract ?? params.agentContract;
946
1099
  const permissionRules = resolvePermissionRules(ctx.permissions, a.permissions);
947
- let modelCandidates: string[] = [];
1100
+ let selectedModel = model;
1101
+ let requestedModel: string | undefined;
948
1102
  if (!externalRunner) {
949
1103
  try {
950
- modelCandidates = buildModelCandidates(primaryModel, a.fallbackModels, availableModels, a.modelProvider ?? ctx.currentModelProvider, {
1104
+ const modelEvidence = resolveModelSelection(primaryModel, availableModels, a.modelProvider ?? ctx.currentModelProvider, {
951
1105
  scope: modelScopes,
952
1106
  primaryModelFromParent,
953
1107
  origin: modelOrigin,
954
- }).flatMap((candidate) => {
955
- const resolved = applyThinkingSuffix(candidate, effectiveThinking, thinkingOverride !== undefined);
956
- return resolved ? [resolved] : [];
957
1108
  });
958
- for (const candidate of modelCandidates) assertThinkingWithinCeiling({ model: candidate, configThinking: effectiveThinking, ceiling: thinkingCeiling, agent: a.name, runId: id });
1109
+ requestedModel = modelEvidence.requestedModel;
1110
+ selectedModel = applyThinkingSuffix(modelEvidence.model, effectiveThinking, thinkingOverride !== undefined);
1111
+ assertThinkingWithinCeiling({ model: selectedModel, configThinking: effectiveThinking, ceiling: thinkingCeiling, agent: a.name, runId: id });
959
1112
  } catch (error) {
960
1113
  throw new AsyncStartValidationError(error instanceof Error ? error.message : String(error));
961
1114
  }
962
1115
  }
963
- const launchRuleError = applyWatchdogLaunchRules({ cwd: stepCwd, agent: a.name, model: modelCandidates[0] ?? model, warn: (violation) => sendRuleViolationWarning(ctx.pi, violation) });
1116
+ const launchRuleError = applyWatchdogLaunchRules({ cwd: machine ? runnerCwd : stepCwd, agent: a.name, model: selectedModel, warn: (violation) => sendRuleViolationWarning(ctx.pi, violation) });
964
1117
  if (launchRuleError) throw new AsyncStartValidationError(launchRuleError);
965
1118
  const fast = s.fast ?? params.fast ?? a.fast;
966
1119
  const hostAvailableBuiltins = getHostBuiltinToolNames(ctx.pi);
1120
+ const requiredExtensions = externalRunner ? [] : ctx.childRuntime?.requiredExtensions ?? resolveRequiredChildExtensions(ctx.parentSessionId ?? ctx.currentSessionId ?? undefined);
967
1121
  const toolPlan = resolvePiLaunchToolPlan({
968
1122
  tools: a.tools,
969
1123
  excludeTools: a.excludeTools,
970
1124
  allowNestedSubagents: a.allowNestedSubagents,
971
1125
  extensions: a.extensions,
972
1126
  subagentOnlyExtensions: a.subagentOnlyExtensions,
1127
+ requiredExtensions,
973
1128
  mcpDirectTools: a.mcpDirectTools,
974
1129
  cwd: stepCwd,
975
1130
  requireReadTool: Boolean(resolvedSkills.length),
976
- structuredOutput: Boolean(s.outputSchema),
1131
+ structuredOutput: Boolean(behavior.outputSchema),
977
1132
  fast,
978
- model,
979
- modelCandidates,
1133
+ model: selectedModel,
980
1134
  capabilityCeiling: params.capabilityCeiling,
981
1135
  inheritedCapabilityCeiling: ctx.childRuntime?.capabilityCeiling,
982
1136
  agentName: a.name,
@@ -1010,21 +1164,23 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
1010
1164
  agent: s.agent,
1011
1165
  task,
1012
1166
  ...(a.runner ? { runner: a.runner } : {}),
1167
+ ...(machine ? { machine } : {}),
1168
+ ...(machineEnv ? { machineEnv } : {}),
1013
1169
  ...(params.contextForAgent ? { context: params.contextForAgent(s.agent) } : {}),
1014
1170
  ...(agentContract ? { agentContract } : {}),
1015
1171
  phase: s.phase,
1016
1172
  label: s.label,
1017
1173
  outputName: s.as,
1018
- structured: Boolean(s.outputSchema),
1174
+ structured: Boolean(behavior.outputSchema),
1019
1175
  cwd: stepCwd,
1020
- requestedCwd: s.cwd ?? stepCwd,
1021
- model,
1176
+ requestedCwd: machine ? machine.cwd : s.cwd ?? stepCwd,
1177
+ model: selectedModel,
1022
1178
  ...(contextLimit !== undefined ? { contextLimit } : {}),
1023
1179
  ...(fast !== undefined ? { fast } : {}),
1024
- thinking: resolveEffectiveThinking(model, effectiveThinking),
1180
+ thinking: resolveEffectiveThinking(selectedModel, effectiveThinking),
1025
1181
  ...(thinkingCeiling ? { thinkingCeiling } : {}),
1026
1182
  launchResolvedExtensions,
1027
- modelCandidates: externalRunner ? undefined : modelCandidates,
1183
+ ...(requestedModel ? { requestedModel } : {}),
1028
1184
  ...(primaryModelFromParent ? { skipPrimaryModelVerification: true } : {}),
1029
1185
  ...(availableModels && availableModels.length > 0 ? { modelVerificationRegistry: availableModels } : {}),
1030
1186
  ...(ctx.modelResponseAliases ? { modelResponseAliases: ctx.modelResponseAliases } : {}),
@@ -1033,6 +1189,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
1033
1189
  allowNestedSubagents: a.allowNestedSubagents,
1034
1190
  extensions: a.extensions,
1035
1191
  subagentOnlyExtensions: a.subagentOnlyExtensions,
1192
+ ...(!externalRunner ? { requiredExtensions } : {}),
1036
1193
  mcpDirectTools: a.mcpDirectTools,
1037
1194
  mutationTools: a.mutationTools,
1038
1195
  completionGuard: a.completionGuard,
@@ -1064,8 +1221,8 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
1064
1221
  acceptanceInput: s.acceptance,
1065
1222
  acceptanceRole: a.acceptanceRole,
1066
1223
  ...(s.gateOn ? { gateOn: s.gateOn } : {}),
1067
- ...(s.outputSchema ? { structuredOutputSchema: s.outputSchema } : {}),
1068
- ...(s.outputSchema ? { structuredOutput: createStructuredOutputRuntime(s.outputSchema, path.join(asyncDir, "structured-output"), { acceptanceReport: resolveAcceptanceReportMode(s.acceptance) }) } : {}),
1224
+ ...(behavior.outputSchema ? { structuredOutputSchema: behavior.outputSchema } : {}),
1225
+ ...(behavior.outputSchema ? { structuredOutput: createStructuredOutputRuntime(behavior.outputSchema, path.join(asyncDir, "structured-output"), { acceptanceReport: resolveAcceptanceReportMode(s.acceptance) }) } : {}),
1069
1226
  ...(resolvedToolBudget.budget ? { toolBudget: resolvedToolBudget.budget } : {}),
1070
1227
  ...(s.worktree ? { worktree: true } : {}),
1071
1228
  };
@@ -1109,7 +1266,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
1109
1266
  }
1110
1267
  }
1111
1268
  const staticStep = nextFlatStep();
1112
- return buildSeqStep({ ...t, agentContract: t.agentContract ?? s.agentContract, gateOn: t.gateOn ?? s.gateOn }, staticStep.sessionFile, behaviorCwd, progressPrecreated, parallelBehaviors[taskIndex], staticStep.index, { stepIndex, taskIndex }, resultMode === "parallel" ? `tasks[${taskIndex}]` : `chain[${stepIndex}].parallel[${taskIndex}]`);
1269
+ return buildSeqStep({ ...t, machine: t.machine ?? s.machine, worktree: s.worktree, agentContract: t.agentContract ?? s.agentContract, gateOn: t.gateOn ?? s.gateOn }, staticStep.sessionFile, behaviorCwd, progressPrecreated, parallelBehaviors[taskIndex], staticStep.index, { stepIndex, taskIndex }, resultMode === "parallel" ? `tasks[${taskIndex}]` : `chain[${stepIndex}].parallel[${taskIndex}]`);
1113
1270
  }),
1114
1271
  concurrency: s.concurrency,
1115
1272
  failFast: s.failFast,
@@ -1238,11 +1395,9 @@ export function executeAsyncChain(
1238
1395
  } = params;
1239
1396
  const resultMode = params.resultMode ?? "chain";
1240
1397
  const acceptanceErrors = validateExecutionAcceptance({
1241
- chain: chain.map((step) => {
1242
- if (isParallelStep(step)) return { parallel: step.parallel };
1243
- if (isDynamicParallelStep(step)) return { acceptance: step.acceptance, parallel: step.parallel };
1244
- return { acceptance: step.acceptance, outputSchema: step.outputSchema };
1245
- }),
1398
+ chain: projectChainOutputSchemas(chain, agents,
1399
+ (step, outputSchema) => ({ acceptance: step.acceptance, outputSchema }),
1400
+ (step, parallel) => Array.isArray(step.parallel) ? { parallel } : { acceptance: "acceptance" in step ? step.acceptance : undefined, parallel }),
1246
1401
  });
1247
1402
  if (acceptanceErrors.length > 0) return formatAsyncStartError(resultMode, acceptanceErrors.join(" "));
1248
1403
  const capabilityCeiling = params.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(ctx.currentSessionId);
@@ -1276,6 +1431,8 @@ export function executeAsyncChain(
1276
1431
  availableModels: params.availableModels,
1277
1432
  cwd,
1278
1433
  chainSkills: params.chainSkills,
1434
+ machine: params.machine,
1435
+ machineCwd: params.machineCwd,
1279
1436
  sessionFilesByFlatIndex,
1280
1437
  thinkingOverridesByFlatIndex,
1281
1438
  contextForAgent: params.contextForAgent,
@@ -1410,7 +1567,7 @@ export function executeAsyncChain(
1410
1567
  path.join(asyncDir, "status.json"),
1411
1568
  (proof) => emitProcessTerminalEvent(ctx, proof),
1412
1569
  (runnerProcessInstanceId) => params.activeAsyncCapacity?.markStarted(runnerProcessInstanceId),
1413
- );
1570
+ ) as SpawnRunnerResult;
1414
1571
  } catch (error) {
1415
1572
  params.activeAsyncCapacity?.rollback();
1416
1573
  const message = error instanceof Error ? error.message : String(error);
@@ -1550,7 +1707,7 @@ export function workflowAwaitedAsyncResultPath(asyncDir: string): string {
1550
1707
  export function executeAsyncSingle(
1551
1708
  id: string,
1552
1709
  params: AsyncSingleParams,
1553
- ): AsyncExecutionResult {
1710
+ ): AsyncExecutionResult | Promise<AsyncExecutionResult> {
1554
1711
  const {
1555
1712
  agent,
1556
1713
  agentConfig,
@@ -1614,6 +1771,20 @@ export function executeAsyncSingle(
1614
1771
  return formatAsyncStartError("single", error instanceof Error ? error.message : String(error));
1615
1772
  }
1616
1773
  const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
1774
+ const requestedMachine = params.machine ?? agentConfig.machine;
1775
+ const machineUnsupported = formatHerdrMachineRunnerUnsupported({ machine: requestedMachine, agentName: agentConfig.name, runnerType: agentConfig.runner?.type, adapter: agentConfig.runner?.type === "external-cli" ? agentConfig.runner.adapter : undefined, worktree: params.worktree });
1776
+ if (machineUnsupported) return formatAsyncStartError("single", machineUnsupported);
1777
+ let machine: HerdrMachineReference | undefined;
1778
+ let machineEnv: Record<string, string> | undefined;
1779
+ if (requestedMachine) {
1780
+ try {
1781
+ const placement = resolveHerdrMachinePlacement({ machine: requestedMachine, cwd: runnerCwd, stepCwd: params.machineCwd });
1782
+ machine = placement.machine;
1783
+ machineEnv = placement.env;
1784
+ } catch (error) {
1785
+ return formatAsyncStartError("single", error instanceof Error ? error.message : String(error));
1786
+ }
1787
+ }
1617
1788
  let managedWorktreeProvider: "native" | "worktrunk" | undefined;
1618
1789
  if (params.worktree === true) {
1619
1790
  try {
@@ -1627,7 +1798,7 @@ export function executeAsyncSingle(
1627
1798
  ? WORKTREE_AGENT_CWD_PLACEHOLDER
1628
1799
  : params.worktree === true && managedWorktreeProvider === "native"
1629
1800
  ? resolveExpectedWorktreeAgentCwd(runnerCwd, `${id}-s0`, 0, worktreeBaseDir)
1630
- : runnerCwd;
1801
+ : machine?.cwd ?? runnerCwd;
1631
1802
  const readExistenceCwd = params.worktree === true ? runnerCwd : instructionCwd;
1632
1803
  const skillNames = params.skills ?? agentConfig.skills ?? [];
1633
1804
  const availableModels = params.availableModels;
@@ -1670,7 +1841,11 @@ export function executeAsyncSingle(
1670
1841
  // absolute paths pass through; relative paths resolve against the child cwd.
1671
1842
  const reads = params.reads !== undefined ? params.reads : agentConfig.defaultReads ?? false;
1672
1843
  const readPaths = Array.isArray(reads)
1673
- ? managedWorktreeProvider === "worktrunk"
1844
+ ? machine
1845
+ ? externalRunner
1846
+ ? reads.map((read) => read === "~" || read.startsWith("~/") || path.posix.isAbsolute(read) ? read : path.posix.resolve(instructionCwd, read))
1847
+ : []
1848
+ : managedWorktreeProvider === "worktrunk"
1674
1849
  ? resolveExistingReadInstructionPaths(reads, instructionCwd, readExistenceCwd)
1675
1850
  : resolveExistingReadPaths(reads, readExistenceCwd)
1676
1851
  : [];
@@ -1736,36 +1911,37 @@ export function executeAsyncSingle(
1736
1911
  const structuredOutput = params.structuredOutputSchema
1737
1912
  ? createStructuredOutputRuntime(params.structuredOutputSchema, path.join(asyncDir, "structured-output"), { acceptanceReport: resolveAcceptanceReportMode(params.acceptance) })
1738
1913
  : undefined;
1739
- let modelCandidates: string[] = [];
1914
+ let selectedModel = model;
1915
+ let requestedModel: string | undefined;
1740
1916
  if (!externalRunner) {
1741
1917
  try {
1742
- modelCandidates = buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, agentConfig.modelProvider ?? ctx.currentModelProvider, {
1918
+ const modelEvidence = resolveModelSelection(primaryModel, availableModels, agentConfig.modelProvider ?? ctx.currentModelProvider, {
1743
1919
  scope: modelScopes,
1744
1920
  primaryModelFromParent: modelOrigin === "inherited",
1745
1921
  origin: modelOrigin,
1746
- }).flatMap((candidate) => {
1747
- const resolved = applyThinkingSuffix(candidate, effectiveThinking, params.thinkingOverride !== undefined);
1748
- return resolved ? [resolved] : [];
1749
1922
  });
1750
- for (const candidate of modelCandidates) assertThinkingWithinCeiling({ model: candidate, configThinking: effectiveThinking, ceiling: thinkingCeiling, agent: agentConfig.name, runId: id });
1923
+ requestedModel = modelEvidence.requestedModel;
1924
+ selectedModel = applyThinkingSuffix(modelEvidence.model, effectiveThinking, params.thinkingOverride !== undefined);
1925
+ assertThinkingWithinCeiling({ model: selectedModel, configThinking: effectiveThinking, ceiling: thinkingCeiling, agent: agentConfig.name, runId: id });
1751
1926
  } catch (error) {
1752
1927
  return formatAsyncStartError("single", error instanceof Error ? error.message : String(error));
1753
1928
  }
1754
1929
  }
1755
1930
  const hostAvailableBuiltins = getHostBuiltinToolNames(ctx.pi);
1931
+ const requiredExtensions = externalRunner ? [] : params.requiredExtensions ?? ctx.childRuntime?.requiredExtensions ?? resolveRequiredChildExtensions(ctx.parentSessionId ?? ctx.currentSessionId ?? undefined);
1756
1932
  const toolPlan = resolvePiLaunchToolPlan({
1757
1933
  tools: agentConfig.tools,
1758
1934
  excludeTools: agentConfig.excludeTools,
1759
1935
  allowNestedSubagents: agentConfig.allowNestedSubagents,
1760
1936
  extensions: agentConfig.extensions,
1761
1937
  subagentOnlyExtensions: agentConfig.subagentOnlyExtensions,
1938
+ requiredExtensions,
1762
1939
  mcpDirectTools: agentConfig.mcpDirectTools,
1763
1940
  cwd: runnerCwd,
1764
1941
  requireReadTool: Boolean(resolvedSkills.length),
1765
1942
  structuredOutput: Boolean(params.structuredOutputSchema),
1766
1943
  fast: params.fast ?? agentConfig.fast,
1767
- model,
1768
- modelCandidates,
1944
+ model: selectedModel,
1769
1945
  capabilityCeiling,
1770
1946
  inheritedCapabilityCeiling: ctx.childRuntime?.capabilityCeiling,
1771
1947
  agentName: agentConfig.name,
@@ -1789,11 +1965,11 @@ export function executeAsyncSingle(
1789
1965
  if (contractError) return formatAsyncStartError("single", contractError);
1790
1966
  }
1791
1967
  const fast = params.fast ?? agentConfig.fast;
1792
- const launchThinking = resolveEffectiveThinking(model, effectiveThinking);
1968
+ const launchThinking = resolveEffectiveThinking(selectedModel, effectiveThinking);
1793
1969
  const { definitionDigest, launchContractDigest } = resolveLaunchBinding({
1794
1970
  agent: agentConfig,
1795
1971
  task,
1796
- modelCandidates,
1972
+ model: selectedModel,
1797
1973
  ...(fast !== undefined ? { fast } : {}),
1798
1974
  ...(launchThinking ? { thinking: launchThinking } : {}),
1799
1975
  systemPrompt,
@@ -1820,6 +1996,7 @@ export function executeAsyncSingle(
1820
1996
  ...(lane ? { lane } : {}),
1821
1997
  launchContractDigest,
1822
1998
  ...(extensionBindings ? { extensionBindings } : {}),
1999
+ ...(requiredExtensions.length > 0 ? { requiredExtensions } : {}),
1823
2000
  runFanoutBudget,
1824
2001
  sourceRunId: id,
1825
2002
  ...(params.agentContract ? { agentContract: params.agentContract } : {}),
@@ -1827,13 +2004,12 @@ export function executeAsyncSingle(
1827
2004
  launchResolvedExtensions,
1828
2005
  ...(sessionFile ? { sessionFile } : {}),
1829
2006
  cwd: runnerCwd,
1830
- ...(model ? { model } : {}),
2007
+ ...(selectedModel ? { model: selectedModel } : {}),
1831
2008
  ...(params.fast ?? recoveryAgentConfig.fast ? { fast: params.fast ?? recoveryAgentConfig.fast } : {}),
1832
2009
  ...(recoveryAgentConfig.modelProvider ? { modelProvider: recoveryAgentConfig.modelProvider } : {}),
1833
2010
  ...(modelOrigin === "inherited" ? { modelOverrideFromParent: true } : {}),
1834
2011
  modelOrigin,
1835
- ...(recoveryAgentConfig.fallbackModels ? { fallbackModels: [...recoveryAgentConfig.fallbackModels] } : {}),
1836
- ...(effectiveThinking ? { thinking: resolveEffectiveThinking(model, effectiveThinking) } : {}),
2012
+ ...(effectiveThinking ? { thinking: resolveEffectiveThinking(selectedModel, effectiveThinking) } : {}),
1837
2013
  ...(thinkingCeiling ? { thinkingCeiling } : {}),
1838
2014
  ...(recoveryAgentConfig.tools ? { tools: [...recoveryAgentConfig.tools] } : {}),
1839
2015
  ...(recoveryAgentConfig.excludeTools ? { excludeTools: [...recoveryAgentConfig.excludeTools] } : {}),
@@ -1877,11 +2053,11 @@ export function executeAsyncSingle(
1877
2053
  return formatAsyncStartError("single", `Failed to persist async recovery descriptor for '${id}': ${error instanceof Error ? error.message : String(error)}`);
1878
2054
  }
1879
2055
  }
1880
- let spawnResult: SpawnRunnerResult = {};
2056
+ let spawnResultOrPromise: SpawnRunnerResult | Promise<SpawnRunnerResult> = {};
1881
2057
  const initialStatusAt = Date.now();
1882
2058
  const initialCompletionOwnerId = ctx.completionOwnerId ?? currentCompletionOwnerId();
1883
2059
  try {
1884
- spawnResult = spawnRunner(
2060
+ spawnResultOrPromise = spawnRunner(
1885
2061
  {
1886
2062
  id,
1887
2063
  steps: [
@@ -1892,16 +2068,19 @@ export function executeAsyncSingle(
1892
2068
  agent,
1893
2069
  task: taskText,
1894
2070
  ...(agentConfig.runner ? { runner: agentConfig.runner } : {}),
2071
+ ...(machine ? { machine } : {}),
2072
+ ...(!externalRunner && machine && params.reads !== undefined ? { remoteReads: params.reads } : {}),
2073
+ ...(machineEnv ? { machineEnv } : {}),
1895
2074
  ...(params.externalJobFollowUp ? { externalJobFollowUp: params.externalJobFollowUp } : {}),
1896
2075
  ...(params.context ? { context: params.context } : {}),
1897
- cwd: runnerCwd,
1898
- requestedCwd: params.requestedCwd ?? runnerCwd,
1899
- model,
2076
+ cwd: machine?.cwd ?? runnerCwd,
2077
+ requestedCwd: machine?.cwd ?? params.requestedCwd ?? runnerCwd,
2078
+ model: selectedModel,
1900
2079
  ...(contextLimit !== undefined ? { contextLimit } : {}),
1901
2080
  ...(params.fast ?? agentConfig.fast ? { fast: params.fast ?? agentConfig.fast } : {}),
1902
- thinking: resolveEffectiveThinking(model, effectiveThinking),
2081
+ thinking: resolveEffectiveThinking(selectedModel, effectiveThinking),
1903
2082
  ...(thinkingCeiling ? { thinkingCeiling } : {}),
1904
- modelCandidates,
2083
+ ...(requestedModel ? { requestedModel } : {}),
1905
2084
  ...(modelOrigin === "inherited" ? { skipPrimaryModelVerification: true } : {}),
1906
2085
  ...(availableModels && availableModels.length > 0 ? { modelVerificationRegistry: availableModels } : {}),
1907
2086
  ...(ctx.modelResponseAliases ? { modelResponseAliases: ctx.modelResponseAliases } : {}),
@@ -1910,6 +2089,7 @@ export function executeAsyncSingle(
1910
2089
  allowNestedSubagents: agentConfig.allowNestedSubagents,
1911
2090
  extensions: agentConfig.extensions,
1912
2091
  subagentOnlyExtensions: agentConfig.subagentOnlyExtensions,
2092
+ ...(!externalRunner ? { requiredExtensions } : {}),
1913
2093
  mcpDirectTools: agentConfig.mcpDirectTools,
1914
2094
  mutationTools: agentConfig.mutationTools,
1915
2095
  completionGuard: agentConfig.completionGuard,
@@ -1968,6 +2148,7 @@ export function executeAsyncSingle(
1968
2148
  timeoutMs,
1969
2149
  deadlineAt,
1970
2150
  toolTimeoutMs,
2151
+ checkpointBeforeDeadlineMs: params.checkpointBeforeDeadlineMs,
1971
2152
  toolBudget: params.toolBudget,
1972
2153
  usageBudget: params.usageBudget,
1973
2154
  controlIntercomTarget,
@@ -2015,7 +2196,7 @@ export function executeAsyncSingle(
2015
2196
  const message = error instanceof Error ? error.message : String(error);
2016
2197
  return formatAsyncStartError("single", `Failed to start async run '${id}': ${message}`);
2017
2198
  }
2018
-
2199
+ const finishSpawnResult = (spawnResult: SpawnRunnerResult): AsyncExecutionResult => {
2019
2200
  if (spawnResult.error) {
2020
2201
  if (spawnResult.startupDidNotProceed) {
2021
2202
  if (!spawnResult.runnerProcessInstanceId || params.activeAsyncCapacity?.rollbackBeforeRunnerProceed(spawnResult.runnerProcessInstanceId) !== true) params.activeAsyncCapacity?.rollback();
@@ -2092,4 +2273,6 @@ export function executeAsyncSingle(
2092
2273
  content: [{ type: "text", text: formatAsyncStartedMessage(`Async: ${agent} [${id}]`, ctx.interactive === true) }],
2093
2274
  details: { mode: "single", runId: id, results: [], asyncId: id, asyncDir, launchContractDigest, launchResolvedExtensions, ...(capabilityCeiling ? { capabilityCeiling } : {}), ...(params.context ? { context: params.context } : {}), ...(timeoutMs !== undefined ? { timeoutMs, deadlineAt } : {}), ...(params.toolBudget ? { toolBudget: resolvedToolBudget.budget ?? params.toolBudget } : {}), ...(initialUsageBudget ? { usageBudget: initialUsageBudget } : {}) } as Details,
2094
2275
  };
2276
+ };
2277
+ return spawnResultOrPromise instanceof Promise ? spawnResultOrPromise.then(finishSpawnResult) : finishSpawnResult(spawnResultOrPromise);
2095
2278
  }