pi-cohort 7.1.2 → 7.2.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.
@@ -1,11 +1,12 @@
1
1
  import { spawn, spawnSync } from "node:child_process";
2
+ import { PINNED_AGENT_DIR_OVERRIDE } from "../../shared/agent-dir.ts";
2
3
  import * as fs from "node:fs";
3
4
  import * as path from "node:path";
4
5
  import { pathToFileURL } from "node:url";
5
6
  import type { Message } from "@earendil-works/pi-ai";
6
7
  import { writeAtomicJson } from "../../shared/atomic-json.ts";
7
8
  import { appendJsonl, getArtifactPaths } from "../../shared/artifacts.ts";
8
- import { PI_CODING_AGENT_PACKAGE, getPiSpawnCommand, resolveInstalledPiPackageRoot } from "../shared/pi-spawn.ts";
9
+ import { getPiSpawnCommand, parsePiRuntime, PiRuntimeLaunchError, type PiRuntimeIdentity } from "../shared/pi-spawn.ts";
9
10
  import { captureSingleOutputSnapshot, finalizeSingleOutput, formatSavedOutputReference, resolveSingleOutput, type SingleOutputSnapshot } from "../shared/single-output.ts";
10
11
  import {
11
12
  type ActivityState,
@@ -110,8 +111,7 @@ interface SubagentRunConfig {
110
111
  sessionDir?: string;
111
112
  asyncDir: string;
112
113
  sessionId?: string | null;
113
- piPackageRoot?: string;
114
- piArgv1?: string;
114
+ runtime: PiRuntimeIdentity;
115
115
  forwardedFlags?: string[];
116
116
  worktreeSetupHook?: string;
117
117
  worktreeSetupHookTimeoutMs?: number;
@@ -244,21 +244,18 @@ function runPiStreaming(
244
244
  args: string[],
245
245
  cwd: string,
246
246
  outputFile: string,
247
- env?: Record<string, string | undefined>,
248
- piPackageRoot?: string,
249
- piArgv1?: string,
247
+ env: Record<string, string | undefined> | undefined,
248
+ runtime: PiRuntimeIdentity,
250
249
  maxSubagentDepth?: number,
251
250
  childEventContext?: ChildEventContext,
252
251
  registerInterrupt?: (interrupt: (() => void) | undefined) => void,
253
252
  onChildEvent?: (event: ChildEvent) => void,
254
253
  ): Promise<RunPiStreamingResult> {
255
- return new Promise((resolve) => {
254
+ return new Promise((resolve, reject) => {
255
+ const spawnSpec = getPiSpawnCommand(args, runtime);
256
256
  const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
257
257
  const spawnEnv = { ...process.env, ...(env ?? {}), ...getSubagentDepthEnv(maxSubagentDepth) };
258
- const spawnSpec = getPiSpawnCommand(args, {
259
- ...(piPackageRoot ? { piPackageRoot } : {}),
260
- ...(piArgv1 ? { argv1: piArgv1 } : {}),
261
- });
258
+ if (PINNED_AGENT_DIR_OVERRIDE) spawnEnv.PI_CODING_AGENT_DIR = PINNED_AGENT_DIR_OVERRIDE;
262
259
  const child = spawn(spawnSpec.command, spawnSpec.args, {
263
260
  cwd,
264
261
  stdio: ["ignore", "pipe", "pipe"],
@@ -431,6 +428,7 @@ function runPiStreaming(
431
428
  clearDrainTimers();
432
429
  });
433
430
  child.on("close", (exitCode, signal) => {
431
+ if (settled) return;
434
432
  settled = true;
435
433
  registerInterrupt?.(undefined);
436
434
  clearDrainTimers();
@@ -460,22 +458,13 @@ function runPiStreaming(
460
458
  clearDrainTimers();
461
459
  clearStdioGuard();
462
460
  outputStream.end();
463
- const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
464
- const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
465
- resolve({ stderr, exitCode: 1, messages, usage, model, error: error ?? assistantError ?? spawnErrorMessage, finalOutput, observedMutationAttempt });
461
+ reject(new PiRuntimeLaunchError(`Failed to spawn selected Pi runtime ${runtime.nodeExecPath} ${runtime.piEntryPath}: ${String(spawnError)}`));
466
462
  });
467
463
  });
468
464
  }
469
465
 
470
- function resolvePiPackageRootFallback(): string {
471
- const root = resolveInstalledPiPackageRoot();
472
- if (root) return root;
473
- throw new Error(`Could not resolve ${PI_CODING_AGENT_PACKAGE} package root`);
474
- }
475
-
476
- async function exportSessionHtml(sessionFile: string, outputDir: string, piPackageRoot?: string): Promise<string> {
477
- const pkgRoot = piPackageRoot ?? resolvePiPackageRootFallback();
478
- const exportModulePath = path.join(pkgRoot, "dist", "core", "export-html", "index.js");
466
+ async function exportSessionHtml(sessionFile: string, outputDir: string, piPackageRoot: string): Promise<string> {
467
+ const exportModulePath = path.join(piPackageRoot, "dist", "core", "export-html", "index.js");
479
468
  const moduleUrl = pathToFileURL(exportModulePath).href;
480
469
  const mod = await import(moduleUrl);
481
470
  const exportFromFile = (mod as { exportFromFile?: (inputPath: string, options?: { outputPath?: string }) => string })
@@ -587,8 +576,7 @@ export interface SingleStepContext {
587
576
  flatIndex: number;
588
577
  flatStepCount: number;
589
578
  outputFile: string;
590
- piPackageRoot?: string;
591
- piArgv1?: string;
579
+ runtime: PiRuntimeIdentity;
592
580
  forwardedFlags?: string[];
593
581
  registerInterrupt?: (interrupt: (() => void) | undefined) => void;
594
582
  nestedRoute?: NestedRouteInfo;
@@ -636,6 +624,7 @@ export async function runSingleStep(
636
624
  output: message,
637
625
  exitCode: 1,
638
626
  error: message,
627
+ sessionFile: step.sessionFile,
639
628
  };
640
629
  }
641
630
 
@@ -673,11 +662,11 @@ export async function runSingleStep(
673
662
  const attemptNotes: string[] = [];
674
663
  const eventsPath = path.join(path.dirname(ctx.outputFile), "events.jsonl");
675
664
  let finalResult: RunPiStreamingResult | undefined;
665
+ let finalSessionFile = step.sessionFile;
676
666
  let finalOutputSnapshot: SingleOutputSnapshot | undefined;
677
667
  let completionGuardTriggeredFinal = false;
678
668
  let executionSurface: ExecutionSurfaceResult | undefined;
679
669
 
680
- const asyncDir = path.dirname(ctx.outputFile);
681
670
  let externalOwner: ExternalExecution | undefined;
682
671
 
683
672
  if (backendSelection.selection.kind === "external") {
@@ -687,6 +676,7 @@ export async function runSingleStep(
687
676
  const ownerController = new AbortController();
688
677
  try {
689
678
  externalOwner = await createExternalExecution({
679
+ runtime: ctx.runtime,
690
680
  runId: ctx.id,
691
681
  childId: `${step.agent}-${ctx.flatIndex}`,
692
682
  backend: selection.backend,
@@ -695,8 +685,8 @@ export async function runSingleStep(
695
685
  signal: ownerController.signal,
696
686
  });
697
687
  } catch (startErr) {
698
- const message = safeExternalFailure("startup", startErr);
699
- return { agent: step.agent, output: message, exitCode: 1, error: message };
688
+ const message = startErr instanceof PiRuntimeLaunchError ? startErr.message : safeExternalFailure("startup", startErr);
689
+ return { agent: step.agent, output: message, exitCode: 1, error: message, sessionFile: step.sessionFile };
700
690
  }
701
691
 
702
692
  // Bridge registerInterrupt to an AbortController for the current external attempt.
@@ -718,7 +708,9 @@ export async function runSingleStep(
718
708
  // External background: baseArgs=[] (no --mode json or -p); task is the last arg.
719
709
  // Core allocates a trusted session file for every external attempt regardless of sessionEnabled.
720
710
  // Caller owns tempDir cleanup (F1): cleanupTempDir is in finally, not in the helper.
721
- const attemptSessionFile = path.join(asyncDir, `session-${ctx.flatIndex}-attempt-${index}.jsonl`);
711
+ if (!step.sessionFile) fs.mkdirSync(ctx.sessionDir!, { recursive: true });
712
+ const attemptSessionFile = step.sessionFile ?? path.join(ctx.sessionDir!, `session-${ctx.flatIndex}-attempt-${index}.jsonl`);
713
+ finalSessionFile = attemptSessionFile;
722
714
  const { args, env, tempDir } = buildPiArgs({
723
715
  baseArgs: [],
724
716
  task,
@@ -754,6 +746,7 @@ export async function runSingleStep(
754
746
  try {
755
747
  try {
756
748
  run = await runExternalBackgroundAttempt({
749
+ runtime: ctx.runtime,
757
750
  owner: externalOwner,
758
751
  attemptId: `${ctx.id}-${ctx.flatIndex}-attempt-${index}`,
759
752
  args,
@@ -764,6 +757,7 @@ export async function runSingleStep(
764
757
  onChildEvent: ctx.onChildEvent,
765
758
  });
766
759
  } catch (error) {
760
+ if (error instanceof PiRuntimeLaunchError) throw error;
767
761
  run = {
768
762
  stderr: "",
769
763
  exitCode: 1,
@@ -840,7 +834,7 @@ export async function runSingleStep(
840
834
  attemptNotes.push(formatModelAttemptNote(attempt, candidates[index + 1]));
841
835
  }
842
836
  } catch (error) {
843
- const message = safeExternalFailure("attempt", error);
837
+ const message = error instanceof PiRuntimeLaunchError ? error.message : safeExternalFailure("attempt", error);
844
838
  finalResult = {
845
839
  stderr: "",
846
840
  exitCode: 1,
@@ -854,7 +848,6 @@ export async function runSingleStep(
854
848
  ctx.registerInterrupt?.(undefined);
855
849
  }
856
850
  } else {
857
- // Native path: unchanged, uses runPiStreaming.
858
851
  for (let index = 0; index < candidates.length; index++) {
859
852
  const candidate = candidates[index];
860
853
  ctx.onAttemptStart?.({ model: candidate, thinking: resolveEffectiveThinking(candidate, step.thinking) });
@@ -898,13 +891,16 @@ export async function runSingleStep(
898
891
  step.cwd ?? ctx.cwd,
899
892
  ctx.outputFile,
900
893
  env,
901
- ctx.piPackageRoot,
902
- ctx.piArgv1,
894
+ ctx.runtime,
903
895
  step.maxSubagentDepth,
904
896
  { eventsPath, runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
905
897
  ctx.registerInterrupt,
906
898
  ctx.onChildEvent,
907
899
  );
900
+ } catch (error) {
901
+ if (!(error instanceof PiRuntimeLaunchError)) throw error;
902
+ finalResult = { stderr: "", exitCode: 1, messages: [], usage: emptyUsage(), error: error.message, finalOutput: "" };
903
+ break;
908
904
  } finally {
909
905
  cleanupTempDir(tempDir); // F1: ensure no prompt-dir leak on throw
910
906
  }
@@ -1086,7 +1082,7 @@ export async function runSingleStep(
1086
1082
  output: outputForSummary,
1087
1083
  exitCode: effectiveFinalExitCode,
1088
1084
  error: effectiveFinalError,
1089
- sessionFile: step.sessionFile,
1085
+ sessionFile: finalSessionFile ?? (sessionEnabled && sessionDir ? findLatestSessionFile(sessionDir) ?? undefined : undefined),
1090
1086
  model: finalResult?.model,
1091
1087
  attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
1092
1088
  modelAttempts,
@@ -1229,6 +1225,7 @@ function ensureParallelProgressFile(cwd: string, group: Extract<RunnerStep, { pa
1229
1225
  }
1230
1226
 
1231
1227
  async function runSubagent(config: SubagentRunConfig): Promise<void> {
1228
+ config.runtime = parsePiRuntime(config.runtime, `Async run ${config.id}`);
1232
1229
  const { id, steps, resultPath, cwd, placeholder, taskIndex, totalTasks, maxOutput, artifactsDir, artifactConfig } =
1233
1230
  config;
1234
1231
  let previousOutput = "";
@@ -1253,7 +1250,6 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1253
1250
  let activityTimer: NodeJS.Timeout | undefined;
1254
1251
  let previousCumulativeTokens: TokenUsage = { input: 0, output: 0, total: 0 };
1255
1252
  let previousCumulativeCost = 0;
1256
- let latestSessionFile: string | undefined;
1257
1253
 
1258
1254
  const parallelGroups: Array<{ start: number; count: number; stepIndex: number }> = [];
1259
1255
  const initialStatusSteps: RunnerStatusStep[] = [];
@@ -1317,6 +1313,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1317
1313
  || shareEnabled
1318
1314
  || flatSteps.some((step) => Boolean(step.sessionFile));
1319
1315
  const statusPayload: RunnerStatusPayload = {
1316
+ runtime: config.runtime,
1320
1317
  runId: id,
1321
1318
  ...(config.sessionId ? { sessionId: config.sessionId } : {}),
1322
1319
  mode: config.resultMode ?? (flatSteps.length > 1 ? "chain" : "single"),
@@ -1874,8 +1871,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1874
1871
  artifactsDir, artifactConfig, id,
1875
1872
  flatIndex: fi, flatStepCount: Math.max(statusPayload.steps.length, 1),
1876
1873
  outputFile: path.join(asyncDir, `output-${fi}.log`),
1877
- piPackageRoot: config.piPackageRoot,
1878
- piArgv1: config.piArgv1,
1874
+ runtime: config.runtime,
1879
1875
  forwardedFlags: config.forwardedFlags,
1880
1876
  executionBackendCoordinator,
1881
1877
  executionBackendConfig,
@@ -1891,6 +1887,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1891
1887
  statusPayload.steps[fi].endedAt = taskEndTime;
1892
1888
  statusPayload.steps[fi].durationMs = taskEndTime - taskStartTime;
1893
1889
  statusPayload.steps[fi].exitCode = singleResult.exitCode;
1890
+ statusPayload.steps[fi].sessionFile = singleResult.sessionFile;
1894
1891
  statusPayload.steps[fi].model = singleResult.model;
1895
1892
  statusPayload.steps[fi].thinking = resolveEffectiveThinking(singleResult.model, statusPayload.steps[fi].thinking);
1896
1893
  statusPayload.steps[fi].attemptedModels = singleResult.attemptedModels;
@@ -2111,7 +2108,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2111
2108
  }));
2112
2109
 
2113
2110
  const taskSessionDir = config.sessionDir
2114
- ? path.join(config.sessionDir, `parallel-${taskIdx}`)
2111
+ ? path.join(config.sessionDir, `parallel-${fi}`)
2115
2112
  : undefined;
2116
2113
  const { taskForRun, taskCwd } = prepareParallelTaskRun(task, cwd, worktreeSetup, taskIdx);
2117
2114
 
@@ -2122,8 +2119,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2122
2119
  artifactsDir, artifactConfig, id,
2123
2120
  flatIndex: fi, flatStepCount: flatSteps.length,
2124
2121
  outputFile: path.join(asyncDir, `output-${fi}.log`),
2125
- piPackageRoot: config.piPackageRoot,
2126
- piArgv1: config.piArgv1,
2122
+ runtime: config.runtime,
2127
2123
  forwardedFlags: config.forwardedFlags,
2128
2124
  executionBackendCoordinator,
2129
2125
  executionBackendConfig,
@@ -2134,9 +2130,6 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2134
2130
  onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking),
2135
2131
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
2136
2132
  });
2137
- if (task.sessionFile) {
2138
- latestSessionFile = task.sessionFile;
2139
- }
2140
2133
 
2141
2134
  const taskEndTime = Date.now();
2142
2135
  const taskDuration = taskEndTime - taskStartTime;
@@ -2145,6 +2138,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2145
2138
  statusPayload.steps[fi].endedAt = taskEndTime;
2146
2139
  statusPayload.steps[fi].durationMs = taskDuration;
2147
2140
  statusPayload.steps[fi].exitCode = singleResult.exitCode;
2141
+ statusPayload.steps[fi].sessionFile = singleResult.sessionFile;
2148
2142
  statusPayload.steps[fi].model = singleResult.model;
2149
2143
  statusPayload.steps[fi].thinking = resolveEffectiveThinking(singleResult.model, statusPayload.steps[fi].thinking);
2150
2144
  statusPayload.steps[fi].attemptedModels = singleResult.attemptedModels;
@@ -2186,7 +2180,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2186
2180
  for (let t = 0; t < group.parallel.length; t++) {
2187
2181
  const fi = groupStartFlatIndex + t;
2188
2182
  const sessionTokens = config.sessionDir
2189
- ? parseSessionTokens(path.join(config.sessionDir, `parallel-${t}`))
2183
+ ? parseSessionTokens(path.join(config.sessionDir, `parallel-${fi}`))
2190
2184
  : null;
2191
2185
  const taskTokens = sessionTokens ?? tokenUsageFromAttempts(parallelResults[t]?.modelAttempts);
2192
2186
  if (!taskTokens) continue;
@@ -2286,12 +2280,11 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2286
2280
  const singleResult = await runSingleStep(seqStep, {
2287
2281
  previousOutput, placeholder, cwd, sessionEnabled,
2288
2282
  outputs,
2289
- sessionDir: config.sessionDir,
2283
+ sessionDir: config.sessionDir ? path.join(config.sessionDir, `parallel-${flatIndex}`) : undefined,
2290
2284
  artifactsDir, artifactConfig, id,
2291
2285
  flatIndex, flatStepCount: flatSteps.length,
2292
2286
  outputFile: path.join(asyncDir, `output-${flatIndex}.log`),
2293
- piPackageRoot: config.piPackageRoot,
2294
- piArgv1: config.piArgv1,
2287
+ runtime: config.runtime,
2295
2288
  forwardedFlags: config.forwardedFlags,
2296
2289
  executionBackendCoordinator,
2297
2290
  executionBackendConfig,
@@ -2302,9 +2295,6 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2302
2295
  onAttemptStart: (attempt) => updateStepModel(flatIndex, attempt.model, attempt.thinking),
2303
2296
  onChildEvent: (event) => updateStepFromChildEvent(flatIndex, event),
2304
2297
  });
2305
- if (seqStep.sessionFile) {
2306
- latestSessionFile = seqStep.sessionFile;
2307
- }
2308
2298
 
2309
2299
  previousOutput = singleResult.output;
2310
2300
  results.push({
@@ -2333,26 +2323,15 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2333
2323
  }
2334
2324
  statusPayload.outputs = outputs;
2335
2325
 
2336
- const cumulativeTokens = config.sessionDir ? parseSessionTokens(config.sessionDir) : null;
2337
- let stepTokens: TokenUsage | null = cumulativeTokens
2338
- ? {
2339
- input: cumulativeTokens.input - previousCumulativeTokens.input,
2340
- output: cumulativeTokens.output - previousCumulativeTokens.output,
2341
- total: cumulativeTokens.total - previousCumulativeTokens.total,
2342
- }
2343
- : null;
2344
- if (cumulativeTokens) {
2345
- previousCumulativeTokens = cumulativeTokens;
2346
- previousCumulativeCost = cumulativeTokens.cost;
2347
- } else {
2348
- stepTokens = tokenUsageFromAttempts(singleResult.modelAttempts);
2349
- if (stepTokens) {
2350
- previousCumulativeTokens = {
2351
- input: previousCumulativeTokens.input + stepTokens.input,
2352
- output: previousCumulativeTokens.output + stepTokens.output,
2353
- total: previousCumulativeTokens.total + stepTokens.total,
2354
- };
2355
- }
2326
+ const sessionTokens = config.sessionDir ? parseSessionTokens(path.join(config.sessionDir, `parallel-${flatIndex}`)) : null;
2327
+ const stepTokens = sessionTokens ?? tokenUsageFromAttempts(singleResult.modelAttempts);
2328
+ if (stepTokens) {
2329
+ previousCumulativeTokens = {
2330
+ input: previousCumulativeTokens.input + stepTokens.input,
2331
+ output: previousCumulativeTokens.output + stepTokens.output,
2332
+ total: previousCumulativeTokens.total + stepTokens.total,
2333
+ };
2334
+ previousCumulativeCost += sessionTokens?.cost ?? 0;
2356
2335
  }
2357
2336
 
2358
2337
  const stepEndTime = Date.now();
@@ -2360,6 +2339,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2360
2339
  statusPayload.steps[flatIndex].endedAt = stepEndTime;
2361
2340
  statusPayload.steps[flatIndex].durationMs = stepEndTime - stepStartTime;
2362
2341
  statusPayload.steps[flatIndex].exitCode = singleResult.exitCode;
2342
+ statusPayload.steps[flatIndex].sessionFile = singleResult.sessionFile;
2363
2343
  statusPayload.steps[flatIndex].model = singleResult.model;
2364
2344
  statusPayload.steps[flatIndex].thinking = resolveEffectiveThinking(singleResult.model, statusPayload.steps[flatIndex].thinking);
2365
2345
  statusPayload.steps[flatIndex].attemptedModels = singleResult.attemptedModels;
@@ -2427,22 +2407,16 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2427
2407
  : resultMode === "parallel"
2428
2408
  ? `parallel:${flatSteps.map((s) => s.agent).join("+")}`
2429
2409
  : `chain:${flatSteps.map((s) => s.agent).join("->")}`;
2430
- let sessionFile: string | undefined;
2410
+ const sessionFile = results.findLast((result) => result.sessionFile)?.sessionFile;
2431
2411
  let shareUrl: string | undefined;
2432
2412
  let gistUrl: string | undefined;
2433
2413
  let shareError: string | undefined;
2434
2414
 
2435
2415
  if (shareEnabled) {
2436
- sessionFile = config.sessionDir
2437
- ? (findLatestSessionFile(config.sessionDir) ?? undefined)
2438
- : undefined;
2439
- if (!sessionFile && latestSessionFile) {
2440
- sessionFile = latestSessionFile;
2441
- }
2442
2416
  if (sessionFile) {
2443
2417
  try {
2444
2418
  const exportDir = config.sessionDir ?? path.dirname(sessionFile);
2445
- const htmlPath = await exportSessionHtml(sessionFile, exportDir, config.piPackageRoot);
2419
+ const htmlPath = await exportSessionHtml(sessionFile, exportDir, config.runtime.piPackageRoot);
2446
2420
  const share = createShareLink(htmlPath);
2447
2421
  if ("error" in share) shareError = share.error;
2448
2422
  else {
@@ -2461,7 +2435,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2461
2435
  clearInterval(activityTimer);
2462
2436
  activityTimer = undefined;
2463
2437
  }
2464
- const effectiveSessionFile = sessionFile ?? latestSessionFile;
2438
+ const effectiveSessionFile = sessionFile;
2465
2439
  const runEndedAt = Date.now();
2466
2440
  statusPayload.state = interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed";
2467
2441
  statusPayload.activityState = undefined;
@@ -2509,6 +2483,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2509
2483
 
2510
2484
  try {
2511
2485
  writeAtomicJson(resultPath, {
2486
+ runtime: config.runtime,
2512
2487
  id,
2513
2488
  agent: agentName,
2514
2489
  mode: resultMode,
@@ -32,6 +32,7 @@ import {
32
32
  } from "../../shared/settings.ts";
33
33
  import { discoverAvailableSkills, normalizeSkillInput } from "../../agents/skills.ts";
34
34
  import { runSync } from "./execution.ts";
35
+ import type { PiRuntimeIdentity } from "../shared/pi-spawn.ts";
35
36
  import { buildChainSummary } from "../../shared/formatters.ts";
36
37
  import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, resolveChildCwd } from "../../shared/utils.ts";
37
38
  import { recordRun } from "../shared/run-history.ts";
@@ -85,6 +86,7 @@ interface ChainExecutionDetailsInput {
85
86
  }
86
87
 
87
88
  interface ParallelChainRunInput {
89
+ runtime: PiRuntimeIdentity;
88
90
  step: ParallelStep;
89
91
  parallelTemplates: string[];
90
92
  parallelBehaviors: ResolvedStepBehavior[];
@@ -260,6 +262,7 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
260
262
  ? createStructuredOutputRuntime(task.outputSchema, path.join(input.chainDir, "structured-output"))
261
263
  : undefined;
262
264
  const result = await runSync(input.ctx.cwd, input.agents, task.agent, taskStr, {
265
+ runtime: input.runtime,
263
266
  cwd: taskCwd,
264
267
  signal: input.signal,
265
268
  interruptSignal: interruptController.signal,
@@ -347,6 +350,7 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
347
350
  }
348
351
 
349
352
  interface ChainExecutionParams {
353
+ runtime: PiRuntimeIdentity;
350
354
  chain: ChainStep[];
351
355
  task?: string;
352
356
  agents: AgentConfig[];
@@ -629,6 +633,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
629
633
  createParallelDirs(chainDir, stepIndex, step.parallel.length, agentNames);
630
634
 
631
635
  const parallelResults = await runParallelChainTasks({
636
+ runtime: params.runtime,
632
637
  step,
633
638
  parallelTemplates,
634
639
  parallelBehaviors,
@@ -822,6 +827,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
822
827
  progressCreated = ensureParallelProgressFile(chainDir, progressCreated, parallelBehaviors);
823
828
  createParallelDirs(chainDir, stepIndex, dynamicParallelStep.parallel.length, dynamicParallelStep.parallel.map((task) => task.agent));
824
829
  const parallelResults = await runParallelChainTasks({
830
+ runtime: params.runtime,
825
831
  step: dynamicParallelStep,
826
832
  parallelTemplates,
827
833
  parallelBehaviors,
@@ -1021,6 +1027,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
1021
1027
  ? createStructuredOutputRuntime(seqStep.outputSchema, path.join(chainDir, "structured-output"))
1022
1028
  : undefined;
1023
1029
  const r = await runSync(ctx.cwd, agents, seqStep.agent, stepTask, {
1030
+ runtime: params.runtime,
1024
1031
  cwd: resolveChildCwd(cwd ?? ctx.cwd, seqStep.cwd),
1025
1032
  signal,
1026
1033
  interruptSignal: interruptController.signal,
@@ -41,7 +41,8 @@ import {
41
41
  } from "../../shared/utils.ts";
42
42
  import { buildSkillInjection, resolveSkillsWithFallback } from "../../agents/skills.ts";
43
43
  import { blockedLine } from "../shared/completion-guard.ts";
44
- import { getPiSpawnCommand } from "../shared/pi-spawn.ts";
44
+ import { getPiSpawnCommand, PiRuntimeLaunchError } from "../shared/pi-spawn.ts";
45
+ import { PINNED_AGENT_DIR_OVERRIDE } from "../../shared/agent-dir.ts";
45
46
  import { createJsonlWriter } from "../../shared/jsonl-writer.ts";
46
47
  import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
47
48
  import { buildPiArgs, cleanupTempDir } from "../shared/pi-args.ts";
@@ -170,11 +171,11 @@ async function runSingleAttempt(
170
171
  let interruptedByControl = false;
171
172
  let pendingControlEvents: ControlEvent[] = init.pendingControlEvents;
172
173
  const controlConfig = init.controlConfig;
173
- const spawnEnv = { ...process.env, ...sharedEnv, ...getSubagentDepthEnv(options.maxSubagentDepth) };
174
+ const spawnEnv = { ...process.env, ...sharedEnv, ...getSubagentDepthEnv(options.maxSubagentDepth), ...(PINNED_AGENT_DIR_OVERRIDE ? { PI_CODING_AGENT_DIR: PINNED_AGENT_DIR_OVERRIDE } : {}) };
174
175
  let observedMutationAttempt = false;
175
176
 
176
- const exitCode = await new Promise<number>((resolve) => {
177
- const spawnSpec = getPiSpawnCommand(args);
177
+ const exitCode = await new Promise<number>((resolve, reject) => {
178
+ const spawnSpec = getPiSpawnCommand(args, options.runtime);
178
179
  const proc = spawn(spawnSpec.command, spawnSpec.args, {
179
180
  cwd: options.cwd ?? runtimeCwd,
180
181
  env: spawnEnv,
@@ -553,6 +554,7 @@ async function runSingleAttempt(
553
554
  if (!result.error) {
554
555
  result.error = error instanceof Error ? error.message : String(error);
555
556
  }
557
+ reject(new PiRuntimeLaunchError(`${options.runtime.nodeExecPath} ${options.runtime.piEntryPath}: ${error.message}`));
556
558
  finish(1);
557
559
  });
558
560
 
@@ -591,7 +593,7 @@ async function runSingleAttempt(
591
593
  removeInterruptListener = () => options.interruptSignal?.removeEventListener("abort", interrupt);
592
594
  }
593
595
  }
594
- });
596
+ }).finally(() => cleanupTempDir(tempDir));
595
597
  result.exitCode = exitCode;
596
598
  return finalizeSingleAttempt(result, progress, {
597
599
  agent,
@@ -640,6 +642,12 @@ export async function runSync(
640
642
  };
641
643
  }
642
644
 
645
+ try {
646
+ getPiSpawnCommand([], options.runtime);
647
+ } catch (error) {
648
+ return failedResult(agentName, task, error instanceof Error ? error.message : String(error));
649
+ }
650
+
643
651
  const childCwd = options.cwd ?? runtimeCwd;
644
652
  let backendSelection: ExecutionBackendSelectionResult;
645
653
  try {
@@ -717,6 +725,7 @@ export async function runSync(
717
725
  try {
718
726
  externalOwner = await (dependencies.createExternalOwner ?? createExternalExecution)({
719
727
  backend: backendSelection.selection.backend,
728
+ runtime: options.runtime,
720
729
  runId: options.runId,
721
730
  childId,
722
731
  cwd: childCwd,
@@ -724,8 +733,8 @@ export async function runSync(
724
733
  signal: options.signal ?? NEVER_ABORTED_SIGNAL,
725
734
  });
726
735
  await externalOwner.ready;
727
- } catch {
728
- lastResult = failedResult(agentName, task, safeExternalFailure("startup"));
736
+ } catch (error) {
737
+ lastResult = failedResult(agentName, task, error instanceof PiRuntimeLaunchError ? error.message : safeExternalFailure("startup"));
729
738
  }
730
739
  }
731
740
 
@@ -746,6 +755,7 @@ export async function runSync(
746
755
  originalTask: task,
747
756
  };
748
757
  let result: SingleResult;
758
+ let runtimeFailure = false;
749
759
  if (externalOwner) {
750
760
  try {
751
761
  result = await (dependencies.runExternalAttempt ?? runExternalSingleAttempt)({
@@ -758,13 +768,20 @@ export async function runSync(
758
768
  options,
759
769
  shared,
760
770
  });
761
- } catch {
762
- result = failedResult(agentName, task, safeExternalFailure("attempt"));
771
+ } catch (error) {
772
+ runtimeFailure = error instanceof PiRuntimeLaunchError;
773
+ result = failedResult(agentName, task, runtimeFailure ? (error as Error).message : safeExternalFailure("attempt"));
763
774
  }
764
775
  } else {
765
- result = await (dependencies.runNativeAttempt ?? runSingleAttempt)(
766
- runtimeCwd, effectiveAgent, taskWithAcceptance, candidate, options, shared,
767
- );
776
+ try {
777
+ result = await (dependencies.runNativeAttempt ?? runSingleAttempt)(
778
+ runtimeCwd, effectiveAgent, taskWithAcceptance, candidate, options, shared,
779
+ );
780
+ } catch (error) {
781
+ if (!(error instanceof PiRuntimeLaunchError)) throw error;
782
+ runtimeFailure = true;
783
+ result = failedResult(agentName, task, error.message);
784
+ }
768
785
  }
769
786
  lastResult = result;
770
787
  sumUsage(aggregateUsage, result.usage);
@@ -782,7 +799,7 @@ export async function runSync(
782
799
  if (attemptSucceeded) {
783
800
  break;
784
801
  }
785
- if (blockedLine(result.error ?? "") || !isRetryableModelFailure(result.error) || i === modelsToTry.length - 1) {
802
+ if (runtimeFailure || blockedLine(result.error ?? "") || !isRetryableModelFailure(result.error) || i === modelsToTry.length - 1) {
786
803
  break;
787
804
  }
788
805
  attemptNotes.push(formatModelAttemptNote(attempt, modelsToTry[i + 1]));
@@ -12,6 +12,7 @@ import type { RunSyncOptions, SingleResult, Usage } from "../../shared/types.ts"
12
12
  import { getSubagentDepthEnv } from "../../shared/types.ts";
13
13
  import { buildPiArgs, cleanupTempDir } from "../shared/pi-args.ts";
14
14
  import { getPiSpawnCommand } from "../shared/pi-spawn.ts";
15
+ import { PINNED_AGENT_DIR_OVERRIDE } from "../../shared/agent-dir.ts";
15
16
  import { isMutatingTool } from "../shared/long-running-guard.ts";
16
17
  import type { ExternalAttemptResult, ExternalExecution } from "../shared/external-execution.ts";
17
18
  import { type AttemptSharedInit, finalizeSingleAttempt, initializeAttempt } from "./attempt-finalization.ts";
@@ -104,8 +105,8 @@ export async function runExternalSingleAttempt(input: RunExternalSingleAttemptIn
104
105
  args.push("--extension", REPORTING_EXTENSION_PATH);
105
106
  if (taskArg !== undefined) args.push(taskArg);
106
107
 
107
- const spawnSpec = getPiSpawnCommand(args);
108
- const mergedEnv: Record<string, string | undefined> = { ...process.env, ...sharedEnv, ...getSubagentDepthEnv(options.maxSubagentDepth) };
108
+ const spawnSpec = getPiSpawnCommand(args, options.runtime);
109
+ const mergedEnv: Record<string, string | undefined> = { ...process.env, ...sharedEnv, ...getSubagentDepthEnv(options.maxSubagentDepth), ...(PINNED_AGENT_DIR_OVERRIDE ? { PI_CODING_AGENT_DIR: PINNED_AGENT_DIR_OVERRIDE } : {}) };
109
110
  const environment: Record<string, string> = {};
110
111
  for (const [key, value] of Object.entries(mergedEnv)) {
111
112
  if (value !== undefined) environment[key] = value;