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.
- package/CHANGELOG.md +6 -0
- package/README.md +6 -0
- package/package.json +3 -3
- package/src/execution-backend/child-host-controller.ts +10 -8
- package/src/extension/doctor.ts +6 -2
- package/src/extension/fanout-child.ts +2 -2
- package/src/extension/index.ts +8 -6
- package/src/runs/background/async-execution.ts +154 -87
- package/src/runs/background/async-job-tracker.ts +4 -1
- package/src/runs/background/async-resume.ts +93 -17
- package/src/runs/background/async-status.ts +12 -9
- package/src/runs/background/external-attempt.ts +6 -2
- package/src/runs/background/run-id-resolver.ts +15 -18
- package/src/runs/background/run-status.ts +22 -11
- package/src/runs/background/stale-run-reconciler.ts +9 -1
- package/src/runs/background/subagent-runner.ts +54 -79
- package/src/runs/foreground/chain-execution.ts +7 -0
- package/src/runs/foreground/execution.ts +30 -13
- package/src/runs/foreground/external-single-attempt.ts +3 -2
- package/src/runs/foreground/subagent-executor.ts +142 -63
- package/src/runs/shared/external-execution.ts +2 -0
- package/src/runs/shared/jiti-cli.ts +17 -48
- package/src/runs/shared/nested-events.ts +103 -30
- package/src/runs/shared/pi-spawn.ts +75 -67
- package/src/shared/agent-dir.ts +12 -0
- package/src/shared/atomic-json.ts +12 -1
- package/src/shared/fork-context.ts +15 -3
- package/src/shared/types.ts +11 -1
- package/src/shared/utils.ts +1 -6
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import
|
|
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 =
|
|
129
|
-
const targetPath =
|
|
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
|
|
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
|
|
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
|
-
...
|
|
181
|
+
...byId.keys(),
|
|
156
182
|
...prefixedRunIds(resultRoot, requestedId, ".json"),
|
|
157
183
|
])].sort();
|
|
158
184
|
return matchingIds.map((id) => {
|
|
159
|
-
const
|
|
160
|
-
|
|
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
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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:
|
|
246
|
+
export function listAsyncRuns(asyncDirRoot: string | readonly string[], options: AsyncRunListOptions = {}): AsyncRunSummary[] {
|
|
247
|
+
let entries: ReturnType<typeof winningAsyncRunEntries>;
|
|
247
248
|
try {
|
|
248
|
-
entries =
|
|
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 =
|
|
260
|
-
|
|
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
|
|
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 (!
|
|
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 =
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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"
|
|
338
|
+
const terminalStatus = effectiveStatus.state === "running"
|
|
331
339
|
? terminalStatusFromResult(effectiveStatus, resultPath, now)
|
|
332
340
|
: undefined;
|
|
333
341
|
if (terminalStatus) {
|