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,10 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
-
import { resolvePiPackageRoot } from "./pi-spawn.ts";
|
|
5
|
-
|
|
6
|
-
const require = createRequire(import.meta.url);
|
|
7
|
-
const piPackageRoot = resolvePiPackageRoot();
|
|
8
4
|
|
|
9
5
|
function resolveJitiCliFromPackageJson(packageJsonPath: string): string | undefined {
|
|
10
6
|
if (!fs.existsSync(packageJsonPath)) return undefined;
|
|
@@ -24,45 +20,18 @@ function resolveJitiCliFromPackageJson(packageJsonPath: string): string | undefi
|
|
|
24
20
|
return undefined;
|
|
25
21
|
}
|
|
26
22
|
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
() => require.resolve(specifier),
|
|
30
|
-
() => piPackageRoot
|
|
31
|
-
? createRequire(path.join(piPackageRoot, "package.json")).resolve(specifier)
|
|
32
|
-
: undefined,
|
|
33
|
-
() => {
|
|
34
|
-
if (!process.argv[1]) return undefined;
|
|
35
|
-
const piEntry = fs.realpathSync(process.argv[1]);
|
|
36
|
-
return createRequire(piEntry).resolve(specifier);
|
|
37
|
-
},
|
|
38
|
-
];
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function resolveJitiCliPath(): string | undefined {
|
|
23
|
+
function resolveJitiCliPath(piPackageRoot: string): string | undefined {
|
|
24
|
+
const require = createRequire(path.join(piPackageRoot, "package.json"));
|
|
42
25
|
const candidates = [
|
|
43
|
-
|
|
44
|
-
() =>
|
|
26
|
+
() => require.resolve("jiti/package.json"),
|
|
27
|
+
() => path.join(piPackageRoot, "node_modules", "jiti", "package.json"),
|
|
45
28
|
];
|
|
46
29
|
for (const candidate of candidates) {
|
|
47
30
|
try {
|
|
48
|
-
const
|
|
49
|
-
if (!packageJsonPath) continue;
|
|
50
|
-
const cliPath = resolveJitiCliFromPackageJson(packageJsonPath);
|
|
31
|
+
const cliPath = resolveJitiCliFromPackageJson(candidate());
|
|
51
32
|
if (cliPath) return cliPath;
|
|
52
33
|
} catch {
|
|
53
|
-
//
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return undefined;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function resolveFromHost(specifier: string): string | undefined {
|
|
60
|
-
for (const candidate of hostResolveCandidates(specifier)) {
|
|
61
|
-
try {
|
|
62
|
-
const resolved = candidate();
|
|
63
|
-
if (resolved) return resolved;
|
|
64
|
-
} catch {
|
|
65
|
-
// Candidate not available in this install, continue probing.
|
|
34
|
+
// Package exports may hide package.json; try the selected install's layout.
|
|
66
35
|
}
|
|
67
36
|
}
|
|
68
37
|
return undefined;
|
|
@@ -72,25 +41,25 @@ function resolveFromHost(specifier: string): string | undefined {
|
|
|
72
41
|
// under the bare jiti CLI, so it gets the same map through JITI_ALIAS.
|
|
73
42
|
const HOST_ALIASED_SPECIFIERS = ["typebox", "typebox/compile", "typebox/value"];
|
|
74
43
|
|
|
75
|
-
export function resolveHostModuleAliases(): Record<string, string> {
|
|
44
|
+
export function resolveHostModuleAliases(piPackageRoot: string): Record<string, string> {
|
|
45
|
+
const require = createRequire(path.join(piPackageRoot, "package.json"));
|
|
76
46
|
const aliases: Record<string, string> = {};
|
|
77
47
|
for (const specifier of HOST_ALIASED_SPECIFIERS) {
|
|
78
|
-
|
|
79
|
-
|
|
48
|
+
try {
|
|
49
|
+
aliases[specifier] = require.resolve(specifier);
|
|
50
|
+
} catch (cause) {
|
|
51
|
+
throw new Error(`Cannot resolve required alias ${specifier} from selected Pi installation ${piPackageRoot}`, { cause });
|
|
52
|
+
}
|
|
80
53
|
}
|
|
81
54
|
return aliases;
|
|
82
55
|
}
|
|
83
56
|
|
|
84
|
-
export function createJitiCliResolver(deps: { resolve?: () => string | undefined; exists?: (p: string) => boolean } = {}): () => string | undefined {
|
|
57
|
+
export function createJitiCliResolver(deps: { resolve?: (piPackageRoot: string) => string | undefined; exists?: (p: string) => boolean } = {}): (piPackageRoot: string) => string | undefined {
|
|
85
58
|
const resolve = deps.resolve ?? resolveJitiCliPath;
|
|
86
59
|
const exists = deps.exists ?? ((p: string) => fs.existsSync(p));
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
cached = resolve();
|
|
91
|
-
if (cached && exists(cached)) return cached;
|
|
92
|
-
cached = undefined;
|
|
93
|
-
return undefined;
|
|
60
|
+
return (piPackageRoot) => {
|
|
61
|
+
const cliPath = resolve(piPackageRoot);
|
|
62
|
+
return cliPath && exists(cliPath) ? cliPath : undefined;
|
|
94
63
|
};
|
|
95
64
|
}
|
|
96
65
|
|
|
@@ -3,6 +3,8 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import {
|
|
5
5
|
ASYNC_DIR,
|
|
6
|
+
LEGACY_ASYNC_DIR,
|
|
7
|
+
RUN_STATE_ROOT_DIR,
|
|
6
8
|
RESULTS_DIR,
|
|
7
9
|
TEMP_ROOT_DIR,
|
|
8
10
|
type AsyncJobState,
|
|
@@ -25,9 +27,11 @@ import {
|
|
|
25
27
|
SUBAGENT_PARENT_ROOT_RUN_ID_ENV,
|
|
26
28
|
SUBAGENT_PARENT_RUN_ID_ENV,
|
|
27
29
|
} from "./pi-args.ts";
|
|
30
|
+
import { parsePiRuntime } from "./pi-spawn.ts";
|
|
28
31
|
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
29
32
|
|
|
30
|
-
export const NESTED_EVENTS_DIR = path.join(
|
|
33
|
+
export const NESTED_EVENTS_DIR = path.join(RUN_STATE_ROOT_DIR, "nested-subagent-events");
|
|
34
|
+
const LEGACY_NESTED_EVENTS_DIR = path.join(TEMP_ROOT_DIR, "nested-subagent-events");
|
|
31
35
|
const ROUTE_FILE = "route.json";
|
|
32
36
|
const REGISTRY_FILE = "registry.json";
|
|
33
37
|
const MAX_EVENT_BYTES = 64 * 1024;
|
|
@@ -92,8 +96,19 @@ function assertSafeId(label: string, value: string): void {
|
|
|
92
96
|
}
|
|
93
97
|
|
|
94
98
|
function containedPath(base: string, candidate: string): boolean {
|
|
95
|
-
const
|
|
96
|
-
const
|
|
99
|
+
const lexicalBase = path.resolve(base);
|
|
100
|
+
const lexicalCandidate = path.resolve(candidate);
|
|
101
|
+
if (lexicalCandidate !== lexicalBase && !lexicalCandidate.startsWith(`${lexicalBase}${path.sep}`)) return false;
|
|
102
|
+
const canonical = (value: string): string => {
|
|
103
|
+
const resolved = path.resolve(value);
|
|
104
|
+
try { return fs.realpathSync(resolved); } catch (error) {
|
|
105
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
106
|
+
const parent = path.dirname(resolved);
|
|
107
|
+
return parent === resolved ? resolved : path.join(canonical(parent), path.basename(resolved));
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
const resolvedBase = canonical(base);
|
|
111
|
+
const resolvedCandidate = canonical(candidate);
|
|
97
112
|
return resolvedCandidate === resolvedBase || resolvedCandidate.startsWith(`${resolvedBase}${path.sep}`);
|
|
98
113
|
}
|
|
99
114
|
|
|
@@ -104,8 +119,9 @@ function commonRouteRoot(route: Pick<NestedRoute, "eventSink" | "controlInbox">)
|
|
|
104
119
|
function validateRouteShape(route: NestedRoute): void {
|
|
105
120
|
assertSafeId("rootRunId", route.rootRunId);
|
|
106
121
|
assertSafeId("capabilityToken", route.capabilityToken);
|
|
107
|
-
|
|
108
|
-
if (!
|
|
122
|
+
const root = [NESTED_EVENTS_DIR, LEGACY_NESTED_EVENTS_DIR].find((base) => containedPath(base, route.eventSink));
|
|
123
|
+
if (!root) throw new Error("Nested event sink is outside the subagent nested event root.");
|
|
124
|
+
if (!containedPath(root, route.controlInbox)) throw new Error("Nested control inbox is outside the subagent nested event root.");
|
|
109
125
|
if (commonRouteRoot(route) !== path.dirname(path.resolve(route.controlInbox))) throw new Error("Nested event sink and control inbox must share one route root.");
|
|
110
126
|
}
|
|
111
127
|
|
|
@@ -158,11 +174,12 @@ export function resolveNestedParentAddressFromEnv(env: NodeJS.ProcessEnv = proce
|
|
|
158
174
|
}
|
|
159
175
|
|
|
160
176
|
export function resolveNestedAsyncDir(rootRunId: string, run: NestedRunSummary): string | undefined {
|
|
161
|
-
if (!run.asyncDir) return undefined;
|
|
177
|
+
if (!run.asyncDir || !isSafeNestedId(rootRunId) || !isSafeNestedId(run.id)) return undefined;
|
|
162
178
|
const resolved = path.resolve(run.asyncDir);
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
179
|
+
return [RUN_STATE_ROOT_DIR, TEMP_ROOT_DIR].some((base) => {
|
|
180
|
+
const root = path.join(base, "nested-subagent-runs");
|
|
181
|
+
return containedPath(root, resolved) && containedPath(path.join(root, rootRunId, run.id), resolved);
|
|
182
|
+
}) ? resolved : undefined;
|
|
166
183
|
}
|
|
167
184
|
|
|
168
185
|
function clampNumber(value: unknown): number | undefined {
|
|
@@ -190,7 +207,7 @@ function sanitizeState(value: unknown, fallback: NestedRunState): NestedRunState
|
|
|
190
207
|
: fallback;
|
|
191
208
|
}
|
|
192
209
|
|
|
193
|
-
function sanitizeStep(input: unknown, depth: number): NestedStepSummary | undefined {
|
|
210
|
+
function sanitizeStep(input: unknown, depth: number, privateOrigin = false): NestedStepSummary | undefined {
|
|
194
211
|
if (!input || typeof input !== "object") return undefined;
|
|
195
212
|
const raw = input as Record<string, unknown>;
|
|
196
213
|
const agent = stringValue(raw.agent, 128);
|
|
@@ -212,22 +229,33 @@ function sanitizeStep(input: unknown, depth: number): NestedStepSummary | undefi
|
|
|
212
229
|
...(clampNumber(raw.startedAt) !== undefined ? { startedAt: clampNumber(raw.startedAt) } : {}),
|
|
213
230
|
...(clampNumber(raw.endedAt) !== undefined ? { endedAt: clampNumber(raw.endedAt) } : {}),
|
|
214
231
|
...(stringValue(raw.error, 1024) ? { error: stringValue(raw.error, 1024) } : {}),
|
|
215
|
-
...(depth < MAX_DEPTH && Array.isArray(raw.children) ? { children: raw.children.map((child) =>
|
|
232
|
+
...(depth < MAX_DEPTH && Array.isArray(raw.children) ? { children: raw.children.map((child) => sanitizeInternalSummary(child, depth + 1, privateOrigin)).filter((child): child is NestedRunSummary => Boolean(child)).slice(0, MAX_CHILDREN) } : {}),
|
|
216
233
|
};
|
|
217
234
|
}
|
|
218
235
|
|
|
219
236
|
export function sanitizeSummary(input: unknown, depth = 0): NestedRunSummary | undefined {
|
|
237
|
+
return sanitizeInternalSummary(input, depth, false);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function sanitizeInternalSummary(input: unknown, depth = 0, privateOrigin = true): NestedRunSummary | undefined {
|
|
220
241
|
if (!input || typeof input !== "object") return undefined;
|
|
221
242
|
const raw = input as Record<string, unknown>;
|
|
222
243
|
if (!isSafeNestedId(raw.id) || !isSafeNestedId(raw.parentRunId)) return undefined;
|
|
244
|
+
let runtime: NestedRunSummary["runtime"];
|
|
245
|
+
try {
|
|
246
|
+
if ("runtime" in raw) runtime = parsePiRuntime(raw.runtime, "nested origin");
|
|
247
|
+
} catch { return undefined; }
|
|
248
|
+
if ("sessionRoot" in raw && (typeof raw.sessionRoot !== "string" || !path.isAbsolute(raw.sessionRoot))) return undefined;
|
|
223
249
|
const pathParts = sanitizeNestedPath(raw.path);
|
|
224
250
|
const steps = Array.isArray(raw.steps)
|
|
225
|
-
? raw.steps.map((step) => sanitizeStep(step, depth + 1)).filter((step): step is NestedStepSummary => Boolean(step)).slice(0, MAX_STEPS)
|
|
251
|
+
? raw.steps.map((step) => sanitizeStep(step, depth + 1, privateOrigin)).filter((step): step is NestedStepSummary => Boolean(step)).slice(0, MAX_STEPS)
|
|
226
252
|
: undefined;
|
|
227
253
|
const totalTokens = sanitizeTokenUsage(raw.totalTokens);
|
|
228
254
|
const totalCost = typeof raw.totalCost === "number" && Number.isFinite(raw.totalCost) ? raw.totalCost : undefined;
|
|
229
255
|
return {
|
|
230
256
|
id: raw.id,
|
|
257
|
+
...(privateOrigin && runtime ? { runtime } : {}),
|
|
258
|
+
...(privateOrigin && typeof raw.sessionRoot === "string" ? { sessionRoot: raw.sessionRoot } : {}),
|
|
231
259
|
parentRunId: raw.parentRunId,
|
|
232
260
|
...(clampNumber(raw.parentStepIndex) !== undefined ? { parentStepIndex: clampNumber(raw.parentStepIndex) } : {}),
|
|
233
261
|
...(stringValue(raw.parentAgent, 128) ? { parentAgent: stringValue(raw.parentAgent, 128) } : {}),
|
|
@@ -260,7 +288,7 @@ export function sanitizeSummary(input: unknown, depth = 0): NestedRunSummary | u
|
|
|
260
288
|
...(clampNumber(raw.lastUpdate) !== undefined ? { lastUpdate: clampNumber(raw.lastUpdate) } : {}),
|
|
261
289
|
...(stringValue(raw.error, 1024) ? { error: stringValue(raw.error, 1024) } : {}),
|
|
262
290
|
...(steps && steps.length > 0 ? { steps } : {}),
|
|
263
|
-
...(depth < MAX_DEPTH && Array.isArray(raw.children) ? { children: raw.children.map((child) =>
|
|
291
|
+
...(depth < MAX_DEPTH && Array.isArray(raw.children) ? { children: raw.children.map((child) => sanitizeInternalSummary(child, depth + 1, privateOrigin)).filter((child): child is NestedRunSummary => Boolean(child)).slice(0, MAX_CHILDREN) } : {}),
|
|
264
292
|
};
|
|
265
293
|
}
|
|
266
294
|
|
|
@@ -279,7 +307,7 @@ function parseRecord(content: string, route: NestedRoute): NestedEventRecord | u
|
|
|
279
307
|
if (!isSafeNestedId(raw.parentRunId)) return undefined;
|
|
280
308
|
const ts = clampNumber(raw.ts);
|
|
281
309
|
if (ts === undefined) return undefined;
|
|
282
|
-
const child =
|
|
310
|
+
const child = sanitizeInternalSummary(raw.child);
|
|
283
311
|
if (!child || child.id === route.rootRunId) return undefined;
|
|
284
312
|
const routedChild: NestedRunSummary = {
|
|
285
313
|
...child,
|
|
@@ -322,7 +350,24 @@ function mergeSummary(existing: NestedRunSummary | undefined, event: NestedEvent
|
|
|
322
350
|
if (incomingUpdate < existingUpdate) return existing;
|
|
323
351
|
if (terminal(existing.state) && !terminal(incoming.state)) return existing;
|
|
324
352
|
if (terminal(existing.state) && terminal(incoming.state) && incomingUpdate === existingUpdate) return existing;
|
|
325
|
-
|
|
353
|
+
const preserveOrigins = (summary: NestedRunSummary): NestedRunSummary => {
|
|
354
|
+
const known = findNestedRun([existing], summary.id);
|
|
355
|
+
return {
|
|
356
|
+
...summary,
|
|
357
|
+
...(known?.runtime && known.sessionRoot ? { runtime: known.runtime, sessionRoot: known.sessionRoot } : {}),
|
|
358
|
+
...(summary.children ? { children: summary.children.map(preserveOrigins) } : {}),
|
|
359
|
+
...(summary.steps ? { steps: summary.steps.map((step) => ({
|
|
360
|
+
...step,
|
|
361
|
+
...(step.children ? { children: step.children.map(preserveOrigins) } : {}),
|
|
362
|
+
})) } : {}),
|
|
363
|
+
};
|
|
364
|
+
};
|
|
365
|
+
return {
|
|
366
|
+
...existing,
|
|
367
|
+
...preserveOrigins(incoming),
|
|
368
|
+
state: incoming.state,
|
|
369
|
+
lastUpdate: Math.max(existingUpdate, incomingUpdate),
|
|
370
|
+
};
|
|
326
371
|
}
|
|
327
372
|
|
|
328
373
|
function attachChild(children: NestedRunSummary[], event: NestedEventRecord): NestedRunSummary[] {
|
|
@@ -367,14 +412,19 @@ export function findNestedRouteForRootId(rootRunId: string): NestedRoute | undef
|
|
|
367
412
|
assertSafeId("rootRunId", rootRunId);
|
|
368
413
|
let entries: string[];
|
|
369
414
|
try {
|
|
370
|
-
entries =
|
|
415
|
+
entries = [NESTED_EVENTS_DIR, LEGACY_NESTED_EVENTS_DIR].flatMap((base) => {
|
|
416
|
+
try { return fs.readdirSync(base).map((entry) => path.join(base, entry)); } catch (error) {
|
|
417
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
418
|
+
throw error;
|
|
419
|
+
}
|
|
420
|
+
});
|
|
371
421
|
} catch (error) {
|
|
372
422
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
373
423
|
throw error;
|
|
374
424
|
}
|
|
375
425
|
for (const entry of entries) {
|
|
376
|
-
if (!entry.startsWith(`${rootRunId}-`)) continue;
|
|
377
|
-
const routeRoot =
|
|
426
|
+
if (!path.basename(entry).startsWith(`${rootRunId}-`)) continue;
|
|
427
|
+
const routeRoot = entry;
|
|
378
428
|
try {
|
|
379
429
|
const metadata = JSON.parse(fs.readFileSync(path.join(routeRoot, ROUTE_FILE), "utf-8")) as { rootRunId?: unknown; capabilityToken?: unknown };
|
|
380
430
|
if (metadata.rootRunId !== rootRunId || typeof metadata.capabilityToken !== "string") continue;
|
|
@@ -393,9 +443,18 @@ export function findNestedRouteForRootId(rootRunId: string): NestedRoute | undef
|
|
|
393
443
|
return undefined;
|
|
394
444
|
}
|
|
395
445
|
|
|
396
|
-
export function
|
|
446
|
+
export function isLegacyNestedRoute(route: NestedRoute): boolean {
|
|
447
|
+
validateRouteShape(route);
|
|
448
|
+
return containedPath(LEGACY_NESTED_EVENTS_DIR, route.eventSink);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export interface NestedProjectionOptions {
|
|
452
|
+
readOnlyLegacy?: boolean;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export function projectNestedRegistryForRoot(rootRunId: string, options: NestedProjectionOptions = {}): NestedRegistry | undefined {
|
|
397
456
|
const route = findNestedRouteForRootId(rootRunId);
|
|
398
|
-
return route ? projectNestedEvents(route) : undefined;
|
|
457
|
+
return route ? projectNestedEvents(route, options) : undefined;
|
|
399
458
|
}
|
|
400
459
|
|
|
401
460
|
export function findNestedRun(children: NestedRunSummary[] | undefined, id: string): NestedRunSummary | undefined {
|
|
@@ -444,14 +503,19 @@ function collectScopedNestedRuns(children: NestedRunSummary[] | undefined, scope
|
|
|
444
503
|
function listNestedRoutes(): NestedRoute[] {
|
|
445
504
|
let entries: string[];
|
|
446
505
|
try {
|
|
447
|
-
entries =
|
|
506
|
+
entries = [NESTED_EVENTS_DIR, LEGACY_NESTED_EVENTS_DIR].flatMap((base) => {
|
|
507
|
+
try { return fs.readdirSync(base).map((entry) => path.join(base, entry)); } catch (error) {
|
|
508
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
509
|
+
throw error;
|
|
510
|
+
}
|
|
511
|
+
});
|
|
448
512
|
} catch (error) {
|
|
449
513
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
450
514
|
throw error;
|
|
451
515
|
}
|
|
452
516
|
const routes: NestedRoute[] = [];
|
|
453
517
|
for (const entry of entries) {
|
|
454
|
-
const routeRoot =
|
|
518
|
+
const routeRoot = entry;
|
|
455
519
|
try {
|
|
456
520
|
const metadata = JSON.parse(fs.readFileSync(path.join(routeRoot, ROUTE_FILE), "utf-8")) as { rootRunId?: unknown; capabilityToken?: unknown };
|
|
457
521
|
if (typeof metadata.rootRunId !== "string" || typeof metadata.capabilityToken !== "string") continue;
|
|
@@ -470,20 +534,29 @@ function listNestedRoutes(): NestedRoute[] {
|
|
|
470
534
|
return routes;
|
|
471
535
|
}
|
|
472
536
|
|
|
473
|
-
export function findNestedRunMatchesById(id: string, options: { prefix?: boolean; scope?: NestedRunResolutionScope } = {}): NestedRunMatch[] {
|
|
537
|
+
export function findNestedRunMatchesById(id: string, options: { prefix?: boolean; scope?: NestedRunResolutionScope } & NestedProjectionOptions = {}): NestedRunMatch[] {
|
|
474
538
|
assertSafeId("id", id);
|
|
475
539
|
const matches: NestedRunMatch[] = [];
|
|
540
|
+
const persistentIds = new Set<string>();
|
|
541
|
+
const authorityId = (route: NestedRoute, run: NestedRunSummary) => JSON.stringify([route.rootRunId, route.capabilityToken, run.id]);
|
|
476
542
|
for (const route of options.scope?.routes ?? listNestedRoutes()) {
|
|
477
543
|
try {
|
|
478
|
-
const registry = projectNestedEvents(route);
|
|
544
|
+
const registry = projectNestedEvents(route, options);
|
|
545
|
+
const persistent = containedPath(NESTED_EVENTS_DIR, route.eventSink);
|
|
546
|
+
if (persistent) {
|
|
547
|
+
// Shadow before descendant filtering so stale legacy scope cannot revive a moved run.
|
|
548
|
+
for (const run of collectNestedRuns(registry.children)) persistentIds.add(authorityId(route, run));
|
|
549
|
+
}
|
|
479
550
|
for (const run of collectScopedNestedRuns(registry.children, options.scope?.descendantOf)) {
|
|
480
|
-
if (options.prefix ? run.id.startsWith(id) : run.id === id)
|
|
551
|
+
if (options.prefix ? run.id.startsWith(id) : run.id === id) {
|
|
552
|
+
matches.push({ rootRunId: route.rootRunId, route, run });
|
|
553
|
+
}
|
|
481
554
|
}
|
|
482
555
|
} catch {
|
|
483
556
|
continue;
|
|
484
557
|
}
|
|
485
558
|
}
|
|
486
|
-
return matches;
|
|
559
|
+
return matches.filter(({ route, run }) => !containedPath(LEGACY_NESTED_EVENTS_DIR, route.eventSink) || !persistentIds.has(authorityId(route, run)));
|
|
487
560
|
}
|
|
488
561
|
|
|
489
562
|
export function findNestedRunById(id: string): { rootRunId: string; run: NestedRunSummary } | undefined {
|
|
@@ -498,7 +571,7 @@ export function readNestedRegistry(route: NestedRoute): NestedRegistry {
|
|
|
498
571
|
return {
|
|
499
572
|
rootRunId: route.rootRunId,
|
|
500
573
|
updatedAt: typeof parsed.updatedAt === "number" ? parsed.updatedAt : 0,
|
|
501
|
-
children: Array.isArray(parsed.children) ? parsed.children.map((child) =>
|
|
574
|
+
children: Array.isArray(parsed.children) ? parsed.children.map((child) => sanitizeInternalSummary(child)).filter((child): child is NestedRunSummary => Boolean(child)) : [],
|
|
502
575
|
processedEvents: Array.isArray(parsed.processedEvents) ? parsed.processedEvents.filter((item): item is string => typeof item === "string") : [],
|
|
503
576
|
};
|
|
504
577
|
} catch (error) {
|
|
@@ -507,7 +580,7 @@ export function readNestedRegistry(route: NestedRoute): NestedRegistry {
|
|
|
507
580
|
}
|
|
508
581
|
}
|
|
509
582
|
|
|
510
|
-
export function projectNestedEvents(route: NestedRoute): NestedRegistry {
|
|
583
|
+
export function projectNestedEvents(route: NestedRoute, options: NestedProjectionOptions = {}): NestedRegistry {
|
|
511
584
|
validateRouteShape(route);
|
|
512
585
|
let registry = readNestedRegistry(route);
|
|
513
586
|
const seen = new Set(registry.processedEvents);
|
|
@@ -542,7 +615,7 @@ export function projectNestedEvents(route: NestedRoute): NestedRegistry {
|
|
|
542
615
|
// Parent projection is the only writer to this sidecar registry. Child and
|
|
543
616
|
// runner processes only create immutable event files, so parent status.json
|
|
544
617
|
// remains owned by the existing runner writer and is never rewritten here.
|
|
545
|
-
writeAtomicJson(registryPath(route), registry);
|
|
618
|
+
if (!options.readOnlyLegacy || !isLegacyNestedRoute(route)) writeAtomicJson(registryPath(route), registry);
|
|
546
619
|
}
|
|
547
620
|
return registry;
|
|
548
621
|
}
|
|
@@ -809,7 +882,7 @@ export function nestedArtifactEnv(rootRunId: string, parentRunId: string): Recor
|
|
|
809
882
|
|
|
810
883
|
export function isTopLevelAsyncDir(asyncDir: string): boolean {
|
|
811
884
|
const resolved = path.resolve(asyncDir);
|
|
812
|
-
return
|
|
885
|
+
return [ASYNC_DIR, LEGACY_ASYNC_DIR].some((root) => containedPath(root, resolved));
|
|
813
886
|
}
|
|
814
887
|
|
|
815
888
|
export function nestedResultsPath(rootRunId: string, id: string): string {
|
|
@@ -6,15 +6,15 @@ export const PI_CODING_AGENT_PACKAGE = "@earendil-works/pi-coding-agent";
|
|
|
6
6
|
|
|
7
7
|
export function findPiPackageRootFromEntry(entryPoint: string): string | undefined {
|
|
8
8
|
let dir = path.dirname(entryPoint);
|
|
9
|
-
while (
|
|
9
|
+
while (true) {
|
|
10
10
|
const packageJsonPath = path.join(dir, "package.json");
|
|
11
11
|
if (fs.existsSync(packageJsonPath)) {
|
|
12
12
|
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) as { name?: unknown };
|
|
13
13
|
if (pkg.name === PI_CODING_AGENT_PACKAGE) return dir;
|
|
14
14
|
}
|
|
15
|
+
if (dir === path.dirname(dir)) return undefined;
|
|
15
16
|
dir = path.dirname(dir);
|
|
16
17
|
}
|
|
17
|
-
return undefined;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export function resolveInstalledPiPackageRoot(): string | undefined {
|
|
@@ -26,90 +26,98 @@ export function resolvePiPackageRoot(): string | undefined {
|
|
|
26
26
|
const entry = process.argv[1];
|
|
27
27
|
return entry ? findPiPackageRootFromEntry(fs.realpathSync(entry)) : undefined;
|
|
28
28
|
} catch {
|
|
29
|
-
//
|
|
29
|
+
// Non-launch package inspection is best-effort.
|
|
30
30
|
return undefined;
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
export interface
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
existsSync?: (filePath: string) => boolean;
|
|
39
|
-
readFileSync?: (filePath: string, encoding: "utf-8") => string;
|
|
40
|
-
resolvePackageJson?: () => string;
|
|
41
|
-
resolvePackageEntry?: () => string;
|
|
42
|
-
piPackageRoot?: string;
|
|
34
|
+
export interface PiRuntimeIdentity {
|
|
35
|
+
readonly nodeExecPath: string;
|
|
36
|
+
readonly piEntryPath: string;
|
|
37
|
+
readonly piPackageRoot: string;
|
|
43
38
|
}
|
|
44
39
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
function isRunnableNodeScript(filePath: string, existsSync: (filePath: string) => boolean): boolean {
|
|
51
|
-
if (!existsSync(filePath)) return false;
|
|
52
|
-
return /\.(?:mjs|cjs|js)$/i.test(filePath);
|
|
40
|
+
export class PiRuntimeLaunchError extends Error {
|
|
41
|
+
constructor(message: string) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = "PiRuntimeLaunchError";
|
|
44
|
+
}
|
|
53
45
|
}
|
|
54
46
|
|
|
55
|
-
function
|
|
56
|
-
|
|
47
|
+
function canonicalPath(value: string | undefined, source: string): string {
|
|
48
|
+
if (!value) throw new PiRuntimeLaunchError(`${source}: runtime path was not recorded`);
|
|
49
|
+
try {
|
|
50
|
+
return fs.realpathSync.native(value);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
throw new PiRuntimeLaunchError(`${source} ${value}: ${String(error)}`);
|
|
53
|
+
}
|
|
57
54
|
}
|
|
58
55
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
56
|
+
function owningPackageRoot(entry: string): string {
|
|
57
|
+
let dir = path.dirname(entry);
|
|
58
|
+
while (true) {
|
|
59
|
+
const manifest = path.join(dir, "package.json");
|
|
60
|
+
if (fs.existsSync(manifest)) {
|
|
61
|
+
try {
|
|
62
|
+
const pkg = JSON.parse(fs.readFileSync(manifest, "utf-8"));
|
|
63
|
+
if (pkg?.name !== PI_CODING_AGENT_PACKAGE) {
|
|
64
|
+
throw new PiRuntimeLaunchError(`${entry}: owning package ${manifest} is not ${PI_CODING_AGENT_PACKAGE}`);
|
|
65
|
+
}
|
|
66
|
+
const bin = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.pi;
|
|
67
|
+
if (typeof bin !== "string" || !bin) {
|
|
68
|
+
throw new PiRuntimeLaunchError(`${manifest}: missing declared Pi bin for ${entry}`);
|
|
69
|
+
}
|
|
70
|
+
if (canonicalPath(path.resolve(dir, bin), "Declared Pi bin") !== entry) {
|
|
71
|
+
throw new PiRuntimeLaunchError(`${entry}: does not match declared Pi bin in ${manifest}`);
|
|
72
|
+
}
|
|
73
|
+
return dir;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (error instanceof PiRuntimeLaunchError) throw error;
|
|
76
|
+
throw new PiRuntimeLaunchError(`${manifest} for ${entry}: ${String(error)}`);
|
|
77
|
+
}
|
|
68
78
|
}
|
|
79
|
+
if (dir === path.dirname(dir)) throw new PiRuntimeLaunchError(`${entry}: no owning ${PI_CODING_AGENT_PACKAGE} package`);
|
|
80
|
+
dir = path.dirname(dir);
|
|
69
81
|
}
|
|
82
|
+
}
|
|
70
83
|
|
|
84
|
+
function checkAvailability(file: string, executable: boolean): void {
|
|
71
85
|
try {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
? findPiPackageRootFromEntry(deps.resolvePackageEntry())
|
|
77
|
-
: resolveInstalledPiPackageRoot();
|
|
78
|
-
if (!packageRoot) throw new Error(`Could not resolve ${PI_CODING_AGENT_PACKAGE} package root`);
|
|
79
|
-
return path.join(packageRoot, "package.json");
|
|
80
|
-
});
|
|
81
|
-
const packageJsonPath = resolvePackageJson();
|
|
82
|
-
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as {
|
|
83
|
-
bin?: string | Record<string, string>;
|
|
84
|
-
};
|
|
85
|
-
const binField = packageJson.bin;
|
|
86
|
-
const binPath = typeof binField === "string"
|
|
87
|
-
? binField
|
|
88
|
-
: binField?.pi ?? Object.values(binField ?? {})[0];
|
|
89
|
-
if (!binPath) return undefined;
|
|
90
|
-
const candidate = path.resolve(path.dirname(packageJsonPath), binPath);
|
|
91
|
-
if (isRunnableNodeScript(candidate, existsSync)) {
|
|
92
|
-
return candidate;
|
|
93
|
-
}
|
|
94
|
-
} catch {
|
|
95
|
-
// Windows CLI resolution is optional; falling back to `pi` lets PATH handle execution.
|
|
96
|
-
return undefined;
|
|
86
|
+
if (!fs.statSync(file).isFile()) throw new Error("not a file");
|
|
87
|
+
fs.accessSync(file, executable ? fs.constants.X_OK : fs.constants.R_OK);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
throw new PiRuntimeLaunchError(`${file}: ${String(error)}`);
|
|
97
90
|
}
|
|
91
|
+
}
|
|
98
92
|
|
|
99
|
-
|
|
93
|
+
export function capturePiRuntime(deps: { execPath?: string; argv1?: string } = {}): PiRuntimeIdentity {
|
|
94
|
+
const nodeExecPath = canonicalPath(deps.execPath ?? process.execPath, "Node executable");
|
|
95
|
+
const piEntryPath = canonicalPath(deps.argv1 ?? process.argv[1], "Pi entry");
|
|
96
|
+
checkAvailability(nodeExecPath, true);
|
|
97
|
+
checkAvailability(piEntryPath, false);
|
|
98
|
+
const piPackageRoot = canonicalPath(owningPackageRoot(piEntryPath), "Pi package root");
|
|
99
|
+
return { nodeExecPath, piEntryPath, piPackageRoot };
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
export function
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
};
|
|
102
|
+
export function parsePiRuntime(value: unknown, source: string): PiRuntimeIdentity {
|
|
103
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
104
|
+
throw new PiRuntimeLaunchError(`${source}: original runtime was not recorded; a new run is required`);
|
|
105
|
+
}
|
|
106
|
+
const record = value as Record<string, unknown>;
|
|
107
|
+
for (const field of ["nodeExecPath", "piEntryPath", "piPackageRoot"] as const) {
|
|
108
|
+
if (typeof record[field] !== "string" || !record[field] || !path.isAbsolute(record[field])) {
|
|
109
|
+
throw new PiRuntimeLaunchError(`${source}: invalid runtime ${field} ${String(record[field])}; a new run is required`);
|
|
111
110
|
}
|
|
112
111
|
}
|
|
112
|
+
return { nodeExecPath: record.nodeExecPath as string, piEntryPath: record.piEntryPath as string, piPackageRoot: record.piPackageRoot as string };
|
|
113
|
+
}
|
|
113
114
|
|
|
114
|
-
|
|
115
|
+
export function getPiSpawnCommand(args: string[], runtime: PiRuntimeIdentity): { command: string; args: string[] } {
|
|
116
|
+
checkAvailability(runtime.nodeExecPath, true);
|
|
117
|
+
checkAvailability(runtime.piEntryPath, false);
|
|
118
|
+
const root = owningPackageRoot(runtime.piEntryPath);
|
|
119
|
+
if (root !== runtime.piPackageRoot) {
|
|
120
|
+
throw new PiRuntimeLaunchError(`${runtime.piEntryPath}: owning package ${root} does not match ${runtime.piPackageRoot}`);
|
|
121
|
+
}
|
|
122
|
+
return { command: runtime.nodeExecPath, args: [runtime.piEntryPath, ...args] };
|
|
115
123
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as os from "node:os";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
export function getAgentDir(): string {
|
|
5
|
+
const configured = process.env.PI_CODING_AGENT_DIR;
|
|
6
|
+
if (configured === "~") return os.homedir();
|
|
7
|
+
if (configured?.startsWith("~/")) return path.join(os.homedir(), configured.slice(2));
|
|
8
|
+
return configured || path.join(os.homedir(), ".pi", "agent");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const PINNED_AGENT_DIR = path.resolve(getAgentDir());
|
|
12
|
+
export const PINNED_AGENT_DIR_OVERRIDE = process.env.PI_CODING_AGENT_DIR ? PINNED_AGENT_DIR : undefined;
|
|
@@ -9,7 +9,18 @@ export function writeAtomicJson(filePath: string, payload: object): void {
|
|
|
9
9
|
);
|
|
10
10
|
try {
|
|
11
11
|
fs.writeFileSync(tempPath, JSON.stringify(payload, null, 2), "utf-8");
|
|
12
|
-
|
|
12
|
+
for (let retries = 0; ; retries++) {
|
|
13
|
+
try {
|
|
14
|
+
fs.renameSync(tempPath, filePath);
|
|
15
|
+
break;
|
|
16
|
+
} catch (error) {
|
|
17
|
+
const code = (error as NodeJS.ErrnoException | null)?.code;
|
|
18
|
+
if (process.platform !== "win32" || !["EPERM", "EACCES", "EBUSY"].includes(code ?? "") || retries >= 5) {
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
13
24
|
} finally {
|
|
14
25
|
fs.rmSync(tempPath, { force: true });
|
|
15
26
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
2
3
|
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
3
4
|
|
|
4
5
|
type SubagentExecutionContext = "fresh" | "fork";
|
|
@@ -11,6 +12,7 @@ interface ForkableSessionManager {
|
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
interface ForkContextResolverOptions {
|
|
15
|
+
sessionDirForIndex?: (index: number) => string;
|
|
14
16
|
openSession?: (path: string, sessionDir?: string) => { createBranchedSession(leafId: string): string | undefined };
|
|
15
17
|
}
|
|
16
18
|
|
|
@@ -57,7 +59,10 @@ export function createForkContextResolver(
|
|
|
57
59
|
if (!fs.existsSync(parentSessionFile)) {
|
|
58
60
|
throw new Error(`Parent session file does not exist: ${parentSessionFile}. Pi has not persisted enough history to fork yet.`);
|
|
59
61
|
}
|
|
60
|
-
const
|
|
62
|
+
const destination = options.sessionDirForIndex?.(index);
|
|
63
|
+
if (destination) fs.mkdirSync(destination, { recursive: true });
|
|
64
|
+
const assignedDir = destination ? fs.realpathSync.native(destination) : undefined;
|
|
65
|
+
const sourceManager = openSession(parentSessionFile, destination ?? sessionDir);
|
|
61
66
|
const sessionFile = sourceManager.createBranchedSession(leafId);
|
|
62
67
|
if (!sessionFile) {
|
|
63
68
|
throw new Error("Session manager did not return a forked session file.");
|
|
@@ -65,8 +70,15 @@ export function createForkContextResolver(
|
|
|
65
70
|
if (!fs.existsSync(sessionFile)) {
|
|
66
71
|
throw new Error(`Session manager returned a forked session file that does not exist: ${sessionFile}`);
|
|
67
72
|
}
|
|
68
|
-
|
|
69
|
-
|
|
73
|
+
if (assignedDir) {
|
|
74
|
+
const relative = path.relative(assignedDir, fs.realpathSync.native(sessionFile));
|
|
75
|
+
if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
76
|
+
throw new Error(`Forked session file is outside assigned session directory: ${sessionFile}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const resolvedFile = assignedDir ? fs.realpathSync.native(sessionFile) : sessionFile;
|
|
80
|
+
cachedSessionFiles.set(index, resolvedFile);
|
|
81
|
+
return resolvedFile;
|
|
70
82
|
} catch (error) {
|
|
71
83
|
const cause = error instanceof Error ? error : new Error(String(error));
|
|
72
84
|
throw new Error(`Failed to create forked subagent session: ${cause.message}`, { cause });
|