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,4 +1,5 @@
1
- import { randomUUID } from "node:crypto";
1
+ import { randomUUID, timingSafeEqual } from "node:crypto";
2
+ import { capturePiRuntime, parsePiRuntime, type PiRuntimeIdentity } from "../shared/pi-spawn.ts";
2
3
  import * as fs from "node:fs";
3
4
  import * as path from "node:path";
4
5
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
@@ -41,7 +42,7 @@ import { finalizeSingleOutput, injectSingleOutputInstruction, normalizeTopLevelO
41
42
  import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, readStatus, resolveChildCwd } from "../../shared/utils.ts";
42
43
  import { resolveSubagentResultStatus } from "../shared/result-children.ts";
43
44
  import { buildRevivedAsyncTask, resolveAsyncResumeTarget } from "../background/async-resume.ts";
44
- import { createNestedRoute, readNestedControlResults, resolveInheritedNestedRouteFromEnv, resolveNestedAsyncDir, resolveNestedParentAddressFromEnv, updateForegroundNestedProjection, writeNestedControlRequest, writeNestedEvent, type NestedRunResolutionScope } from "../shared/nested-events.ts";
45
+ import { projectNestedEvents, nestedResultsPath, createNestedRoute, readNestedControlResults, resolveInheritedNestedRouteFromEnv, resolveNestedAsyncDir, resolveNestedParentAddressFromEnv, updateForegroundNestedProjection, writeNestedControlRequest, writeNestedEvent, type NestedRunResolutionScope } from "../shared/nested-events.ts";
45
46
  import { resolveSubagentRunId, type ResolvedSubagentRunId } from "../background/run-id-resolver.ts";
46
47
  import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
47
48
  import { inspectSubagentStatus } from "../background/run-status.ts";
@@ -132,6 +133,7 @@ export interface SubagentParamsLike {
132
133
  }
133
134
 
134
135
  interface ExecutorDeps {
136
+ captureRuntime?: () => PiRuntimeIdentity;
135
137
  pi: ExtensionAPI;
136
138
  state: SubagentState;
137
139
  config: ExtensionConfig;
@@ -145,6 +147,7 @@ interface ExecutorDeps {
145
147
  }
146
148
 
147
149
  interface ExecutionContextData {
150
+ runtime: PiRuntimeIdentity;
148
151
  params: SubagentParamsLike;
149
152
  effectiveCwd: string;
150
153
  ctx: ExtensionContext;
@@ -156,6 +159,7 @@ interface ExecutionContextData {
156
159
  sessionRoot: string;
157
160
  sessionDirForIndex: (idx?: number) => string;
158
161
  sessionFileForIndex: (idx?: number) => string | undefined;
162
+ sessionFileHintForIndex: (idx?: number) => string;
159
163
  artifactConfig: ArtifactConfig;
160
164
  artifactsDir: string;
161
165
  backgroundRequestedWhileClarifying: boolean;
@@ -210,8 +214,9 @@ function nestedResolutionScopeForExecutor(deps: ExecutorDeps): NestedRunResoluti
210
214
  };
211
215
  }
212
216
 
213
- function buildAsyncContext(ctx: ExtensionContext, deps: ExecutorDeps, cwd: string) {
217
+ function buildAsyncContext(ctx: ExtensionContext, deps: ExecutorDeps, cwd: string, runtime: PiRuntimeIdentity) {
214
218
  return {
219
+ runtime,
215
220
  pi: deps.pi,
216
221
  cwd,
217
222
  currentSessionId: deps.state.currentSessionId!,
@@ -225,7 +230,7 @@ function buildAsyncContext(ctx: ExtensionContext, deps: ExecutorDeps, cwd: strin
225
230
  function foregroundStatusResult(control: SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never): AgentToolResult<Details> {
226
231
  let nestedWarning: string | undefined;
227
232
  try {
228
- updateForegroundNestedProjection(control);
233
+ if (control.nestedRoute) control.nestedChildren = projectNestedEvents(control.nestedRoute, { readOnlyLegacy: true }).children;
229
234
  } catch (error) {
230
235
  nestedWarning = `Nested status unavailable: ${error instanceof Error ? error.message : String(error)}`;
231
236
  }
@@ -242,9 +247,11 @@ function foregroundStatusResult(control: SubagentState["foregroundControls"] ext
242
247
  return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "management", results: [] } };
243
248
  }
244
249
 
245
- function rememberForegroundRun(state: SubagentState, input: { runId: string; mode: "single" | "parallel" | "chain"; cwd: string; results: SingleResult[] }): void {
250
+ function rememberForegroundRun(state: SubagentState, input: { runtime: PiRuntimeIdentity; sessionRoot: string; runId: string; mode: "single" | "parallel" | "chain"; cwd: string; results: SingleResult[] }): void {
246
251
  state.foregroundRuns ??= new Map();
247
- state.foregroundRuns.set(input.runId, {
252
+ const rememberedRun = {
253
+ sessionRoot: input.sessionRoot,
254
+ runtime: input.runtime,
248
255
  runId: input.runId,
249
256
  mode: input.mode,
250
257
  cwd: input.cwd,
@@ -255,7 +262,8 @@ function rememberForegroundRun(state: SubagentState, input: { runId: string; mod
255
262
  status: resolveSubagentResultStatus({ exitCode: result.exitCode, interrupted: result.interrupted }),
256
263
  ...(result.sessionFile ? { sessionFile: result.sessionFile } : {}),
257
264
  })),
258
- });
265
+ };
266
+ state.foregroundRuns.set(input.runId, rememberedRun);
259
267
  while (state.foregroundRuns.size > 50) {
260
268
  const oldest = [...state.foregroundRuns.values()].sort((left, right) => left.updatedAt - right.updatedAt)[0];
261
269
  if (!oldest) break;
@@ -263,7 +271,7 @@ function rememberForegroundRun(state: SubagentState, input: { runId: string; mod
263
271
  }
264
272
  }
265
273
 
266
- function resolveForegroundResumeTarget(params: SubagentParamsLike, state: SubagentState): { runId: string; mode: "single" | "parallel" | "chain"; state: "complete"; agent: string; index: number; cwd: string; sessionFile: string } | undefined {
274
+ function resolveForegroundResumeTarget(params: SubagentParamsLike, state: SubagentState): { runId: string; mode: "single" | "parallel" | "chain"; state: "complete"; agent: string; index: number; cwd: string; sessionFile: string; runtime: PiRuntimeIdentity; sessionRoot: string } | undefined {
267
275
  const requested = (params.id ?? params.runId)?.trim();
268
276
  if (!requested || !state.foregroundRuns?.size) return undefined;
269
277
  const direct = state.foregroundRuns.get(requested);
@@ -280,7 +288,10 @@ function resolveForegroundResumeTarget(params: SubagentParamsLike, state: Subage
280
288
  if (path.extname(child.sessionFile) !== ".jsonl") throw new Error(`Foreground run '${run.runId}' child ${index} session file must be a .jsonl file: ${child.sessionFile}`);
281
289
  const sessionFile = path.resolve(child.sessionFile);
282
290
  if (!fs.existsSync(sessionFile)) throw new Error(`Foreground run '${run.runId}' child ${index} session file does not exist: ${child.sessionFile}`);
283
- return { runId: run.runId, mode: run.mode, state: "complete", agent: child.agent, index, cwd: run.cwd, sessionFile };
291
+ const runtime = parsePiRuntime(run.runtime, `Foreground run '${run.runId}'`);
292
+ const stat = fs.lstatSync(sessionFile);
293
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`Foreground run '${run.runId}' session file is not a regular file: ${sessionFile}`);
294
+ return { runId: run.runId, mode: run.mode, state: "complete", agent: child.agent, index, cwd: run.cwd, sessionFile: fs.realpathSync.native(sessionFile), runtime, sessionRoot: validateResumeSessionRoot((run as typeof run & { sessionRoot?: string }).sessionRoot ?? path.dirname(fs.realpathSync.native(sessionFile)), sessionFile, `Foreground run '${run.runId}'`) };
284
295
  }
285
296
 
286
297
  type AsyncResumeSourceTarget = ReturnType<typeof resolveAsyncResumeTarget> & { source: "async" };
@@ -288,6 +299,8 @@ type ForegroundResumeSourceTarget = NonNullable<ReturnType<typeof resolveForegro
288
299
  type NestedResumeSourceTarget = {
289
300
  kind: "revive";
290
301
  source: "nested";
302
+ runtime: PiRuntimeIdentity;
303
+ sessionRoot: string;
291
304
  runId: string;
292
305
  state: "complete" | "failed" | "paused";
293
306
  agent: string;
@@ -437,7 +450,7 @@ function pathWithin(base: string, candidate: string): boolean {
437
450
  return resolvedCandidate === resolvedBase || resolvedCandidate.startsWith(`${resolvedBase}${path.sep}`);
438
451
  }
439
452
 
440
- function validateNestedSessionFile(run: NestedRunSummary, trustedSessionRoots: string[]): string {
453
+ function validateNestedSessionFile(run: NestedRunSummary, trustedSessionRoots: string[], route: NestedRouteInfo): string {
441
454
  const sessionFile = nestedRunSessionFile(run);
442
455
  if (!sessionFile) throw new Error(`Nested run '${run.id}' does not have a persisted session file to resume from.`);
443
456
  if (path.extname(sessionFile) !== ".jsonl") throw new Error(`Nested run '${run.id}' session file must be a .jsonl file: ${sessionFile}`);
@@ -447,6 +460,15 @@ function validateNestedSessionFile(run: NestedRunSummary, trustedSessionRoots: s
447
460
  const stat = fs.lstatSync(resolved);
448
461
  if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`Nested run '${run.id}' session file is not a regular file: ${sessionFile}`);
449
462
  const realSessionFile = fs.realpathSync(resolved);
463
+ if (run.runtime !== undefined || run.sessionRoot !== undefined) {
464
+ if (!run.sessionRoot || !path.isAbsolute(run.sessionRoot)) throw new Error(`Nested run '${run.id}' has no absolute original session root.`);
465
+ const root = fs.realpathSync.native(run.sessionRoot);
466
+ if (root !== run.sessionRoot || !fs.statSync(root).isDirectory() || !pathWithin(root, realSessionFile)) throw new Error(`Nested run '${run.id}' session file is outside its stamped session root.`);
467
+ const recorded = Buffer.from(run.capabilityToken ?? "");
468
+ const expected = Buffer.from(route.capabilityToken);
469
+ if (!recorded.length || recorded.length !== expected.length || !timingSafeEqual(recorded, expected)) throw new Error(`Nested run '${run.id}' capability does not match its route.`);
470
+ return realSessionFile;
471
+ }
450
472
  const trustedRoots = trustedSessionRoots
451
473
  .filter((root) => fs.existsSync(root))
452
474
  .map((root) => fs.realpathSync(root));
@@ -459,22 +481,33 @@ function validateNestedSessionFile(run: NestedRunSummary, trustedSessionRoots: s
459
481
  return realSessionFile;
460
482
  }
461
483
 
462
- function resolveNestedResumeTarget(match: ResolvedSubagentRunId & { kind: "nested" }, trustedSessionRoots: string[]): NestedResumeSourceTarget {
484
+ function resolveNestedResumeTarget(match: ResolvedSubagentRunId & { kind: "nested" }, trustedSessionRoots: string[], index: SubagentParamsLike["index"]): NestedResumeSourceTarget {
463
485
  const run = match.match.run;
464
- if (run.state === "running" || run.state === "queued") throw new Error(`Nested run '${run.id}' is live; route the follow-up to the owner process instead.`);
465
486
  const agent = nestedRunAgent(run);
466
487
  if (!agent) throw new Error(`Could not determine child agent for nested run '${run.id}'.`);
467
488
  const state = run.state === "complete" || run.state === "failed" || run.state === "paused" ? run.state : "failed";
468
489
  const asyncDir = resolveNestedAsyncDir(match.match.rootRunId, run);
490
+ const authoritative = asyncDir ? resolveAsyncResumeTarget({ dir: asyncDir, id: run.id, index }, {
491
+ asyncDirRoot: path.dirname(asyncDir),
492
+ resultsDir: path.dirname(nestedResultsPath(match.match.rootRunId, run.id)),
493
+ }) : undefined;
494
+ if (!authoritative && (run.state === "running" || run.state === "queued")) throw new Error(`Nested run '${run.id}' is live; route the follow-up to the owner process instead.`);
495
+ const sessionFile = validateNestedSessionFile(
496
+ authoritative ? { ...run, sessionFile: authoritative.sessionFile } : run,
497
+ trustedSessionRoots,
498
+ match.match.route,
499
+ );
469
500
  return {
470
501
  kind: "revive",
471
502
  source: "nested",
472
503
  runId: run.id,
473
- state,
474
- agent,
475
- index: 0,
476
- cwd: asyncDir ? path.dirname(asyncDir) : undefined,
477
- sessionFile: validateNestedSessionFile(run, trustedSessionRoots),
504
+ runtime: authoritative?.runtime ?? parsePiRuntime(run.runtime, `Nested run '${run.id}'`),
505
+ sessionRoot: run.sessionRoot!,
506
+ state: authoritative?.state ?? state,
507
+ agent: authoritative?.agent ?? agent,
508
+ index: authoritative?.index ?? 0,
509
+ cwd: authoritative?.cwd ?? (asyncDir ? path.dirname(asyncDir) : undefined),
510
+ sessionFile,
478
511
  };
479
512
  }
480
513
 
@@ -535,6 +568,14 @@ async function resumeLiveNestedRun(input: { target: ResolvedSubagentRunId & { ki
535
568
  return { content: [{ type: "text", text: `Nested run ${run.id} appears live but its owner route is not reachable. Wait for completion, then retry action='resume'.` }], isError: true, details: { mode: "management", results: [] } };
536
569
  }
537
570
 
571
+ function validateResumeSessionRoot(value: unknown, sessionFile: string | undefined, source: string): string {
572
+ if (typeof value !== "string" || !path.isAbsolute(value)) throw new Error(`${source}: original session root was not recorded; a new run is required`);
573
+ const root = fs.realpathSync.native(value);
574
+ if (root !== value || !fs.statSync(root).isDirectory()) throw new Error(`${source}: original session root must be a canonical directory`);
575
+ if (sessionFile && !pathWithin(root, fs.realpathSync.native(sessionFile))) throw new Error(`${source}: session file is outside its original session root`);
576
+ return root;
577
+ }
578
+
538
579
  async function resumeAsyncRun(input: {
539
580
  params: SubagentParamsLike;
540
581
  requestCwd: string;
@@ -556,14 +597,14 @@ async function resumeAsyncRun(input: {
556
597
  const requestedId = input.params.id ?? input.params.runId;
557
598
  const resolved = requestedId ? resolveSubagentRunId(requestedId, { state: input.deps.state, nested: nestedResolutionScopeForExecutor(input.deps) }) : undefined;
558
599
  if (resolved?.kind === "nested") {
559
- if (resolved.match.run.state === "running" || resolved.match.run.state === "queued") {
600
+ if (!resolveNestedAsyncDir(resolved.match.rootRunId, resolved.match.run) && (resolved.match.run.state === "running" || resolved.match.run.state === "queued")) {
560
601
  return resumeLiveNestedRun({ target: resolved, message: followUp });
561
602
  }
562
603
  const trustedSessionRoots = [
563
604
  ...(input.deps.config.defaultSessionDir ? [path.resolve(input.deps.expandTilde(input.deps.config.defaultSessionDir))] : []),
564
605
  ...(parentSessionFile ? [input.deps.getSubagentSessionRoot(parentSessionFile)] : []),
565
606
  ];
566
- target = resolveNestedResumeTarget(resolved, trustedSessionRoots);
607
+ target = resolveNestedResumeTarget(resolved, trustedSessionRoots, input.params.index);
567
608
  } else {
568
609
  target = resolveResumeTarget(input.params, input.deps.state);
569
610
  }
@@ -596,20 +637,29 @@ async function resumeAsyncRun(input: {
596
637
  };
597
638
  }
598
639
 
599
- const runId = randomUUID().slice(0, 8);
640
+ const runId = randomUUID();
641
+ const recordedRoot = target.sessionRoot ?? (target.source === "async" && target.sessionFile
642
+ ? path.dirname(fs.realpathSync.native(target.sessionFile))
643
+ : undefined);
644
+ const originalRoot = validateResumeSessionRoot(recordedRoot, target.sessionFile, `Run '${target.runId}'`);
600
645
  const artifactConfig: ArtifactConfig = { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false };
601
646
  const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo);
602
647
  const result = executeAsyncSingle(runId, {
603
648
  agent: target.agent,
604
649
  task: buildRevivedAsyncTask(target, followUp),
605
650
  agentConfig,
606
- ctx: buildAsyncContext(input.ctx, input.deps, input.requestCwd),
651
+ ctx: buildAsyncContext(input.ctx, input.deps, input.requestCwd, target.runtime),
607
652
  cwd: effectiveCwd,
608
653
  maxOutput: input.params.maxOutput,
609
654
  artifactsDir: input.deps.tempArtifactsDir,
610
655
  artifactConfig,
611
656
  shareEnabled: input.params.share === true,
612
- sessionRoot: input.deps.getSubagentSessionRoot(parentSessionFile),
657
+ sessionRoot: input.params.sessionDir
658
+ ? path.resolve(input.deps.expandTilde(input.params.sessionDir))
659
+ : input.deps.config.defaultSessionDir
660
+ ? path.resolve(input.deps.expandTilde(input.deps.config.defaultSessionDir))
661
+ : input.deps.getSubagentSessionRoot(parentSessionFile),
662
+ originSessionRoot: originalRoot,
613
663
  sessionFile: target.sessionFile,
614
664
  maxSubagentDepth: resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth),
615
665
  worktreeSetupHook: input.deps.config.worktreeSetupHook,
@@ -891,10 +941,7 @@ function collectChainSessionFiles(
891
941
  }
892
942
  continue;
893
943
  }
894
- if (isDynamicParallelStep(step)) {
895
- sessionFiles.push(undefined);
896
- continue;
897
- }
944
+ if (isDynamicParallelStep(step)) continue;
898
945
  sessionFiles.push(sessionFileForIndex(flatIndex));
899
946
  flatIndex++;
900
947
  }
@@ -972,15 +1019,15 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
972
1019
  }
973
1020
  }
974
1021
 
975
- if (!isAsyncAvailable()) {
1022
+ if (!isAsyncAvailable(data.runtime)) {
976
1023
  return {
977
- content: [{ type: "text", text: "Async mode requires upstream jiti for TypeScript execution but it could not be found. Ensure the pi-cohort package dependencies are installed." }],
1024
+ content: [{ type: "text", text: `Cannot resolve jiti from selected Pi installation ${data.runtime.piPackageRoot}` }],
978
1025
  isError: true,
979
1026
  details: { mode: "single" as const, results: [] },
980
1027
  };
981
1028
  }
982
1029
  const id = randomUUID();
983
- const asyncCtx = buildAsyncContext(ctx, deps, ctx.cwd);
1030
+ const asyncCtx = buildAsyncContext(ctx, deps, ctx.cwd, data.runtime);
984
1031
  const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo);
985
1032
  const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
986
1033
  const currentProvider = ctx.model?.provider;
@@ -1020,6 +1067,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1020
1067
  shareEnabled,
1021
1068
  sessionRoot,
1022
1069
  chainSkills: [],
1070
+ originSessionRoot: params.context === "fork" ? sessionRoot : undefined,
1023
1071
  sessionFilesByFlatIndex: params.tasks.map((_, index) => sessionFileForIndex(index)),
1024
1072
  maxSubagentDepth: currentMaxSubagentDepth,
1025
1073
  worktreeSetupHook: deps.config.worktreeSetupHook,
@@ -1047,6 +1095,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1047
1095
  shareEnabled,
1048
1096
  sessionRoot,
1049
1097
  chainSkills,
1098
+ originSessionRoot: params.context === "fork" ? sessionRoot : undefined,
1050
1099
  sessionFilesByFlatIndex: collectChainSessionFiles(chain, sessionFileForIndex),
1051
1100
  dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
1052
1101
  maxSubagentDepth: currentMaxSubagentDepth,
@@ -1085,6 +1134,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1085
1134
  artifactConfig,
1086
1135
  shareEnabled,
1087
1136
  sessionRoot,
1137
+ originSessionRoot: params.context === "fork" ? sessionRoot : undefined,
1088
1138
  sessionFile: sessionFileForIndex(0),
1089
1139
  skills,
1090
1140
  output: effectiveOutput,
@@ -1127,6 +1177,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1127
1177
  const chain = wrapChainTasksForFork(params.chain as ChainStep[], params.context);
1128
1178
  const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
1129
1179
  const chainResult = await executeChain({
1180
+ runtime: data.runtime,
1130
1181
  chain,
1131
1182
  task: params.task,
1132
1183
  agents,
@@ -1136,7 +1187,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1136
1187
  cwd: effectiveCwd,
1137
1188
  shareEnabled,
1138
1189
  sessionDirForIndex,
1139
- sessionFileForIndex,
1190
+ sessionFileForIndex: data.sessionFileHintForIndex,
1140
1191
  artifactsDir,
1141
1192
  artifactConfig,
1142
1193
  includeProgress: params.includeProgress,
@@ -1156,15 +1207,15 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1156
1207
  });
1157
1208
 
1158
1209
  if (chainResult.requestedAsync) {
1159
- if (!isAsyncAvailable()) {
1210
+ if (!isAsyncAvailable(data.runtime)) {
1160
1211
  return {
1161
- content: [{ type: "text", text: "Background mode requires upstream jiti for TypeScript execution but it could not be found. Ensure the pi-cohort package dependencies are installed." }],
1212
+ content: [{ type: "text", text: `Cannot resolve jiti from selected Pi installation ${data.runtime.piPackageRoot}` }],
1162
1213
  isError: true,
1163
1214
  details: { mode: "chain" as const, results: [] },
1164
1215
  };
1165
1216
  }
1166
1217
  const id = randomUUID();
1167
- const asyncCtx = buildAsyncContext(ctx, deps, ctx.cwd);
1218
+ const asyncCtx = buildAsyncContext(ctx, deps, ctx.cwd, data.runtime);
1168
1219
  const asyncChain = wrapChainTasksForFork(chainResult.requestedAsync.chain, params.context);
1169
1220
  return executeAsyncChain(id, {
1170
1221
  chain: asyncChain,
@@ -1179,6 +1230,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1179
1230
  shareEnabled,
1180
1231
  sessionRoot,
1181
1232
  chainSkills: chainResult.requestedAsync.chainSkills,
1233
+ originSessionRoot: params.context === "fork" ? sessionRoot : undefined,
1182
1234
  sessionFilesByFlatIndex: collectChainSessionFiles(asyncChain, sessionFileForIndex),
1183
1235
  dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
1184
1236
  maxSubagentDepth: currentMaxSubagentDepth,
@@ -1192,7 +1244,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1192
1244
 
1193
1245
  const chainDetails = chainResult.details ? compactForegroundDetails({ ...chainResult.details, runId }) : undefined;
1194
1246
  if (foregroundControl) updateForegroundNestedProjection(foregroundControl);
1195
- if (chainDetails) rememberForegroundRun(deps.state, { runId, mode: "chain", cwd: effectiveCwd, results: chainDetails.results });
1247
+ if (chainDetails) rememberForegroundRun(deps.state, { runtime: data.runtime, sessionRoot: data.sessionRoot, runId, mode: "chain", cwd: effectiveCwd, results: chainDetails.results });
1196
1248
  recordSyncCost(deps.state.grandTotal, runId, (chainDetails?.results.reduce((sum, r) => sum + r.usage.cost, 0) ?? 0) + sumNestedCost(foregroundControl?.nestedChildren));
1197
1249
  renderGrandTotal(deps.state);
1198
1250
 
@@ -1200,6 +1252,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1200
1252
  }
1201
1253
 
1202
1254
  interface ForegroundParallelRunInput {
1255
+ runtime: PiRuntimeIdentity;
1203
1256
  tasks: TaskParam[];
1204
1257
  taskTexts: string[];
1205
1258
  agents: AgentConfig[];
@@ -1405,6 +1458,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
1405
1458
  }
1406
1459
  const agentConfig = input.agents.find((agent) => agent.name === task.agent);
1407
1460
  return runSync(input.ctx.cwd, input.agents, task.agent, taskText, {
1461
+ runtime: input.runtime,
1408
1462
  cwd: taskCwd,
1409
1463
  signal: input.signal,
1410
1464
  interruptSignal: interruptController.signal,
@@ -1593,15 +1647,15 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1593
1647
  }
1594
1648
 
1595
1649
  if (result.runInBackground) {
1596
- if (!isAsyncAvailable()) {
1650
+ if (!isAsyncAvailable(data.runtime)) {
1597
1651
  return {
1598
- content: [{ type: "text", text: "Background mode requires upstream jiti for TypeScript execution but it could not be found. Ensure the pi-cohort package dependencies are installed." }],
1652
+ content: [{ type: "text", text: `Cannot resolve jiti from selected Pi installation ${data.runtime.piPackageRoot}` }],
1599
1653
  isError: true,
1600
1654
  details: { mode: "parallel" as const, results: [] },
1601
1655
  };
1602
1656
  }
1603
1657
  const id = randomUUID();
1604
- const asyncCtx = buildAsyncContext(ctx, deps, ctx.cwd);
1658
+ const asyncCtx = buildAsyncContext(ctx, deps, ctx.cwd, data.runtime);
1605
1659
  const parallelTasks = tasks.map((t, i) => {
1606
1660
  const taskText = params.context === "fork" ? wrapForkTask(taskTexts[i]!) : taskTexts[i]!;
1607
1661
  const progress = taskDisallowsFileUpdates(taskText) ? false : behaviorOverrides[i]?.progress;
@@ -1631,6 +1685,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1631
1685
  shareEnabled,
1632
1686
  sessionRoot,
1633
1687
  chainSkills: [],
1688
+ originSessionRoot: params.context === "fork" ? sessionRoot : undefined,
1634
1689
  sessionFilesByFlatIndex: tasks.map((_, index) => sessionFileForIndex(index)),
1635
1690
  maxSubagentDepth: currentMaxSubagentDepth,
1636
1691
  worktreeSetupHook: deps.config.worktreeSetupHook,
@@ -1702,6 +1757,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1702
1757
  : undefined;
1703
1758
 
1704
1759
  const results = await runForegroundParallelTasks({
1760
+ runtime: data.runtime,
1705
1761
  tasks,
1706
1762
  taskTexts,
1707
1763
  agents,
@@ -1709,7 +1765,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1709
1765
  signal,
1710
1766
  runId,
1711
1767
  sessionDirForIndex,
1712
- sessionFileForIndex,
1768
+ sessionFileForIndex: data.sessionFileHintForIndex,
1713
1769
  shareEnabled,
1714
1770
  artifactConfig,
1715
1771
  artifactsDir,
@@ -1755,7 +1811,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1755
1811
  progress: params.includeProgress ? allProgress : undefined,
1756
1812
  artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
1757
1813
  });
1758
- rememberForegroundRun(deps.state, { runId, mode: "parallel", cwd: effectiveCwd, results: details.results });
1814
+ rememberForegroundRun(deps.state, { runtime: data.runtime, sessionRoot: data.sessionRoot, runId, mode: "parallel", cwd: effectiveCwd, results: details.results });
1759
1815
  if (interrupted) {
1760
1816
  return {
1761
1817
  content: [{ type: "text", text: withWorktreeSuffix(`Parallel run paused after interrupt (${interrupted.agent}). Waiting for explicit next action.`) }],
@@ -1865,15 +1921,15 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1865
1921
  if (override?.skills !== undefined) skillOverride = override.skills;
1866
1922
 
1867
1923
  if (result.runInBackground) {
1868
- if (!isAsyncAvailable()) {
1924
+ if (!isAsyncAvailable(data.runtime)) {
1869
1925
  return {
1870
- content: [{ type: "text", text: "Background mode requires upstream jiti for TypeScript execution but it could not be found. Ensure the pi-cohort package dependencies are installed." }],
1926
+ content: [{ type: "text", text: `Cannot resolve jiti from selected Pi installation ${data.runtime.piPackageRoot}` }],
1871
1927
  isError: true,
1872
1928
  details: { mode: "single" as const, results: [] },
1873
1929
  };
1874
1930
  }
1875
1931
  const id = randomUUID();
1876
- const asyncCtx = buildAsyncContext(ctx, deps, ctx.cwd);
1932
+ const asyncCtx = buildAsyncContext(ctx, deps, ctx.cwd, data.runtime);
1877
1933
  return executeAsyncSingle(id, {
1878
1934
  agent: params.agent!,
1879
1935
  task: params.context === "fork" ? wrapForkTask(task) : task,
@@ -1886,6 +1942,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1886
1942
  artifactConfig,
1887
1943
  shareEnabled,
1888
1944
  sessionRoot,
1945
+ originSessionRoot: params.context === "fork" ? sessionRoot : undefined,
1889
1946
  sessionFile: sessionFileForIndex(0),
1890
1947
  skills: skillOverride === false ? [] : skillOverride,
1891
1948
  output: effectiveOutput,
@@ -1956,12 +2013,13 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1956
2013
  : undefined;
1957
2014
 
1958
2015
  const r = await runSync(ctx.cwd, agents, params.agent!, task, {
2016
+ runtime: data.runtime,
1959
2017
  cwd: effectiveCwd,
1960
2018
  signal,
1961
2019
  interruptSignal: interruptController.signal,
1962
2020
  runId,
1963
2021
  sessionDir: sessionDirForIndex(0),
1964
- sessionFile: sessionFileForIndex(0),
2022
+ sessionFile: data.sessionFileHintForIndex(0),
1965
2023
  share: shareEnabled,
1966
2024
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
1967
2025
  artifactConfig,
@@ -2024,7 +2082,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2024
2082
  artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
2025
2083
  truncation: r.truncation,
2026
2084
  });
2027
- rememberForegroundRun(deps.state, { runId, mode: "single", cwd: effectiveCwd, results: details.results });
2085
+ rememberForegroundRun(deps.state, { runtime: data.runtime, sessionRoot: data.sessionRoot, runId, mode: "single", cwd: effectiveCwd, results: details.results });
2028
2086
 
2029
2087
  if (!r.interrupted) {
2030
2088
  if (foregroundControl) updateForegroundNestedProjection(foregroundControl);
@@ -2059,6 +2117,13 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2059
2117
  ctx: ExtensionContext,
2060
2118
  ) => Promise<AgentToolResult<Details>>;
2061
2119
  } {
2120
+ let runtime: PiRuntimeIdentity | undefined;
2121
+ let runtimeError: unknown;
2122
+ try {
2123
+ runtime = (deps.captureRuntime ?? capturePiRuntime)();
2124
+ } catch (error) {
2125
+ runtimeError = error;
2126
+ }
2062
2127
  const execute = async (
2063
2128
  _id: string,
2064
2129
  params: SubagentParamsLike,
@@ -2091,6 +2156,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2091
2156
  config: deps.config,
2092
2157
  state: deps.state,
2093
2158
  requestedSessionDir: paramsWithResolvedCwd.sessionDir,
2159
+ runtime,
2160
+ runtimeError: runtimeError instanceof Error ? runtimeError.message : runtimeError === undefined ? undefined : String(runtimeError),
2094
2161
  currentSessionFile,
2095
2162
  currentSessionId,
2096
2163
  sessionError,
@@ -2105,7 +2172,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2105
2172
  if (targetRunId) {
2106
2173
  try {
2107
2174
  const nestedScope = nestedResolutionScopeForExecutor(deps);
2108
- const resolved = resolveSubagentRunId(targetRunId, { state: deps.state, nested: nestedScope });
2175
+ const resolved = resolveSubagentRunId(targetRunId, { state: deps.state, nested: nestedScope, readOnlyLegacy: true });
2109
2176
  if (resolved?.kind === "foreground") {
2110
2177
  const foreground = getForegroundControl(deps.state, resolved.id);
2111
2178
  if (foreground) return foregroundStatusResult(foreground);
@@ -2121,7 +2188,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2121
2188
  return inspectSubagentStatus(paramsWithResolvedCwd, { state: deps.state, nested: nestedResolutionScopeForExecutor(deps) });
2122
2189
  }
2123
2190
  if (params.action === "resume") {
2124
- return resumeAsyncRun({ params: paramsWithResolvedCwd, requestCwd, ctx, deps });
2191
+ try {
2192
+ return await resumeAsyncRun({ params: paramsWithResolvedCwd, requestCwd, ctx, deps });
2193
+ } catch (error) {
2194
+ return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true, details: { mode: "management", results: [] } };
2195
+ }
2125
2196
  }
2126
2197
  if (params.action === "interrupt") {
2127
2198
  const targetRunId = paramsWithResolvedCwd.runId ?? paramsWithResolvedCwd.id;
@@ -2177,6 +2248,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2177
2248
  return handleManagementAction(params.action, paramsWithResolvedCwd, { ...ctx, cwd: requestCwd });
2178
2249
  }
2179
2250
 
2251
+ if (!runtime) return toExecutionErrorResult(paramsWithResolvedCwd, runtimeError ?? new Error("Parent runtime capture failed."));
2252
+
2180
2253
  const { blocked, depth, maxDepth } = checkSubagentDepth(deps.config.maxSubagentDepth);
2181
2254
  if (blocked) {
2182
2255
  return {
@@ -2207,7 +2280,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2207
2280
  const scope: AgentScope = resolveExecutionAgentScope(effectiveParams.agentScope);
2208
2281
  const effectiveCwd = effectiveParams.cwd ?? ctx.cwd;
2209
2282
  const parentSessionFile = ctx.sessionManager.getSessionFile() ?? null;
2210
- const runId = randomUUID().slice(0, 8);
2283
+ const runId = randomUUID();
2211
2284
  const inheritedNestedRoute = resolveInheritedNestedRouteFromEnv();
2212
2285
  const nestedParentAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
2213
2286
  const nestedRoute = inheritedNestedRoute ?? createNestedRoute(runId);
@@ -2228,12 +2301,6 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2228
2301
  );
2229
2302
  if (validationError) return validationError;
2230
2303
 
2231
- let sessionFileForIndex: (idx?: number) => string | undefined = () => undefined;
2232
- try {
2233
- sessionFileForIndex = createForkContextResolver(ctx.sessionManager, effectiveParams.context).sessionFileForIndex;
2234
- } catch (error) {
2235
- return toExecutionErrorResult(effectiveParams, error);
2236
- }
2237
2304
  const requestedAsync = effectiveParams.async ?? deps.asyncByDefault;
2238
2305
  const backgroundRequestedWhileClarifying = (hasChain || hasTasks) && requestedAsync && effectiveParams.clarify === true;
2239
2306
  const effectiveAsync = requestedAsync && effectiveParams.clarify !== true;
@@ -2246,17 +2313,16 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2246
2313
  };
2247
2314
  const artifactsDir = effectiveAsync ? deps.tempArtifactsDir : getArtifactsDir(parentSessionFile);
2248
2315
 
2249
- let sessionRoot: string;
2250
- if (effectiveParams.sessionDir) {
2251
- sessionRoot = path.resolve(deps.expandTilde(effectiveParams.sessionDir));
2252
- } else {
2253
- const baseSessionRoot = deps.config.defaultSessionDir
2316
+ const baseSessionRoot = effectiveParams.sessionDir
2317
+ ? path.resolve(deps.expandTilde(effectiveParams.sessionDir))
2318
+ : deps.config.defaultSessionDir
2254
2319
  ? path.resolve(deps.expandTilde(deps.config.defaultSessionDir))
2255
2320
  : deps.getSubagentSessionRoot(parentSessionFile);
2256
- sessionRoot = path.join(baseSessionRoot, runId);
2257
- }
2321
+ let sessionRoot = path.join(baseSessionRoot, runId);
2258
2322
  try {
2259
- fs.mkdirSync(sessionRoot, { recursive: true });
2323
+ fs.mkdirSync(baseSessionRoot, { recursive: true });
2324
+ fs.mkdirSync(sessionRoot, { mode: 0o700 });
2325
+ sessionRoot = fs.realpathSync.native(sessionRoot);
2260
2326
  } catch (error) {
2261
2327
  const message = error instanceof Error ? error.message : String(error);
2262
2328
  return toExecutionErrorResult(
@@ -2266,6 +2332,12 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2266
2332
  }
2267
2333
  const sessionDirForIndex = (idx?: number) =>
2268
2334
  path.join(sessionRoot, `run-${idx ?? 0}`);
2335
+ let sessionFileForIndex: (idx?: number) => string | undefined;
2336
+ try {
2337
+ sessionFileForIndex = createForkContextResolver(ctx.sessionManager, effectiveParams.context, { sessionDirForIndex }).sessionFileForIndex;
2338
+ } catch (error) {
2339
+ return toExecutionErrorResult(effectiveParams, error);
2340
+ }
2269
2341
  const childSessionFileForIndex = (idx?: number) =>
2270
2342
  sessionFileForIndex(idx) ?? path.join(sessionDirForIndex(idx), "session.jsonl");
2271
2343
 
@@ -2274,6 +2346,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2274
2346
  : undefined;
2275
2347
 
2276
2348
  const execData: ExecutionContextData = {
2349
+ runtime,
2277
2350
  params: effectiveParams,
2278
2351
  effectiveCwd,
2279
2352
  ctx,
@@ -2284,7 +2357,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2284
2357
  shareEnabled,
2285
2358
  sessionRoot,
2286
2359
  sessionDirForIndex,
2287
- sessionFileForIndex: childSessionFileForIndex,
2360
+ sessionFileForIndex,
2361
+ sessionFileHintForIndex: childSessionFileForIndex,
2288
2362
  artifactConfig,
2289
2363
  artifactsDir,
2290
2364
  backgroundRequestedWhileClarifying,
@@ -2332,12 +2406,15 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2332
2406
  ? effectiveParams.chain.flatMap((step) => isParallelStep(step) ? step.parallel.map((task) => task.agent) : [(step as SequentialStep).agent])
2333
2407
  : effectiveParams.agent ? [effectiveParams.agent] : [];
2334
2408
  try {
2335
- writeNestedEvent(inheritedNestedRoute, {
2409
+ const written = writeNestedEvent(inheritedNestedRoute, {
2336
2410
  type,
2337
2411
  ts: now,
2338
2412
  parentRunId: nestedParentAddress.parentRunId,
2339
2413
  parentStepIndex: nestedParentAddress.parentStepIndex,
2340
2414
  child: {
2415
+ runtime,
2416
+ sessionRoot,
2417
+ capabilityToken: inheritedNestedRoute.capabilityToken,
2341
2418
  id: runId,
2342
2419
  parentRunId: nestedParentAddress.parentRunId,
2343
2420
  parentStepIndex: nestedParentAddress.parentStepIndex,
@@ -2360,7 +2437,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
2360
2437
  })) } : {}),
2361
2438
  },
2362
2439
  });
2440
+ if (type === "subagent.nested.started" && written === false) throw new Error("Nested origin stamp was refused.");
2363
2441
  } catch (error) {
2442
+ if (type === "subagent.nested.started") throw error;
2364
2443
  console.error("Failed to emit nested foreground status event:", error);
2365
2444
  }
2366
2445
  };
@@ -1,4 +1,5 @@
1
1
  import * as fs from "node:fs";
2
+ import type { PiRuntimeIdentity } from "./pi-spawn.ts";
2
3
  import type { Message } from "@earendil-works/pi-ai";
3
4
  import {
4
5
  createChildHostController,
@@ -16,6 +17,7 @@ import { createSessionWatcher, type OnNewSessionMessage, type SessionWatcher } f
16
17
  import type { ExecutionBackend, ExecutionSurfaceHandle } from "../../execution-backend/types.ts";
17
18
 
18
19
  export interface ExternalExecutionOptions {
20
+ readonly runtime: PiRuntimeIdentity;
19
21
  readonly backend: ExecutionBackend;
20
22
  readonly runId: string;
21
23
  readonly childId: string;