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,7 +1,12 @@
1
- import * as fs from "node:fs";
1
+ import fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { ASYNC_DIR, RESULTS_DIR, type AsyncStatus } from "../../shared/types.ts";
3
+ import { ASYNC_DIR, LEGACY_ASYNC_DIR, RESULTS_DIR, type AsyncStatus } from "../../shared/types.ts";
4
4
  import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
5
+ import { parsePiRuntime, type PiRuntimeIdentity } from "../shared/pi-spawn.ts";
6
+
7
+ export function isLegacyAsyncDir(asyncDir: string): boolean {
8
+ try { assertInsideRoot(LEGACY_ASYNC_DIR, asyncDir, "Async run directory"); return true; } catch { return false; }
9
+ }
5
10
 
6
11
  export interface AsyncResumeParams {
7
12
  id?: string;
@@ -11,7 +16,7 @@ export interface AsyncResumeParams {
11
16
  }
12
17
 
13
18
  export interface AsyncResumeDeps {
14
- asyncDirRoot?: string;
19
+ asyncDirRoot?: string | readonly string[];
15
20
  resultsDir?: string;
16
21
  kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
17
22
  now?: () => number;
@@ -19,6 +24,7 @@ export interface AsyncResumeDeps {
19
24
 
20
25
  export type AsyncResumeTarget = {
21
26
  kind: "revive";
27
+ runtime: PiRuntimeIdentity;
22
28
  runId: string;
23
29
  asyncDir?: string;
24
30
  state: AsyncStatus["state"];
@@ -29,6 +35,7 @@ export type AsyncResumeTarget = {
29
35
  };
30
36
 
31
37
  interface AsyncResultFile {
38
+ runtime?: unknown;
32
39
  id?: string;
33
40
  runId?: string;
34
41
  agent?: string;
@@ -82,6 +89,7 @@ function validateResultFile(value: unknown, resultPath: string): AsyncResultFile
82
89
  const success = data.success;
83
90
  if (success !== undefined && typeof success !== "boolean") throw new Error(`Invalid async result file '${resultPath}': success must be a boolean.`);
84
91
  return {
92
+ runtime: data.runtime,
85
93
  id: validateOptionalString(data, "id", resultPath),
86
94
  runId: validateOptionalString(data, "runId", resultPath),
87
95
  agent: validateOptionalString(data, "agent", resultPath),
@@ -124,9 +132,16 @@ function assertRunId(value: string | undefined, field: "id" | "runId"): string |
124
132
  return value;
125
133
  }
126
134
 
135
+ function canonicalPath(value: string): string {
136
+ const resolved = path.resolve(value);
137
+ if (fs.existsSync(resolved)) return fs.realpathSync(resolved);
138
+ const parent = path.dirname(resolved);
139
+ return parent === resolved ? resolved : path.join(canonicalPath(parent), path.basename(resolved));
140
+ }
141
+
127
142
  function assertInsideRoot(root: string, target: string, label: string): void {
128
- const rootPath = path.resolve(root);
129
- const targetPath = path.resolve(target);
143
+ const rootPath = canonicalPath(root);
144
+ const targetPath = canonicalPath(target);
130
145
  const relative = path.relative(rootPath, targetPath);
131
146
  if (relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))) return;
132
147
  throw new Error(`${label} must be inside ${rootPath}.`);
@@ -146,22 +161,34 @@ function exactResultPath(resultsDir: string, runId: string): string | null {
146
161
  return fs.existsSync(resultPath) ? resultPath : null;
147
162
  }
148
163
 
149
- export function findAsyncRunPrefixMatches(prefix: string, asyncDirRoot: string, resultsDir: string): Array<{ id: string; location: AsyncRunLocation }> {
164
+ export function winningAsyncRunEntries(asyncDirRoot: string | readonly string[], prefix = ""): Array<{ id: string; root: string; asyncDir: string }> {
165
+ const winners = new Map<string, { id: string; root: string; asyncDir: string }>();
166
+ for (const root of typeof asyncDirRoot === "string" ? [asyncDirRoot] : asyncDirRoot) {
167
+ for (const id of prefixedRunIds(root, prefix)) {
168
+ if (!winners.has(id)) winners.set(id, { id, root, asyncDir: path.join(path.resolve(root), id) });
169
+ }
170
+ }
171
+ return [...winners.values()].sort((a, b) => a.id.localeCompare(b.id));
172
+ }
173
+
174
+ export function findAsyncRunPrefixMatches(prefix: string, asyncDirRoot: string | readonly string[], resultsDir: string): Array<{ id: string; location: AsyncRunLocation }> {
150
175
  const requestedId = assertRunId(prefix, "id");
151
176
  if (!requestedId) return [];
152
- const asyncRoot = path.resolve(asyncDirRoot);
177
+ const entries = winningAsyncRunEntries(asyncDirRoot, requestedId);
178
+ const byId = new Map(entries.map((entry) => [entry.id, entry]));
153
179
  const resultRoot = path.resolve(resultsDir);
154
180
  const matchingIds = [...new Set([
155
- ...prefixedRunIds(asyncRoot, requestedId),
181
+ ...byId.keys(),
156
182
  ...prefixedRunIds(resultRoot, requestedId, ".json"),
157
183
  ])].sort();
158
184
  return matchingIds.map((id) => {
159
- const asyncDir = path.join(asyncRoot, id);
160
- assertInsideRoot(asyncRoot, asyncDir, "Async run directory");
185
+ const entry = byId.get(id);
186
+ const asyncDir = entry?.asyncDir;
187
+ if (entry) assertInsideRoot(entry.root, entry.asyncDir, "Async run directory");
161
188
  return {
162
189
  id,
163
190
  location: {
164
- asyncDir: fs.existsSync(asyncDir) ? asyncDir : null,
191
+ asyncDir: asyncDir && fs.existsSync(asyncDir) ? asyncDir : null,
165
192
  resultPath: exactResultPath(resultRoot, id),
166
193
  resolvedId: id,
167
194
  },
@@ -169,7 +196,28 @@ export function findAsyncRunPrefixMatches(prefix: string, asyncDirRoot: string,
169
196
  });
170
197
  }
171
198
 
172
- export function resolveAsyncRunLocation(params: AsyncResumeParams, asyncDirRoot: string, resultsDir: string): AsyncRunLocation {
199
+ export function resolveAsyncRunLocation(params: AsyncResumeParams, asyncDirRoot: string | readonly string[], resultsDir: string): AsyncRunLocation {
200
+ if (typeof asyncDirRoot !== "string") {
201
+ if (params.dir) {
202
+ for (const root of asyncDirRoot) {
203
+ try { assertInsideRoot(root, params.dir, "Async run directory"); } catch { continue; }
204
+ return resolveAsyncRunLocation(params, root, resultsDir);
205
+ }
206
+ throw new Error("Async run directory must be inside an authorized async root.");
207
+ }
208
+ const id = assertRunId(params.id, "id") ?? assertRunId(params.runId, "runId");
209
+ if (!id) return { asyncDir: null, resultPath: null };
210
+ const exact = winningAsyncRunEntries(asyncDirRoot, id).find((entry) => entry.id === id);
211
+ if (exact) {
212
+ assertInsideRoot(exact.root, exact.asyncDir, "Async run directory");
213
+ return { asyncDir: exact.asyncDir, resultPath: exactResultPath(resultsDir, id), resolvedId: id };
214
+ }
215
+ const resultPath = exactResultPath(resultsDir, id);
216
+ if (resultPath) return { asyncDir: null, resultPath, resolvedId: id };
217
+ const matches = findAsyncRunPrefixMatches(id, asyncDirRoot, resultsDir);
218
+ if (matches.length > 1) throw new Error(`Ambiguous async run id prefix '${id}' matched: ${matches.map((match) => match.id).join(", ")}. Provide a longer id.`);
219
+ return matches[0]?.location ?? { asyncDir: null, resultPath: null, resolvedId: id };
220
+ }
173
221
  const asyncRoot = path.resolve(asyncDirRoot);
174
222
  const resultRoot = path.resolve(resultsDir);
175
223
  const requestedId = assertRunId(params.id, "id") ?? assertRunId(params.runId, "runId");
@@ -234,23 +282,50 @@ function validateResumeSessionFile(runId: string, sessionFile: string): string {
234
282
  }
235
283
 
236
284
  export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncResumeDeps = {}): AsyncResumeTarget {
237
- const asyncDirRoot = deps.asyncDirRoot ?? ASYNC_DIR;
285
+ const asyncDirRoot = deps.asyncDirRoot ?? [ASYNC_DIR, LEGACY_ASYNC_DIR];
238
286
  const resultsDir = deps.resultsDir ?? RESULTS_DIR;
239
287
  const location = resolveAsyncRunLocation(params, asyncDirRoot, resultsDir);
240
288
  if (!location.asyncDir && !location.resultPath) {
241
289
  throw new Error("Async run not found. Provide id or dir.");
242
290
  }
243
291
 
244
- const reconciliation = location.asyncDir
245
- ? reconcileAsyncRun(location.asyncDir, { resultsDir, kill: deps.kill, now: deps.now })
246
- : undefined;
247
- const status = reconciliation?.status ?? null;
292
+ const statusPath = location.asyncDir ? path.join(location.asyncDir, "status.json") : undefined;
293
+ let status: AsyncStatus | null = null;
294
+ if (statusPath) {
295
+ let present = false;
296
+ try {
297
+ fs.lstatSync(statusPath);
298
+ present = true;
299
+ } catch (error) {
300
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
301
+ throw new Error(`Failed to inspect async status file '${statusPath}': ${getErrorMessage(error)}`, { cause: error });
302
+ }
303
+ }
304
+ if (present) {
305
+ let value: unknown;
306
+ try {
307
+ value = JSON.parse(fs.readFileSync(statusPath, "utf-8"));
308
+ } catch (error) {
309
+ throw new Error(`Failed to read or parse async status file '${statusPath}': ${getErrorMessage(error)}`, { cause: error });
310
+ }
311
+ status = ensureObject(value, statusPath) as unknown as AsyncStatus;
312
+ }
313
+ }
314
+ if (status && (status.state === "queued" || status.state === "running") && location.asyncDir && !isLegacyAsyncDir(location.asyncDir)) {
315
+ status = reconcileAsyncRun(location.asyncDir, { resultsDir, kill: deps.kill, now: deps.now }).status ?? status;
316
+ }
248
317
  validateStatusForResume(status, location.asyncDir ? path.join(location.asyncDir, "status.json") : "status.json");
249
318
  const result = location.resultPath ? readResultFile(location.resultPath) : undefined;
319
+ const runtime = parsePiRuntime(status ? status.runtime : result?.runtime, statusPath ?? location.resultPath ?? "Async run");
320
+ if (status && result?.runtime !== undefined) {
321
+ const other = parsePiRuntime(result.runtime, location.resultPath!);
322
+ if (runtime.nodeExecPath !== other.nodeExecPath || runtime.piEntryPath !== other.piEntryPath || runtime.piPackageRoot !== other.piPackageRoot) throw new Error("Async status and result runtime identities conflict; a new run is required.");
323
+ }
250
324
  const runId = status?.runId ?? result?.runId ?? result?.id ?? location.resolvedId ?? (location.asyncDir ? path.basename(location.asyncDir) : "unknown");
251
325
  const state = status?.state ?? (result ? resultState(result) : undefined);
252
326
  if (!state) throw new Error(`Status file not found for async run '${runId}'.`);
253
327
 
328
+ if (state === "queued") throw new Error(`Async run '${runId}' is queued and cannot be revived before startup.`);
254
329
  const statusSteps = status?.steps ?? [];
255
330
  const resultSteps = result?.results ?? [];
256
331
  const stepCount = statusSteps.length || resultSteps.length || (result?.agent ? 1 : 0);
@@ -288,6 +363,7 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
288
363
 
289
364
  return {
290
365
  kind: "revive",
366
+ runtime,
291
367
  runId,
292
368
  asyncDir: location.asyncDir ?? undefined,
293
369
  state,
@@ -2,9 +2,10 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../../shared/formatters.ts";
4
4
  import { formatActivityLabel, formatParallelOutcome } from "../../shared/status-format.ts";
5
- import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type NestedRunSummary, type SubagentRunMode, type TokenUsage } from "../../shared/types.ts";
5
+ import { RESULTS_DIR, type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type NestedRunSummary, type SubagentRunMode, type TokenUsage } from "../../shared/types.ts";
6
+ import { isLegacyAsyncDir, resolveAsyncRunLocation, winningAsyncRunEntries } from "./async-resume.ts";
6
7
  import { readStatus } from "../../shared/utils.ts";
7
- import { attachRootChildrenToSteps, findNestedRouteForRootId, projectNestedRegistryForRoot } from "../shared/nested-events.ts";
8
+ import { attachRootChildrenToSteps, findNestedRouteForRootId, isLegacyNestedRoute, projectNestedRegistryForRoot } from "../shared/nested-events.ts";
8
9
  import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
9
10
  import { flatToLogicalStepIndex, normalizeParallelGroups } from "./parallel-groups.ts";
10
11
  import { readRunnerLogTail, reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-reconciler.ts";
@@ -133,7 +134,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
133
134
  let nestedChildren: NestedRunSummary[] = [];
134
135
  if (nestedWarnings.length === 0) {
135
136
  try {
136
- nestedChildren = projectNestedRegistryForRoot(status.runId || path.basename(asyncDir))?.children ?? [];
137
+ nestedChildren = projectNestedRegistryForRoot(status.runId || path.basename(asyncDir), { readOnlyLegacy: true })?.children ?? [];
137
138
  } catch (error) {
138
139
  nestedWarnings.push(`Nested status unavailable: ${getErrorMessage(error)}`);
139
140
  }
@@ -242,10 +243,10 @@ function summarizePreStatusCrash(asyncDir: string, now: number): AsyncRunSummary
242
243
  };
243
244
  }
244
245
 
245
- export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions = {}): AsyncRunSummary[] {
246
- let entries: string[];
246
+ export function listAsyncRuns(asyncDirRoot: string | readonly string[], options: AsyncRunListOptions = {}): AsyncRunSummary[] {
247
+ let entries: ReturnType<typeof winningAsyncRunEntries>;
247
248
  try {
248
- entries = fs.readdirSync(asyncDirRoot).filter((entry) => isAsyncRunDir(asyncDirRoot, entry));
249
+ entries = winningAsyncRunEntries(asyncDirRoot);
249
250
  } catch (error) {
250
251
  if (isNotFoundError(error)) return [];
251
252
  throw new Error(`Failed to list async runs in '${asyncDirRoot}': ${getErrorMessage(error)}`, {
@@ -256,8 +257,10 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
256
257
  const allowedStates = options.states ? new Set(options.states) : undefined;
257
258
  const runs: AsyncRunSummary[] = [];
258
259
  for (const entry of entries) {
259
- const asyncDir = path.join(asyncDirRoot, entry);
260
- const reconciliation = options.reconcile === false
260
+ const asyncDir = resolveAsyncRunLocation({ id: entry.id }, entry.root, options.resultsDir ?? RESULTS_DIR).asyncDir;
261
+ if (!asyncDir || !isAsyncRunDir(entry.root, entry.id)) continue;
262
+ const stored = readStatus(asyncDir);
263
+ const reconciliation = options.reconcile === false || isLegacyAsyncDir(asyncDir) || (stored && stored.state !== "running" && stored.state !== "queued")
261
264
  ? undefined
262
265
  : reconcileAsyncRun(asyncDir, { resultsDir: options.resultsDir, kill: options.kill, now: options.now });
263
266
  const status = (reconciliation?.status ?? readStatus(asyncDir)) as (AsyncStatus & { cwd?: string }) | null;
@@ -269,7 +272,7 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
269
272
  const nestedWarnings: string[] = [];
270
273
  try {
271
274
  const nestedRoute = findNestedRouteForRootId(status.runId || path.basename(asyncDir));
272
- if (nestedRoute) reconcileNestedAsyncDescendants(nestedRoute, { resultsDir: options.resultsDir, kill: options.kill, now: options.now });
275
+ if (nestedRoute && !isLegacyAsyncDir(asyncDir) && !isLegacyNestedRoute(nestedRoute)) reconcileNestedAsyncDescendants(nestedRoute, { resultsDir: options.resultsDir, kill: options.kill, now: options.now });
273
276
  } catch (error) {
274
277
  nestedWarnings.push(`Nested status unavailable: ${getErrorMessage(error)}`);
275
278
  }
@@ -8,10 +8,11 @@
8
8
  * before the task arg and forwards all other args unchanged.
9
9
  */
10
10
  import * as path from "node:path";
11
+ import { PINNED_AGENT_DIR_OVERRIDE } from "../../shared/agent-dir.ts";
11
12
  import { fileURLToPath } from "node:url";
12
13
  import type { Message } from "@earendil-works/pi-ai";
13
14
  import type { Usage } from "../../shared/types.ts";
14
- import { getPiSpawnCommand } from "../shared/pi-spawn.ts";
15
+ import { getPiSpawnCommand, type PiRuntimeIdentity } from "../shared/pi-spawn.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 { OnNewSessionMessage } from "../../execution-backend/session-watcher.ts";
@@ -36,6 +37,7 @@ interface SynthesizedChildEvent {
36
37
  export type OnSynthesizedChildEvent = (event: SynthesizedChildEvent) => void;
37
38
 
38
39
  export interface RunExternalBackgroundAttemptInput {
40
+ readonly runtime: PiRuntimeIdentity;
39
41
  readonly owner: ExternalExecution;
40
42
  readonly attemptId: string;
41
43
  /**
@@ -148,13 +150,15 @@ export async function runExternalBackgroundAttempt(
148
150
  args.push("--extension", REPORTING_EXTENSION_PATH);
149
151
  if (taskArg !== undefined) args.push(taskArg);
150
152
 
151
- const spawnSpec = getPiSpawnCommand(args);
153
+ const spawnSpec = getPiSpawnCommand(args, input.runtime);
152
154
 
153
155
  const mergedEnv: Record<string, string> = {};
154
156
  for (const [key, value] of Object.entries(environment)) {
155
157
  if (value !== undefined) mergedEnv[key] = value;
156
158
  }
157
159
 
160
+ if (PINNED_AGENT_DIR_OVERRIDE) mergedEnv.PI_CODING_AGENT_DIR = PINNED_AGENT_DIR_OVERRIDE;
161
+
158
162
  const onNewSessionMessage: OnNewSessionMessage | undefined = onChildEvent
159
163
  ? (message) => synthesizeChildEvents(message, onChildEvent)
160
164
  : undefined;
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { ASYNC_DIR, RESULTS_DIR, type SubagentState } from "../../shared/types.ts";
4
- import { findAsyncRunPrefixMatches, type AsyncRunLocation } from "./async-resume.ts";
3
+ import { ASYNC_DIR, LEGACY_ASYNC_DIR, RESULTS_DIR, type SubagentState } from "../../shared/types.ts";
4
+ import { findAsyncRunPrefixMatches, resolveAsyncRunLocation, winningAsyncRunEntries, type AsyncRunLocation } from "./async-resume.ts";
5
5
  import { assertSafeNestedId, findNestedRunMatchesById, type NestedRoute, type NestedRunMatch, type NestedRunResolutionScope } from "../shared/nested-events.ts";
6
6
 
7
7
  export type ResolvedSubagentRunId =
@@ -11,20 +11,17 @@ export type ResolvedSubagentRunId =
11
11
 
12
12
  export interface ResolveSubagentRunIdDeps {
13
13
  state?: SubagentState;
14
- asyncDirRoot?: string;
14
+ asyncDirRoot?: string | readonly string[];
15
15
  resultsDir?: string;
16
16
  nested?: NestedRunResolutionScope;
17
+ readOnlyLegacy?: boolean;
17
18
  }
18
19
 
19
- function exactAsyncLocation(id: string, asyncDirRoot: string, resultsDir: string): AsyncRunLocation | undefined {
20
- const asyncDir = path.join(asyncDirRoot, id);
20
+ function exactAsyncLocation(id: string, asyncDirRoot: string | readonly string[], resultsDir: string): AsyncRunLocation | undefined {
21
+ const exact = winningAsyncRunEntries(asyncDirRoot, id).find((entry) => entry.id === id);
21
22
  const resultPath = path.join(resultsDir, `${id}.json`);
22
- if (!fs.existsSync(asyncDir) && !fs.existsSync(resultPath)) return undefined;
23
- return {
24
- asyncDir: fs.existsSync(asyncDir) ? asyncDir : null,
25
- resultPath: fs.existsSync(resultPath) ? resultPath : null,
26
- resolvedId: id,
27
- };
23
+ if (!exact && !fs.existsSync(resultPath)) return undefined;
24
+ return resolveAsyncRunLocation({ id }, asyncDirRoot, resultsDir);
28
25
  }
29
26
 
30
27
  function foregroundIds(state: SubagentState | undefined): string[] {
@@ -37,7 +34,7 @@ function nestedScopeFromState(state: SubagentState | undefined): NestedRunResolu
37
34
  const seen = new Set<string>();
38
35
  const add = (route: NestedRoute | undefined) => {
39
36
  if (!route) return;
40
- const key = `${route.rootRunId}:${route.eventSink}:${route.controlInbox}`;
37
+ const key = JSON.stringify([route.rootRunId, route.capabilityToken, route.eventSink, route.controlInbox]);
41
38
  if (seen.has(key)) return;
42
39
  seen.add(key);
43
40
  routes.push(route);
@@ -47,20 +44,21 @@ function nestedScopeFromState(state: SubagentState | undefined): NestedRunResolu
47
44
  return { routes };
48
45
  }
49
46
 
50
- function asyncPrefixMatches(prefix: string, asyncDirRoot: string, resultsDir: string): Array<{ id: string; location: AsyncRunLocation }> {
47
+ function asyncPrefixMatches(prefix: string, asyncDirRoot: string | readonly string[], resultsDir: string): Array<{ id: string; location: AsyncRunLocation }> {
51
48
  return findAsyncRunPrefixMatches(prefix, asyncDirRoot, resultsDir);
52
49
  }
53
50
 
54
51
  export function resolveSubagentRunId(id: string, deps: ResolveSubagentRunIdDeps = {}): ResolvedSubagentRunId | undefined {
55
52
  assertSafeNestedId("id", id);
56
- const asyncDirRoot = deps.asyncDirRoot ?? ASYNC_DIR;
53
+ const asyncDirRoot = deps.asyncDirRoot ?? [ASYNC_DIR, LEGACY_ASYNC_DIR];
57
54
  const resultsDir = deps.resultsDir ?? RESULTS_DIR;
58
55
 
59
56
  const nestedScope = deps.nested ?? nestedScopeFromState(deps.state);
60
57
  if (deps.state?.foregroundControls.has(id)) return { kind: "foreground", id };
61
58
  const exactAsync = exactAsyncLocation(id, asyncDirRoot, resultsDir);
62
59
  if (exactAsync) return { kind: "async", id, location: exactAsync };
63
- const exactNested = findNestedRunMatchesById(id, nestedScope ? { scope: nestedScope } : {});
60
+ const nestedOptions = { scope: nestedScope, readOnlyLegacy: deps.readOnlyLegacy };
61
+ const exactNested = findNestedRunMatchesById(id, nestedOptions);
64
62
  if (exactNested.length > 1) throw new Error(`Nested run id '${id}' is ambiguous across authorized registries. Provide the full id after stale registries are cleaned up.`);
65
63
  if (exactNested[0]) return { kind: "nested", id, match: exactNested[0] };
66
64
 
@@ -71,11 +69,10 @@ export function resolveSubagentRunId(id: string, deps: ResolveSubagentRunIdDeps
71
69
  for (const match of asyncPrefixMatches(id, asyncDirRoot, resultsDir)) {
72
70
  matches.push({ kind: "async", id: match.id, location: match.location });
73
71
  }
74
- for (const match of findNestedRunMatchesById(id, nestedScope ? { prefix: true, scope: nestedScope } : { prefix: true })) {
72
+ for (const match of findNestedRunMatchesById(id, { ...nestedOptions, prefix: true })) {
75
73
  matches.push({ kind: "nested", id: match.run.id, match });
76
74
  }
77
- const unique = new Map(matches.map((match) => [`${match.kind}:${match.id}`, match]));
78
- const values = [...unique.values()];
75
+ const values = matches;
79
76
  if (values.length > 1) {
80
77
  throw new Error(`Ambiguous subagent run id prefix '${id}' matched: ${values.map((match) => `${match.kind}:${match.id}`).join(", ")}. Provide a longer id.`);
81
78
  }
@@ -1,16 +1,17 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
4
+ import { readStatus } from "../../shared/utils.ts";
4
5
  import { formatAsyncRunList, formatAsyncRunOutputPath, formatAsyncRunProgressLabel, listAsyncRuns } from "./async-status.ts";
5
6
  import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
6
7
  import { formatModelThinking } from "../../shared/formatters.ts";
7
8
  import { formatActivityLabel } from "../../shared/status-format.ts";
8
- import { ASYNC_DIR, RESULTS_DIR, type AsyncStatus, type Details, type NestedRunSummary, type SubagentState } from "../../shared/types.ts";
9
- import { resolveAsyncRunLocation } from "./async-resume.ts";
9
+ import { ASYNC_DIR, LEGACY_ASYNC_DIR, RESULTS_DIR, type AsyncStatus, type Details, type NestedRunSummary, type SubagentState } from "../../shared/types.ts";
10
+ import { isLegacyAsyncDir, resolveAsyncRunLocation } from "./async-resume.ts";
10
11
  import { resolveSubagentRunId } from "./run-id-resolver.ts";
11
12
  import { flatToLogicalStepIndex, normalizeParallelGroups } from "./parallel-groups.ts";
12
13
  import { readRunnerLogTail, reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-reconciler.ts";
13
- import { attachRootChildrenToSteps, findNestedRouteForRootId, projectNestedRegistryForRoot, type NestedRunResolutionScope } from "../shared/nested-events.ts";
14
+ import { attachRootChildrenToSteps, findNestedRouteForRootId, isLegacyNestedRoute, projectNestedRegistryForRoot, type NestedRunResolutionScope } from "../shared/nested-events.ts";
14
15
 
15
16
  interface RunStatusParams {
16
17
  action?: "status";
@@ -20,7 +21,7 @@ interface RunStatusParams {
20
21
  }
21
22
 
22
23
  interface RunStatusDeps {
23
- asyncDirRoot?: string;
24
+ asyncDirRoot?: string | readonly string[];
24
25
  resultsDir?: string;
25
26
  kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
26
27
  now?: () => number;
@@ -98,7 +99,7 @@ function formatNestedExactStatus(rootRunId: string, run: NestedRunSummary): stri
98
99
  }
99
100
 
100
101
  export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDeps = {}): AgentToolResult<Details> {
101
- const asyncDirRoot = deps.asyncDirRoot ?? ASYNC_DIR;
102
+ const asyncDirRoot = deps.asyncDirRoot ?? [ASYNC_DIR, LEGACY_ASYNC_DIR];
102
103
  const resultsDir = deps.resultsDir ?? RESULTS_DIR;
103
104
  if (!params.id && !params.runId && !params.dir) {
104
105
  if (deps.nested) {
@@ -128,10 +129,10 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
128
129
  try {
129
130
  const requestedId = params.id ?? params.runId;
130
131
  if (!params.dir && requestedId) {
131
- const resolved = resolveSubagentRunId(requestedId, { asyncDirRoot, resultsDir, state: deps.state, nested: deps.nested });
132
+ const resolved = resolveSubagentRunId(requestedId, { asyncDirRoot, resultsDir, state: deps.state, nested: deps.nested, readOnlyLegacy: true });
132
133
  if (resolved?.kind === "nested") {
133
- reconcileNestedAsyncDescendants(resolved.match.route, { resultsDir, kill: deps.kill, now: deps.now });
134
- const refreshed = resolveSubagentRunId(requestedId, { asyncDirRoot, resultsDir, state: deps.state, nested: deps.nested });
134
+ if (!isLegacyNestedRoute(resolved.match.route)) reconcileNestedAsyncDescendants(resolved.match.route, { resultsDir, kill: deps.kill, now: deps.now });
135
+ const refreshed = resolveSubagentRunId(requestedId, { asyncDirRoot, resultsDir, state: deps.state, nested: deps.nested, readOnlyLegacy: true });
135
136
  const nested = refreshed?.kind === "nested" ? refreshed : resolved;
136
137
  return { content: [{ type: "text", text: formatNestedExactStatus(nested.match.rootRunId, nested.match.run) }], details: { mode: "single", results: [] } };
137
138
  }
@@ -161,7 +162,17 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
161
162
  if (asyncDir) {
162
163
  let reconciliation;
163
164
  try {
164
- reconciliation = reconcileAsyncRun(asyncDir, { resultsDir, kill: deps.kill, now: deps.now });
165
+ const statusPath = path.join(asyncDir, "status.json");
166
+ let stored: AsyncStatus | null = null;
167
+ try {
168
+ stored = readStatus(asyncDir);
169
+ if (fs.existsSync(statusPath) && (!stored || typeof stored !== "object" || Array.isArray(stored))) throw new Error(`Invalid async status '${statusPath}': expected an object.`);
170
+ } catch (error) {
171
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
172
+ }
173
+ reconciliation = isLegacyAsyncDir(asyncDir) || (stored && stored.state !== "running" && stored.state !== "queued")
174
+ ? { status: stored, message: undefined, resultPath }
175
+ : reconcileAsyncRun(asyncDir, { resultsDir, kill: deps.kill, now: deps.now });
165
176
  } catch (error) {
166
177
  const message = error instanceof Error ? error.message : String(error);
167
178
  return {
@@ -179,8 +190,8 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
179
190
  let nestedWarning: string | undefined;
180
191
  try {
181
192
  const nestedRoute = findNestedRouteForRootId(status.runId);
182
- if (nestedRoute) reconcileNestedAsyncDescendants(nestedRoute, { resultsDir, kill: deps.kill, now: deps.now });
183
- nestedChildren = projectNestedRegistryForRoot(status.runId)?.children ?? [];
193
+ if (nestedRoute && !isLegacyAsyncDir(asyncDir) && !isLegacyNestedRoute(nestedRoute)) reconcileNestedAsyncDescendants(nestedRoute, { resultsDir, kill: deps.kill, now: deps.now });
194
+ nestedChildren = projectNestedRegistryForRoot(status.runId, { readOnlyLegacy: true })?.children ?? [];
184
195
  attachRootChildrenToSteps(status.runId, status.steps, nestedChildren);
185
196
  } catch (error) {
186
197
  nestedWarning = `Nested status unavailable: ${error instanceof Error ? error.message : String(error)}`;
@@ -240,6 +240,9 @@ function buildFailedRepair(status: AsyncStatus, asyncDir: string, now: number, r
240
240
  asyncDir,
241
241
  sessionId: status.sessionId,
242
242
  sessionFile: status.sessionFile,
243
+ sessionDir: status.sessionDir,
244
+ runtime: status.runtime,
245
+ cwd: status.cwd,
243
246
  },
244
247
  };
245
248
  }
@@ -326,8 +329,13 @@ export function reconcileAsyncRun(asyncDir: string, options: ReconcileAsyncRunOp
326
329
 
327
330
  const runId = effectiveStatus.runId || path.basename(asyncDir);
328
331
  const resultPath = path.join(options.resultsDir ?? RESULTS_DIR, `${runId}.json`);
332
+ if (effectiveStatus.state === "queued") {
333
+ return { status: effectiveStatus, repaired: false, resultPath, message: "Unresolved async startup: queued run has not published running status; start a new run." };
334
+ }
335
+ if (terminal(effectiveStatus.state)) return { status: effectiveStatus, repaired: false, resultPath };
336
+
329
337
  if (fs.existsSync(resultPath)) {
330
- const terminalStatus = effectiveStatus.state === "running" || effectiveStatus.state === "queued"
338
+ const terminalStatus = effectiveStatus.state === "running"
331
339
  ? terminalStatusFromResult(effectiveStatus, resultPath, now)
332
340
  : undefined;
333
341
  if (terminalStatus) {