pi-cohort 7.1.2 → 7.2.1
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 +12 -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 +10 -45
- 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/notify.ts +2 -13
- 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
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
|
-
import
|
|
6
|
+
import { PINNED_AGENT_DIR_OVERRIDE } from "../../shared/agent-dir.ts";
|
|
7
|
+
import fs from "node:fs";
|
|
7
8
|
import * as os from "node:os";
|
|
8
9
|
import * as path from "node:path";
|
|
9
10
|
import { fileURLToPath } from "node:url";
|
|
@@ -12,8 +13,9 @@ import type { AgentConfig } from "../../agents/agents.ts";
|
|
|
12
13
|
import { applyThinkingSuffix } from "../shared/pi-args.ts";
|
|
13
14
|
import { injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveParallelTaskOutputPath, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
|
|
14
15
|
import { buildChainInstructions, isDynamicParallelStep, isParallelStep, resolveStepBehavior, suppressProgressForReadOnlyTask, writeInitialProgressFile, type ChainStep, type ResolvedStepBehavior, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
|
|
15
|
-
import
|
|
16
|
-
import {
|
|
16
|
+
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
17
|
+
import { isDynamicRunnerGroup, isParallelGroup, type RunnerStep } from "../shared/parallel-utils.ts";
|
|
18
|
+
import { getPiSpawnCommand, PiRuntimeLaunchError, type PiRuntimeIdentity } from "../shared/pi-spawn.ts";
|
|
17
19
|
import { ensureJitiCliPath, resolveHostModuleAliases } from "../shared/jiti-cli.ts";
|
|
18
20
|
export { createJitiCliResolver } from "../shared/jiti-cli.ts";
|
|
19
21
|
import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../../agents/skills.ts";
|
|
@@ -39,14 +41,14 @@ import {
|
|
|
39
41
|
RESULTS_DIR,
|
|
40
42
|
SUBAGENT_ASYNC_STARTED_EVENT,
|
|
41
43
|
TEMP_ROOT_DIR,
|
|
44
|
+
RUN_STATE_ROOT_DIR,
|
|
42
45
|
getAsyncConfigPath,
|
|
43
46
|
resolveChildMaxSubagentDepth,
|
|
44
47
|
} from "../../shared/types.ts";
|
|
45
48
|
import { nestedResultsPath, resolveInheritedNestedRouteFromEnv, resolveNestedParentAddressFromEnv, writeNestedEvent } from "../shared/nested-events.ts";
|
|
46
49
|
|
|
47
|
-
const piPackageRoot = resolvePiPackageRoot();
|
|
48
|
-
|
|
49
50
|
interface AsyncExecutionContext {
|
|
51
|
+
runtime: PiRuntimeIdentity;
|
|
50
52
|
pi: ExtensionAPI;
|
|
51
53
|
cwd: string;
|
|
52
54
|
currentSessionId: string;
|
|
@@ -73,6 +75,7 @@ interface AsyncChainParams {
|
|
|
73
75
|
sessionRoot?: string;
|
|
74
76
|
chainSkills?: string[];
|
|
75
77
|
sessionFilesByFlatIndex?: (string | undefined)[];
|
|
78
|
+
originSessionRoot?: string;
|
|
76
79
|
dynamicFanoutMaxItems?: number;
|
|
77
80
|
maxSubagentDepth: number;
|
|
78
81
|
worktreeSetupHook?: string;
|
|
@@ -103,6 +106,7 @@ interface AsyncSingleParams {
|
|
|
103
106
|
shareEnabled: boolean;
|
|
104
107
|
sessionRoot?: string;
|
|
105
108
|
sessionFile?: string;
|
|
109
|
+
originSessionRoot?: string;
|
|
106
110
|
skills?: string[];
|
|
107
111
|
output?: string | boolean;
|
|
108
112
|
outputMode?: "inline" | "file-only";
|
|
@@ -136,8 +140,8 @@ export function formatAsyncStartedMessage(headline: string): string {
|
|
|
136
140
|
/**
|
|
137
141
|
* Check if jiti is available for async execution
|
|
138
142
|
*/
|
|
139
|
-
export function isAsyncAvailable(): boolean {
|
|
140
|
-
return ensureJitiCliPath() !== undefined;
|
|
143
|
+
export function isAsyncAvailable(runtime: PiRuntimeIdentity): boolean {
|
|
144
|
+
return ensureJitiCliPath(runtime.piPackageRoot) !== undefined;
|
|
141
145
|
}
|
|
142
146
|
|
|
143
147
|
/**
|
|
@@ -184,18 +188,19 @@ export function spawnDetachedWithLog(command: string, args: string[], cwd: strin
|
|
|
184
188
|
* Spawn the async runner process
|
|
185
189
|
*/
|
|
186
190
|
export function spawnRunner(
|
|
187
|
-
cfg:
|
|
191
|
+
cfg: { runtime: PiRuntimeIdentity },
|
|
188
192
|
suffix: string,
|
|
189
193
|
cwd: string,
|
|
190
194
|
asyncDir: string,
|
|
191
|
-
deps: { ensureJiti?: () => string | undefined; spawnImpl?: typeof spawn; hostAliases?: () => Record<string, string> } = {},
|
|
195
|
+
deps: { ensureJiti?: (root: string) => string | undefined; spawnImpl?: typeof spawn; hostAliases?: (root: string) => Record<string, string> } = {},
|
|
192
196
|
): { pid?: number; error?: string } {
|
|
193
197
|
const ensureJiti = deps.ensureJiti ?? ensureJitiCliPath;
|
|
194
|
-
const jitiCliPath = ensureJiti();
|
|
198
|
+
const jitiCliPath = ensureJiti(cfg.runtime.piPackageRoot);
|
|
195
199
|
if (!jitiCliPath) {
|
|
196
|
-
|
|
200
|
+
throw new PiRuntimeLaunchError(`Cannot resolve jiti from selected Pi installation ${cfg.runtime.piPackageRoot}`);
|
|
197
201
|
}
|
|
198
202
|
|
|
203
|
+
getPiSpawnCommand([], cfg.runtime);
|
|
199
204
|
try {
|
|
200
205
|
const cwdStats = fs.statSync(cwd);
|
|
201
206
|
if (!cwdStats.isDirectory()) {
|
|
@@ -210,12 +215,122 @@ export function spawnRunner(
|
|
|
210
215
|
fs.writeFileSync(cfgPath, JSON.stringify(cfg));
|
|
211
216
|
const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
|
|
212
217
|
|
|
213
|
-
const aliases = (deps.hostAliases ?? resolveHostModuleAliases)();
|
|
218
|
+
const aliases = (deps.hostAliases ?? resolveHostModuleAliases)(cfg.runtime.piPackageRoot);
|
|
214
219
|
const env = Object.keys(aliases).length > 0
|
|
215
220
|
? { ...process.env, JITI_ALIAS: JSON.stringify(aliases) }
|
|
216
|
-
: process.env;
|
|
221
|
+
: { ...process.env };
|
|
222
|
+
if (PINNED_AGENT_DIR_OVERRIDE) env.PI_CODING_AGENT_DIR = PINNED_AGENT_DIR_OVERRIDE;
|
|
223
|
+
|
|
224
|
+
return spawnDetachedWithLog(cfg.runtime.nodeExecPath, [jitiCliPath, runner, cfgPath], cwd, asyncDir, deps.spawnImpl ?? spawn, env);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
type NestedStartWriter = (...args: Parameters<typeof writeNestedEvent>) => void | boolean;
|
|
217
228
|
|
|
218
|
-
|
|
229
|
+
function publishAndSpawnRunner(
|
|
230
|
+
cfg: { runtime: PiRuntimeIdentity; id: string; steps: RunnerStep[]; sessionId: string; sessionDir: string; resultMode: SubagentRunMode; [key: string]: unknown },
|
|
231
|
+
suffix: string,
|
|
232
|
+
cwd: string,
|
|
233
|
+
asyncDir: string,
|
|
234
|
+
origin: { route?: NestedRouteInfo; address?: ReturnType<typeof resolveNestedParentAddressFromEnv>; sessionFile?: string; sessionFiles?: string[]; sessionRoot?: string },
|
|
235
|
+
writeStart: NestedStartWriter,
|
|
236
|
+
): { pid?: number; error?: string } {
|
|
237
|
+
const parallelGroups: Array<{ start: number; count: number; stepIndex: number }> = [];
|
|
238
|
+
const steps: object[] = [];
|
|
239
|
+
for (const [stepIndex, step] of cfg.steps.entries()) {
|
|
240
|
+
const leaves = isParallelGroup(step) ? step.parallel : isDynamicRunnerGroup(step) ? [step.parallel] : [step];
|
|
241
|
+
if (isParallelGroup(step) || isDynamicRunnerGroup(step)) parallelGroups.push({ start: steps.length, count: leaves.length, stepIndex });
|
|
242
|
+
for (const leaf of leaves) {
|
|
243
|
+
steps.push({
|
|
244
|
+
agent: isDynamicRunnerGroup(step) ? `expand:${leaf.agent}` : leaf.agent,
|
|
245
|
+
phase: isDynamicRunnerGroup(step) ? step.phase ?? leaf.phase : leaf.phase,
|
|
246
|
+
label: isDynamicRunnerGroup(step) ? step.label ?? leaf.label ?? `Dynamic fanout (${step.collect.as})` : leaf.label,
|
|
247
|
+
outputName: isDynamicRunnerGroup(step) ? step.collect.as : leaf.outputName,
|
|
248
|
+
structured: isDynamicRunnerGroup(step) ? Boolean(step.collect.outputSchema) : leaf.structured,
|
|
249
|
+
status: "pending",
|
|
250
|
+
...(isDynamicRunnerGroup(step) ? {} : {
|
|
251
|
+
sessionFile: leaf.sessionFile,
|
|
252
|
+
skills: leaf.skills,
|
|
253
|
+
model: leaf.model,
|
|
254
|
+
thinking: leaf.thinking,
|
|
255
|
+
attemptedModels: leaf.modelCandidates && leaf.modelCandidates.length > 0 ? leaf.modelCandidates : leaf.model ? [leaf.model] : undefined,
|
|
256
|
+
}),
|
|
257
|
+
recentTools: [],
|
|
258
|
+
recentOutput: [],
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
let preparationError: unknown;
|
|
263
|
+
try {
|
|
264
|
+
fs.mkdirSync(cfg.sessionDir, { recursive: true });
|
|
265
|
+
cfg.sessionDir = fs.realpathSync.native(cfg.sessionDir);
|
|
266
|
+
} catch (error) {
|
|
267
|
+
preparationError = error;
|
|
268
|
+
}
|
|
269
|
+
const statusPath = path.join(asyncDir, "status.json");
|
|
270
|
+
const now = Date.now();
|
|
271
|
+
const status = {
|
|
272
|
+
runId: cfg.id, runtime: cfg.runtime, sessionId: cfg.sessionId,
|
|
273
|
+
state: "queued", mode: cfg.resultMode, cwd, currentStep: 0, chainStepCount: cfg.steps.length,
|
|
274
|
+
startedAt: now, lastUpdate: now, lastActivityAt: now, outputFile: path.join(asyncDir, "output-0.log"), steps, parallelGroups, workflowGraph: cfg.workflowGraph,
|
|
275
|
+
artifactsDir: cfg.artifactsDir, sessionDir: cfg.sessionDir,
|
|
276
|
+
};
|
|
277
|
+
writeAtomicJson(statusPath, status);
|
|
278
|
+
try {
|
|
279
|
+
if (preparationError) throw preparationError;
|
|
280
|
+
let sessionRoot = cfg.sessionDir;
|
|
281
|
+
if (origin.sessionRoot !== undefined) {
|
|
282
|
+
if (!origin.sessionFile && !origin.sessionFiles?.length) throw new Error("Origin session root requires a reused session file.");
|
|
283
|
+
if (!path.isAbsolute(origin.sessionRoot)) throw new Error("Origin session root must be absolute.");
|
|
284
|
+
let stat: fs.Stats;
|
|
285
|
+
try {
|
|
286
|
+
stat = fs.statSync(origin.sessionRoot);
|
|
287
|
+
} catch {
|
|
288
|
+
throw new Error("Origin session root must exist.");
|
|
289
|
+
}
|
|
290
|
+
if (!stat.isDirectory()) throw new Error("Origin session root must be a directory.");
|
|
291
|
+
sessionRoot = fs.realpathSync.native(origin.sessionRoot);
|
|
292
|
+
if (sessionRoot !== origin.sessionRoot) throw new Error("Origin session root must be canonical and cannot be retargeted.");
|
|
293
|
+
}
|
|
294
|
+
const effectiveFiles = origin.sessionFiles ?? (origin.sessionFile ? [origin.sessionFile] : []);
|
|
295
|
+
if (origin.sessionRoot) {
|
|
296
|
+
const contains = (candidate: string) => {
|
|
297
|
+
const relative = path.relative(sessionRoot, candidate);
|
|
298
|
+
return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
299
|
+
};
|
|
300
|
+
if (origin.sessionFiles && !contains(cfg.sessionDir)) throw new Error("Runner session directory is outside origin session root.");
|
|
301
|
+
for (const file of effectiveFiles) {
|
|
302
|
+
if (!fs.existsSync(file)) throw new Error("Reused session file must be an existing regular .jsonl file.");
|
|
303
|
+
const stat = fs.lstatSync(file);
|
|
304
|
+
if (!stat.isFile() || stat.isSymbolicLink() || path.extname(file) !== ".jsonl") throw new Error("Reused session file must be a regular .jsonl file.");
|
|
305
|
+
if (!contains(fs.realpathSync.native(file))) throw new Error("Reused session file is outside origin session root.");
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (origin.route && origin.address) {
|
|
309
|
+
if (effectiveFiles.some(file => fs.existsSync(file)) && !origin.sessionRoot) throw new Error("Origin session root is required for a reused nested session.");
|
|
310
|
+
const firstLeaf = isParallelGroup(cfg.steps[0]) ? cfg.steps[0].parallel[0] : isDynamicRunnerGroup(cfg.steps[0]) ? cfg.steps[0].parallel : cfg.steps[0];
|
|
311
|
+
const agents = cfg.steps.flatMap(step => isParallelGroup(step) ? step.parallel.map(leaf => leaf.agent) : isDynamicRunnerGroup(step) ? [step.parallel.agent] : [step.agent]);
|
|
312
|
+
try {
|
|
313
|
+
if (writeStart(origin.route, {
|
|
314
|
+
type: "subagent.nested.started", ts: now,
|
|
315
|
+
parentRunId: origin.address.parentRunId, parentStepIndex: origin.address.parentStepIndex,
|
|
316
|
+
child: {
|
|
317
|
+
id: cfg.id, ...origin.address, asyncDir, sessionId: cfg.sessionId, capabilityToken: origin.route.capabilityToken,
|
|
318
|
+
runtime: cfg.runtime, sessionRoot, mode: cfg.resultMode, state: "queued",
|
|
319
|
+
agent: firstLeaf?.agent, agents, chainStepCount: cfg.steps.length, parallelGroups,
|
|
320
|
+
startedAt: now, lastUpdate: now,
|
|
321
|
+
},
|
|
322
|
+
}) === false) throw new Error("Nested origin stamp was refused.");
|
|
323
|
+
} catch (error) {
|
|
324
|
+
throw new Error(`Failed to stamp nested origin: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const result = spawnRunner(cfg, suffix, cwd, asyncDir);
|
|
328
|
+
if (result.error) throw new Error(result.error);
|
|
329
|
+
return result;
|
|
330
|
+
} catch (error) {
|
|
331
|
+
writeAtomicJson(statusPath, { ...status, state: "failed", error: error instanceof Error ? error.message : String(error), lastUpdate: Date.now() });
|
|
332
|
+
throw error;
|
|
333
|
+
}
|
|
219
334
|
}
|
|
220
335
|
|
|
221
336
|
/**
|
|
@@ -245,6 +360,7 @@ class AsyncStartValidationError extends Error {}
|
|
|
245
360
|
export function executeAsyncChain(
|
|
246
361
|
id: string,
|
|
247
362
|
params: AsyncChainParams,
|
|
363
|
+
writeStart: NestedStartWriter = writeNestedEvent,
|
|
248
364
|
): AsyncExecutionResult {
|
|
249
365
|
const {
|
|
250
366
|
chain,
|
|
@@ -304,10 +420,11 @@ export function executeAsyncChain(
|
|
|
304
420
|
const inheritedNestedRoute = resolveInheritedNestedRouteFromEnv();
|
|
305
421
|
const nestedAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
|
|
306
422
|
const asyncDir = inheritedNestedRoute
|
|
307
|
-
? path.join(
|
|
423
|
+
? path.join(RUN_STATE_ROOT_DIR, "nested-subagent-runs", inheritedNestedRoute.rootRunId, id)
|
|
308
424
|
: path.join(ASYNC_DIR, id);
|
|
309
425
|
try {
|
|
310
|
-
fs.mkdirSync(asyncDir, { recursive: true });
|
|
426
|
+
fs.mkdirSync(path.dirname(asyncDir), { recursive: true });
|
|
427
|
+
fs.mkdirSync(asyncDir);
|
|
311
428
|
} catch (error) {
|
|
312
429
|
const message = error instanceof Error ? error.message : String(error);
|
|
313
430
|
return {
|
|
@@ -486,7 +603,7 @@ export function executeAsyncChain(
|
|
|
486
603
|
|
|
487
604
|
let spawnResult: { pid?: number; error?: string } = {};
|
|
488
605
|
try {
|
|
489
|
-
spawnResult =
|
|
606
|
+
spawnResult = publishAndSpawnRunner(
|
|
490
607
|
{
|
|
491
608
|
id,
|
|
492
609
|
steps,
|
|
@@ -497,11 +614,10 @@ export function executeAsyncChain(
|
|
|
497
614
|
artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
|
|
498
615
|
artifactConfig,
|
|
499
616
|
share: shareEnabled,
|
|
500
|
-
sessionDir: sessionRoot
|
|
617
|
+
sessionDir: path.join(sessionRoot ?? path.join(RUN_STATE_ROOT_DIR, "sessions"), `async-${id}`),
|
|
501
618
|
asyncDir,
|
|
502
619
|
sessionId: ctx.currentSessionId,
|
|
503
|
-
|
|
504
|
-
piArgv1: process.argv[1],
|
|
620
|
+
runtime: ctx.runtime,
|
|
505
621
|
forwardedFlags,
|
|
506
622
|
worktreeSetupHook,
|
|
507
623
|
worktreeSetupHookTimeoutMs,
|
|
@@ -521,6 +637,15 @@ export function executeAsyncChain(
|
|
|
521
637
|
id,
|
|
522
638
|
runnerCwd,
|
|
523
639
|
asyncDir,
|
|
640
|
+
{
|
|
641
|
+
route: inheritedNestedRoute,
|
|
642
|
+
address: nestedAddress,
|
|
643
|
+
sessionFiles: steps
|
|
644
|
+
.flatMap(step => isParallelGroup(step) ? step.parallel : isDynamicRunnerGroup(step) ? [step.parallel] : [step])
|
|
645
|
+
.flatMap(step => step.sessionFile ? [step.sessionFile] : []),
|
|
646
|
+
sessionRoot: params.originSessionRoot,
|
|
647
|
+
},
|
|
648
|
+
writeStart,
|
|
524
649
|
);
|
|
525
650
|
} catch (error) {
|
|
526
651
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -556,37 +681,6 @@ export function executeAsyncChain(
|
|
|
556
681
|
flatStepStart++;
|
|
557
682
|
}
|
|
558
683
|
}
|
|
559
|
-
if (inheritedNestedRoute && nestedAddress) {
|
|
560
|
-
const now = Date.now();
|
|
561
|
-
try {
|
|
562
|
-
writeNestedEvent(inheritedNestedRoute, {
|
|
563
|
-
type: "subagent.nested.started",
|
|
564
|
-
ts: now,
|
|
565
|
-
parentRunId: nestedAddress.parentRunId,
|
|
566
|
-
parentStepIndex: nestedAddress.parentStepIndex,
|
|
567
|
-
child: {
|
|
568
|
-
id,
|
|
569
|
-
parentRunId: nestedAddress.parentRunId,
|
|
570
|
-
parentStepIndex: nestedAddress.parentStepIndex,
|
|
571
|
-
depth: nestedAddress.depth,
|
|
572
|
-
path: nestedAddress.path,
|
|
573
|
-
asyncDir,
|
|
574
|
-
pid: spawnResult.pid,
|
|
575
|
-
ownerState: "live",
|
|
576
|
-
mode: resultMode,
|
|
577
|
-
state: "running",
|
|
578
|
-
agent: firstAgents[0],
|
|
579
|
-
agents: flatAgents,
|
|
580
|
-
chainStepCount: chain.length,
|
|
581
|
-
parallelGroups,
|
|
582
|
-
startedAt: now,
|
|
583
|
-
lastUpdate: now,
|
|
584
|
-
},
|
|
585
|
-
});
|
|
586
|
-
} catch (error) {
|
|
587
|
-
console.error("Failed to emit nested async start event:", error);
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
684
|
ctx.pi.events.emit(SUBAGENT_ASYNC_STARTED_EVENT, {
|
|
591
685
|
id,
|
|
592
686
|
pid: spawnResult.pid,
|
|
@@ -629,6 +723,7 @@ export function executeAsyncChain(
|
|
|
629
723
|
export function executeAsyncSingle(
|
|
630
724
|
id: string,
|
|
631
725
|
params: AsyncSingleParams,
|
|
726
|
+
writeStart: NestedStartWriter = writeNestedEvent,
|
|
632
727
|
): AsyncExecutionResult {
|
|
633
728
|
const {
|
|
634
729
|
agent,
|
|
@@ -663,10 +758,11 @@ export function executeAsyncSingle(
|
|
|
663
758
|
const inheritedNestedRoute = resolveInheritedNestedRouteFromEnv();
|
|
664
759
|
const nestedAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
|
|
665
760
|
const asyncDir = inheritedNestedRoute
|
|
666
|
-
? path.join(
|
|
761
|
+
? path.join(RUN_STATE_ROOT_DIR, "nested-subagent-runs", inheritedNestedRoute.rootRunId, id)
|
|
667
762
|
: path.join(ASYNC_DIR, id);
|
|
668
763
|
try {
|
|
669
|
-
fs.mkdirSync(asyncDir, { recursive: true });
|
|
764
|
+
fs.mkdirSync(path.dirname(asyncDir), { recursive: true });
|
|
765
|
+
fs.mkdirSync(asyncDir);
|
|
670
766
|
} catch (error) {
|
|
671
767
|
const message = error instanceof Error ? error.message : String(error);
|
|
672
768
|
return {
|
|
@@ -687,7 +783,7 @@ export function executeAsyncSingle(
|
|
|
687
783
|
const model = applyThinkingSuffix(resolveModelCandidate(primaryRaw, availableModels, ctx.currentModelProvider), thinking);
|
|
688
784
|
let spawnResult: { pid?: number; error?: string } = {};
|
|
689
785
|
try {
|
|
690
|
-
spawnResult =
|
|
786
|
+
spawnResult = publishAndSpawnRunner(
|
|
691
787
|
{
|
|
692
788
|
id,
|
|
693
789
|
steps: [
|
|
@@ -728,11 +824,10 @@ export function executeAsyncSingle(
|
|
|
728
824
|
artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
|
|
729
825
|
artifactConfig,
|
|
730
826
|
share: shareEnabled,
|
|
731
|
-
sessionDir: sessionRoot
|
|
827
|
+
sessionDir: path.join(sessionRoot ?? path.join(RUN_STATE_ROOT_DIR, "sessions"), `async-${id}`),
|
|
732
828
|
asyncDir,
|
|
733
829
|
sessionId: ctx.currentSessionId,
|
|
734
|
-
|
|
735
|
-
piArgv1: process.argv[1],
|
|
830
|
+
runtime: ctx.runtime,
|
|
736
831
|
forwardedFlags,
|
|
737
832
|
worktreeSetupHook,
|
|
738
833
|
worktreeSetupHookTimeoutMs,
|
|
@@ -750,6 +845,8 @@ export function executeAsyncSingle(
|
|
|
750
845
|
id,
|
|
751
846
|
runnerCwd,
|
|
752
847
|
asyncDir,
|
|
848
|
+
{ route: inheritedNestedRoute, address: nestedAddress, sessionFile, sessionRoot: params.originSessionRoot },
|
|
849
|
+
writeStart,
|
|
753
850
|
);
|
|
754
851
|
} catch (error) {
|
|
755
852
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -761,36 +858,6 @@ export function executeAsyncSingle(
|
|
|
761
858
|
}
|
|
762
859
|
|
|
763
860
|
if (spawnResult.pid) {
|
|
764
|
-
if (inheritedNestedRoute && nestedAddress) {
|
|
765
|
-
const now = Date.now();
|
|
766
|
-
try {
|
|
767
|
-
writeNestedEvent(inheritedNestedRoute, {
|
|
768
|
-
type: "subagent.nested.started",
|
|
769
|
-
ts: now,
|
|
770
|
-
parentRunId: nestedAddress.parentRunId,
|
|
771
|
-
parentStepIndex: nestedAddress.parentStepIndex,
|
|
772
|
-
child: {
|
|
773
|
-
id,
|
|
774
|
-
parentRunId: nestedAddress.parentRunId,
|
|
775
|
-
parentStepIndex: nestedAddress.parentStepIndex,
|
|
776
|
-
depth: nestedAddress.depth,
|
|
777
|
-
path: nestedAddress.path,
|
|
778
|
-
asyncDir,
|
|
779
|
-
pid: spawnResult.pid,
|
|
780
|
-
ownerState: "live",
|
|
781
|
-
mode: "single",
|
|
782
|
-
state: "running",
|
|
783
|
-
agent,
|
|
784
|
-
agents: [agent],
|
|
785
|
-
chainStepCount: 1,
|
|
786
|
-
startedAt: now,
|
|
787
|
-
lastUpdate: now,
|
|
788
|
-
},
|
|
789
|
-
});
|
|
790
|
-
} catch (error) {
|
|
791
|
-
console.error("Failed to emit nested async start event:", error);
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
861
|
ctx.pi.events.emit(SUBAGENT_ASYNC_STARTED_EVENT, {
|
|
795
862
|
id,
|
|
796
863
|
pid: spawnResult.pid,
|
|
@@ -140,7 +140,10 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
140
140
|
try {
|
|
141
141
|
emitNewControlEvents(job);
|
|
142
142
|
reconcileNestedDescendants();
|
|
143
|
-
const
|
|
143
|
+
const stored = readStatus(job.asyncDir);
|
|
144
|
+
const reconciliation = stored && stored.state !== "running" && stored.state !== "queued"
|
|
145
|
+
? { status: stored }
|
|
146
|
+
: reconcileAsyncRun(job.asyncDir, {
|
|
144
147
|
resultsDir,
|
|
145
148
|
kill: options.kill,
|
|
146
149
|
now: options.now,
|
|
@@ -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
|
}
|