pi-subagents 0.46.0 → 0.47.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 +37 -0
- package/agents/reviewer.md +3 -4
- package/docs/agents.md +1 -1
- package/docs/configuration.md +14 -6
- package/docs/extension-api.md +1 -1
- package/docs/missions.md +5 -3
- package/docs/models.md +3 -1
- package/docs/observability.md +3 -3
- package/docs/tool-reference.md +2 -2
- package/docs/watchdog.md +1 -1
- package/package.json +1 -1
- package/skills/pi-subagents/references/execution-controls.md +4 -4
- package/skills/pi-subagents/references/management-authoring-rpc.md +1 -1
- package/src/extension/config.ts +13 -0
- package/src/extension/fanout-child.ts +5 -4
- package/src/extension/index.ts +39 -14
- package/src/extension/rpc.ts +7 -6
- package/src/extension/schemas.ts +27 -7
- package/src/extension/tool-description.ts +26 -8
- package/src/inspectors/herdr/project-panes.ts +2 -1
- package/src/intercom/native-supervisor-channel.ts +102 -4
- package/src/missions/store.ts +2 -1
- package/src/missions/workflow-state.ts +19 -13
- package/src/runs/background/active-run-index.ts +42 -0
- package/src/runs/background/async-execution.ts +10 -5
- package/src/runs/background/async-job-tracker.ts +293 -133
- package/src/runs/background/async-resume.ts +19 -3
- package/src/runs/background/async-status.ts +33 -5
- package/src/runs/background/control-channel.ts +91 -17
- package/src/runs/background/result-watcher.ts +102 -7
- package/src/runs/background/run-status.ts +18 -0
- package/src/runs/background/scheduled-runs.ts +25 -1
- package/src/runs/background/stale-run-reconciler.ts +12 -24
- package/src/runs/background/subagent-runner.ts +68 -10
- package/src/runs/foreground/async-dismiss-action.ts +85 -0
- package/src/runs/foreground/async-steering-action.ts +6 -7
- package/src/runs/foreground/chain-execution.ts +3 -0
- package/src/runs/foreground/execution.ts +3 -0
- package/src/runs/foreground/subagent-executor.ts +129 -20
- package/src/runs/foreground/workflow-foreground-steering.ts +187 -0
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/model-fallback.ts +8 -4
- package/src/runs/shared/model-scope.ts +12 -2
- package/src/runs/shared/parallel-utils.ts +1 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +24 -5
- package/src/runs/shared/worktree.ts +3 -2
- package/src/shared/artifacts.ts +15 -15
- package/src/shared/display-text.ts +100 -0
- package/src/shared/file-coalescer.ts +9 -0
- package/src/shared/formatters.ts +4 -6
- package/src/shared/settings.ts +15 -2
- package/src/shared/types.ts +16 -2
- package/src/shared/utils.ts +43 -33
- package/src/slash/slash-commands.ts +3 -1
- package/src/tui/fleet-status.ts +14 -10
- package/src/tui/render.ts +30 -26
- package/src/watchdog/change-signature.ts +4 -3
- package/src/workflows/scripted-workflow.ts +4 -1
|
@@ -15,14 +15,15 @@ import {
|
|
|
15
15
|
SUBAGENT_CONTROL_INTERCOM_EVENT,
|
|
16
16
|
SUBAGENT_STEERING_NOTICE_EVENT,
|
|
17
17
|
} from "../../shared/types.ts";
|
|
18
|
-
import { readStatus } from "../../shared/utils.ts";
|
|
18
|
+
import { readStatus, resolveWatchPath } from "../../shared/utils.ts";
|
|
19
19
|
import { normalizeParallelGroups } from "./parallel-groups.ts";
|
|
20
20
|
import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-reconciler.ts";
|
|
21
|
-
import { hasLiveNestedDescendants, updateAsyncJobNestedProjection } from "../shared/nested-events.ts";
|
|
21
|
+
import { findNestedRouteForRootId, hasLiveNestedDescendants, updateAsyncJobNestedProjection } from "../shared/nested-events.ts";
|
|
22
22
|
import { listAsyncRuns, type AsyncRunSummary } from "./async-status.ts";
|
|
23
23
|
|
|
24
24
|
interface AsyncJobTrackerOptions {
|
|
25
25
|
completionRetentionMs?: number;
|
|
26
|
+
/** Slow safety sweep for liveness repair when filesystem events are missed. */
|
|
26
27
|
pollIntervalMs?: number;
|
|
27
28
|
resultsDir?: string;
|
|
28
29
|
widgetEnabled?: boolean;
|
|
@@ -34,6 +35,9 @@ const CONTROL_EVENT_READ_CHUNK_BYTES = 64 * 1024;
|
|
|
34
35
|
const MAX_CONTROL_EVENT_LINE_BYTES = 1024 * 1024;
|
|
35
36
|
const CONTROL_EVENT_SCAN_WINDOW_BYTES = 2 * 1024 * 1024;
|
|
36
37
|
const MAX_RECENT_FLEET_JOBS = 20;
|
|
38
|
+
const DEFAULT_LIVENESS_INTERVAL_MS = 5000;
|
|
39
|
+
const EVENT_REFRESH_DEBOUNCE_MS = 25;
|
|
40
|
+
const WATCH_ATTACHMENT_RETRY_MS = 100;
|
|
37
41
|
|
|
38
42
|
function rememberFleetJob(state: SubagentState, job: AsyncJobState): void {
|
|
39
43
|
state.fleetJobs ??= new Map();
|
|
@@ -51,12 +55,19 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
51
55
|
handleComplete: (data: unknown) => void;
|
|
52
56
|
resetJobs: (ctx?: ExtensionContext) => void;
|
|
53
57
|
restoreActiveJobs: (ctx?: ExtensionContext) => void;
|
|
58
|
+
dispose: () => void;
|
|
54
59
|
} {
|
|
55
60
|
const completionRetentionMs = options.completionRetentionMs ?? 10000;
|
|
56
|
-
const
|
|
61
|
+
const livenessIntervalMs = options.pollIntervalMs ?? DEFAULT_LIVENESS_INTERVAL_MS;
|
|
57
62
|
const resultsDir = options.resultsDir ?? DIRS.results;
|
|
58
63
|
const steeringNoticeSeen = new Map<string, number>();
|
|
64
|
+
const jobWatchers = new Map<string, { watchers: Map<string, fs.FSWatcher>; retryTimer?: ReturnType<typeof setTimeout> }>();
|
|
65
|
+
const refreshTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
66
|
+
const runningJobIds = new Set<string>();
|
|
67
|
+
let rootWatcher: fs.FSWatcher | undefined;
|
|
68
|
+
let nextLivenessAt = Date.now() + livenessIntervalMs;
|
|
59
69
|
const rerenderWidget = (ctx: ExtensionContext, jobs = Array.from(state.asyncJobs.values())) => {
|
|
70
|
+
if (state.widgetsSuspended) return;
|
|
60
71
|
renderWidget(ctx, options.widgetEnabled === false ? [] : jobs);
|
|
61
72
|
(ctx.ui as { requestRender?: () => void }).requestRender?.();
|
|
62
73
|
};
|
|
@@ -73,6 +84,19 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
73
84
|
throw error;
|
|
74
85
|
}
|
|
75
86
|
};
|
|
87
|
+
const requestLastWidgetRender = () => {
|
|
88
|
+
const ctx = state.lastUiContext;
|
|
89
|
+
if (!ctx || state.widgetsSuspended || options.widgetEnabled === false) return;
|
|
90
|
+
try {
|
|
91
|
+
if (ctx.hasUI) (ctx.ui as { requestRender?: () => void }).requestRender?.();
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (error instanceof Error && error.message.includes("extension ctx is stale")) {
|
|
94
|
+
state.lastUiContext = null;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
76
100
|
const refreshWidget = (ctx: ExtensionContext) => rerenderWidget(ctx);
|
|
77
101
|
const restoredControlEventCursor = (asyncDir: string) => {
|
|
78
102
|
try {
|
|
@@ -82,6 +106,14 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
82
106
|
throw error;
|
|
83
107
|
}
|
|
84
108
|
};
|
|
109
|
+
const findNestedRouteForJob = (runId: string) => {
|
|
110
|
+
try {
|
|
111
|
+
return findNestedRouteForRootId(runId);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error(`Failed to resolve nested event route for '${runId}':`, error);
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
85
117
|
const summaryToJob = (run: AsyncRunSummary): AsyncJobState => {
|
|
86
118
|
const groups = normalizeParallelGroups(run.parallelGroups, run.steps.length, run.chainStepCount ?? run.steps.length);
|
|
87
119
|
const activeGroup = run.currentStep !== undefined
|
|
@@ -130,6 +162,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
130
162
|
totalTokens: run.totalTokens,
|
|
131
163
|
sessionFile: run.sessionFile,
|
|
132
164
|
controlEventCursor: restoredControlEventCursor(run.asyncDir),
|
|
165
|
+
nestedRoute: findNestedRouteForJob(run.id),
|
|
133
166
|
nestedChildren: run.nestedChildren,
|
|
134
167
|
parentWorkflowRunId: run.parentWorkflowRunId,
|
|
135
168
|
workflowKey: run.workflowKey,
|
|
@@ -146,6 +179,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
146
179
|
cancelCleanup(asyncId);
|
|
147
180
|
const timer = setTimeout(() => {
|
|
148
181
|
state.cleanupTimers.delete(asyncId);
|
|
182
|
+
closeJobWatcher(asyncId);
|
|
149
183
|
state.asyncJobs.delete(asyncId);
|
|
150
184
|
rerenderLastWidget();
|
|
151
185
|
}, completionRetentionMs);
|
|
@@ -268,139 +302,245 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
268
302
|
}
|
|
269
303
|
};
|
|
270
304
|
|
|
305
|
+
const closeJobWatcher = (asyncId: string) => {
|
|
306
|
+
const watched = jobWatchers.get(asyncId);
|
|
307
|
+
if (watched?.retryTimer) clearTimeout(watched.retryTimer);
|
|
308
|
+
for (const watcher of watched?.watchers.values() ?? []) watcher.close();
|
|
309
|
+
jobWatchers.delete(asyncId);
|
|
310
|
+
const timer = refreshTimers.get(asyncId);
|
|
311
|
+
if (timer) clearTimeout(timer);
|
|
312
|
+
refreshTimers.delete(asyncId);
|
|
313
|
+
runningJobIds.delete(asyncId);
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const refreshJob = (job: AsyncJobState): boolean => {
|
|
317
|
+
const widgetStateBefore = widgetRenderKey(job);
|
|
318
|
+
let nestedRefreshFailed = false;
|
|
319
|
+
const refreshNestedProjection = () => {
|
|
320
|
+
try {
|
|
321
|
+
updateAsyncJobNestedProjection(job);
|
|
322
|
+
} catch (error) {
|
|
323
|
+
nestedRefreshFailed = true;
|
|
324
|
+
console.error(`Failed to refresh nested async descendants for '${job.asyncDir}':`, error);
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
try {
|
|
328
|
+
emitNewControlEvents(job);
|
|
329
|
+
try {
|
|
330
|
+
if (job.nestedRoute) reconcileNestedAsyncDescendants(job.nestedRoute, { resultsDir, kill: options.kill, now: options.now });
|
|
331
|
+
} catch (error) {
|
|
332
|
+
nestedRefreshFailed = true;
|
|
333
|
+
console.error(`Failed to refresh nested async descendants for '${job.asyncDir}':`, error);
|
|
334
|
+
}
|
|
335
|
+
refreshNestedProjection();
|
|
336
|
+
const reconciliation = reconcileAsyncRun(job.asyncDir, {
|
|
337
|
+
resultsDir,
|
|
338
|
+
kill: options.kill,
|
|
339
|
+
now: options.now,
|
|
340
|
+
startedRun: {
|
|
341
|
+
runId: job.asyncId,
|
|
342
|
+
pid: job.pid,
|
|
343
|
+
sessionId: job.sessionId,
|
|
344
|
+
mode: job.mode,
|
|
345
|
+
agents: job.agents,
|
|
346
|
+
chainStepCount: job.chainStepCount,
|
|
347
|
+
parallelGroups: job.parallelGroups,
|
|
348
|
+
startedAt: job.startedAt,
|
|
349
|
+
sessionFile: job.sessionFile,
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
const status = reconciliation.status ?? readStatus(job.asyncDir);
|
|
353
|
+
if (status) {
|
|
354
|
+
const previousStatus = job.status;
|
|
355
|
+
job.status = status.state;
|
|
356
|
+
if (job.status === "running") runningJobIds.add(job.asyncId);
|
|
357
|
+
else runningJobIds.delete(job.asyncId);
|
|
358
|
+
if (job.status !== "complete" && job.status !== "failed" && job.status !== "paused" && job.status !== "stopped") cancelCleanup(job.asyncId);
|
|
359
|
+
job.sessionId = status.sessionId ?? job.sessionId;
|
|
360
|
+
job.activityState = status.activityState;
|
|
361
|
+
job.lastActivityAt = status.lastActivityAt ?? job.lastActivityAt;
|
|
362
|
+
job.currentTool = status.currentTool;
|
|
363
|
+
job.currentToolStartedAt = status.currentToolStartedAt;
|
|
364
|
+
job.currentPath = status.currentPath;
|
|
365
|
+
job.turnCount = status.turnCount ?? job.turnCount;
|
|
366
|
+
job.toolCount = status.toolCount ?? job.toolCount;
|
|
367
|
+
job.steering = status.steering ?? job.steering;
|
|
368
|
+
job.mode = status.mode;
|
|
369
|
+
job.parentWorkflowRunId = status.parentWorkflowRunId ?? job.parentWorkflowRunId;
|
|
370
|
+
job.workflowKey = status.workflowKey ?? job.workflowKey;
|
|
371
|
+
job.workflow = status.workflow ?? job.workflow;
|
|
372
|
+
job.currentStep = status.currentStep ?? job.currentStep;
|
|
373
|
+
job.chainStepCount = status.chainStepCount ?? job.chainStepCount;
|
|
374
|
+
job.startedAt = status.startedAt ?? job.startedAt;
|
|
375
|
+
if (status.lastUpdate !== undefined) job.updatedAt = status.lastUpdate;
|
|
376
|
+
if (status.steps?.length) {
|
|
377
|
+
const groups = normalizeParallelGroups(status.parallelGroups, status.steps.length, status.chainStepCount ?? status.steps.length);
|
|
378
|
+
job.parallelGroups = groups.length ? groups : job.parallelGroups;
|
|
379
|
+
job.hasParallelGroups = groups.length > 0 || job.hasParallelGroups;
|
|
380
|
+
const activeGroup = status.currentStep !== undefined
|
|
381
|
+
? groups.find((group) => status.currentStep! >= group.start && status.currentStep! < group.start + group.count)
|
|
382
|
+
: undefined;
|
|
383
|
+
const visibleSteps = activeGroup
|
|
384
|
+
? status.steps.slice(activeGroup.start, activeGroup.start + activeGroup.count).map((step, index) => ({ ...step, index: activeGroup.start + index }))
|
|
385
|
+
: status.steps.map((step, index) => ({ ...step, index }));
|
|
386
|
+
job.activeParallelGroup = Boolean(activeGroup);
|
|
387
|
+
job.agents = visibleSteps.map((step) => step.agent);
|
|
388
|
+
job.steps = visibleSteps;
|
|
389
|
+
refreshNestedProjection();
|
|
390
|
+
job.stepsTotal = visibleSteps.length;
|
|
391
|
+
job.runningSteps = visibleSteps.filter((step) => step.status === "running").length;
|
|
392
|
+
job.completedSteps = visibleSteps.filter((step) => step.status === "complete" || step.status === "completed").length;
|
|
393
|
+
if (status.state === "complete") job.completedSteps = visibleSteps.length;
|
|
394
|
+
}
|
|
395
|
+
job.sessionDir = status.sessionDir ?? job.sessionDir;
|
|
396
|
+
job.outputFile = status.outputFile ?? job.outputFile;
|
|
397
|
+
job.totalTokens = status.totalTokens ?? job.totalTokens;
|
|
398
|
+
job.timeoutMs = status.timeoutMs ?? job.timeoutMs;
|
|
399
|
+
job.deadlineAt = status.deadlineAt ?? job.deadlineAt;
|
|
400
|
+
job.timedOut = status.timedOut ?? job.timedOut;
|
|
401
|
+
job.stopped = status.stopped ?? job.stopped;
|
|
402
|
+
job.turnBudget = status.turnBudget ?? job.turnBudget;
|
|
403
|
+
job.turnBudgetExceeded = status.turnBudgetExceeded ?? job.turnBudgetExceeded;
|
|
404
|
+
job.wrapUpRequested = status.wrapUpRequested ?? job.wrapUpRequested;
|
|
405
|
+
job.sessionFile = status.sessionFile ?? job.sessionFile;
|
|
406
|
+
if (job.status === "complete" || job.status === "failed" || job.status === "paused" || job.status === "stopped") {
|
|
407
|
+
rememberFleetJob(state, job);
|
|
408
|
+
if (!nestedRefreshFailed && !hasLiveNestedDescendants(job.nestedChildren) && (previousStatus !== job.status || !state.cleanupTimers.has(job.asyncId))) {
|
|
409
|
+
scheduleCleanup(job.asyncId);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return widgetRenderKey(job) !== widgetStateBefore;
|
|
413
|
+
}
|
|
414
|
+
if (job.status === "queued") {
|
|
415
|
+
job.status = "running";
|
|
416
|
+
job.updatedAt = Date.now();
|
|
417
|
+
runningJobIds.add(job.asyncId);
|
|
418
|
+
}
|
|
419
|
+
} catch (error) {
|
|
420
|
+
if (job.status !== "failed") {
|
|
421
|
+
console.error(`Failed to read async status for '${job.asyncDir}':`, error);
|
|
422
|
+
job.status = "failed";
|
|
423
|
+
job.updatedAt = Date.now();
|
|
424
|
+
}
|
|
425
|
+
runningJobIds.delete(job.asyncId);
|
|
426
|
+
rememberFleetJob(state, job);
|
|
427
|
+
if (!hasLiveNestedDescendants(job.nestedChildren) && !state.cleanupTimers.has(job.asyncId)) scheduleCleanup(job.asyncId);
|
|
428
|
+
}
|
|
429
|
+
return widgetRenderKey(job) !== widgetStateBefore;
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
const scheduleJobRefresh = (asyncId: string, delayMs = EVENT_REFRESH_DEBOUNCE_MS) => {
|
|
433
|
+
if (refreshTimers.has(asyncId)) return;
|
|
434
|
+
const timer = setTimeout(() => {
|
|
435
|
+
refreshTimers.delete(asyncId);
|
|
436
|
+
const job = state.asyncJobs.get(asyncId);
|
|
437
|
+
if (job && refreshJob(job)) rerenderLastWidget();
|
|
438
|
+
}, delayMs);
|
|
439
|
+
timer.unref?.();
|
|
440
|
+
refreshTimers.set(asyncId, timer);
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
const watchJob = (job: AsyncJobState) => {
|
|
444
|
+
const watched = jobWatchers.get(job.asyncId) ?? { watchers: new Map<string, fs.FSWatcher>() };
|
|
445
|
+
const active = job.status === "queued" || job.status === "running";
|
|
446
|
+
const watchPath = (watchPath: string): boolean => {
|
|
447
|
+
if (watched.watchers.has(watchPath)) return true;
|
|
448
|
+
const nestedEventSink = job.nestedRoute?.eventSink === watchPath;
|
|
449
|
+
let watcher: fs.FSWatcher;
|
|
450
|
+
try {
|
|
451
|
+
watcher = fs.watch(resolveWatchPath(watchPath), (_event, file) => {
|
|
452
|
+
const rawFileName = file?.toString();
|
|
453
|
+
const fileName = rawFileName ?? path.basename(watchPath);
|
|
454
|
+
const nestedEventFile = nestedEventSink && (!rawFileName || rawFileName.endsWith(".json") || rawFileName.endsWith(".jsonl"));
|
|
455
|
+
if (fileName === "status.json" || fileName === "events.jsonl" || nestedEventFile) {
|
|
456
|
+
scheduleJobRefresh(job.asyncId);
|
|
457
|
+
}
|
|
458
|
+
if (watchPath !== job.asyncDir && !nestedEventSink) {
|
|
459
|
+
watcher.close();
|
|
460
|
+
watched.watchers.delete(watchPath);
|
|
461
|
+
watchJob(job);
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
} catch {
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
watcher.on("error", () => {
|
|
468
|
+
watcher.close();
|
|
469
|
+
watched.watchers.delete(watchPath);
|
|
470
|
+
if (watchPath.endsWith("status.json") || nestedEventSink) watchJob(job);
|
|
471
|
+
});
|
|
472
|
+
watcher.unref?.();
|
|
473
|
+
watched.watchers.set(watchPath, watcher);
|
|
474
|
+
return true;
|
|
475
|
+
};
|
|
476
|
+
watchPath(job.asyncDir);
|
|
477
|
+
const statusPath = path.join(job.asyncDir, "status.json");
|
|
478
|
+
const hadStatusWatcher = watched.watchers.has(statusPath);
|
|
479
|
+
const statusWatched = watchPath(statusPath);
|
|
480
|
+
if (statusWatched && !hadStatusWatcher) scheduleJobRefresh(job.asyncId);
|
|
481
|
+
watchPath(path.join(job.asyncDir, "events.jsonl"));
|
|
482
|
+
if (job.nestedRoute) watchPath(job.nestedRoute.eventSink);
|
|
483
|
+
if (!statusWatched && active && !watched.retryTimer) {
|
|
484
|
+
watched.retryTimer = setTimeout(() => {
|
|
485
|
+
watched.retryTimer = undefined;
|
|
486
|
+
const current = state.asyncJobs.get(job.asyncId);
|
|
487
|
+
if (current) watchJob(current);
|
|
488
|
+
}, WATCH_ATTACHMENT_RETRY_MS);
|
|
489
|
+
watched.retryTimer.unref?.();
|
|
490
|
+
} else if (statusWatched && watched.retryTimer) {
|
|
491
|
+
clearTimeout(watched.retryTimer);
|
|
492
|
+
watched.retryTimer = undefined;
|
|
493
|
+
}
|
|
494
|
+
if (watched.watchers.size > 0 || watched.retryTimer) jobWatchers.set(job.asyncId, watched);
|
|
495
|
+
else jobWatchers.delete(job.asyncId);
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
const watchAsyncRoot = () => {
|
|
499
|
+
if (rootWatcher) return;
|
|
500
|
+
try {
|
|
501
|
+
rootWatcher = fs.watch(resolveWatchPath(asyncDirRoot), (_event, file) => {
|
|
502
|
+
const runDirName = file?.toString();
|
|
503
|
+
for (const job of state.asyncJobs.values()) {
|
|
504
|
+
if (jobWatchers.has(job.asyncId) || (runDirName && path.basename(job.asyncDir) !== runDirName)) continue;
|
|
505
|
+
watchJob(job);
|
|
506
|
+
if (jobWatchers.has(job.asyncId)) scheduleJobRefresh(job.asyncId);
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
rootWatcher.on("error", () => {
|
|
510
|
+
rootWatcher?.close();
|
|
511
|
+
rootWatcher = undefined;
|
|
512
|
+
});
|
|
513
|
+
rootWatcher.unref?.();
|
|
514
|
+
} catch {
|
|
515
|
+
// The liveness sweep retries if the async root is not available yet.
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
|
|
271
519
|
const ensurePoller = () => {
|
|
520
|
+
watchAsyncRoot();
|
|
272
521
|
if (state.poller) return;
|
|
522
|
+
nextLivenessAt = Date.now() + livenessIntervalMs;
|
|
273
523
|
state.poller = setInterval(() => {
|
|
274
524
|
if (state.asyncJobs.size === 0) {
|
|
275
525
|
rerenderLastWidget([]);
|
|
276
|
-
if (state.poller)
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
526
|
+
if (state.poller) clearInterval(state.poller);
|
|
527
|
+
state.poller = null;
|
|
528
|
+
rootWatcher?.close();
|
|
529
|
+
rootWatcher = undefined;
|
|
280
530
|
return;
|
|
281
531
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
updateAsyncJobNestedProjection(job);
|
|
290
|
-
} catch (error) {
|
|
291
|
-
nestedRefreshFailed = true;
|
|
292
|
-
console.error(`Failed to refresh nested async descendants for '${job.asyncDir}':`, error);
|
|
293
|
-
}
|
|
294
|
-
};
|
|
295
|
-
const reconcileNestedDescendants = () => {
|
|
296
|
-
try {
|
|
297
|
-
if (job.nestedRoute) reconcileNestedAsyncDescendants(job.nestedRoute, { resultsDir, kill: options.kill, now: options.now });
|
|
298
|
-
} catch (error) {
|
|
299
|
-
nestedRefreshFailed = true;
|
|
300
|
-
console.error(`Failed to refresh nested async descendants for '${job.asyncDir}':`, error);
|
|
301
|
-
}
|
|
302
|
-
refreshNestedProjection();
|
|
303
|
-
};
|
|
304
|
-
try {
|
|
305
|
-
emitNewControlEvents(job);
|
|
306
|
-
reconcileNestedDescendants();
|
|
307
|
-
const reconciliation = reconcileAsyncRun(job.asyncDir, {
|
|
308
|
-
resultsDir,
|
|
309
|
-
kill: options.kill,
|
|
310
|
-
now: options.now,
|
|
311
|
-
startedRun: {
|
|
312
|
-
runId: job.asyncId,
|
|
313
|
-
pid: job.pid,
|
|
314
|
-
sessionId: job.sessionId,
|
|
315
|
-
mode: job.mode,
|
|
316
|
-
agents: job.agents,
|
|
317
|
-
chainStepCount: job.chainStepCount,
|
|
318
|
-
parallelGroups: job.parallelGroups,
|
|
319
|
-
startedAt: job.startedAt,
|
|
320
|
-
sessionFile: job.sessionFile,
|
|
321
|
-
},
|
|
322
|
-
});
|
|
323
|
-
const status = reconciliation.status ?? readStatus(job.asyncDir);
|
|
324
|
-
if (status) {
|
|
325
|
-
const previousStatus = job.status;
|
|
326
|
-
job.status = status.state;
|
|
327
|
-
if (job.status !== "complete" && job.status !== "failed" && job.status !== "paused" && job.status !== "stopped") cancelCleanup(job.asyncId);
|
|
328
|
-
job.sessionId = status.sessionId ?? job.sessionId;
|
|
329
|
-
job.activityState = status.activityState;
|
|
330
|
-
job.lastActivityAt = status.lastActivityAt ?? job.lastActivityAt;
|
|
331
|
-
job.currentTool = status.currentTool;
|
|
332
|
-
job.currentToolStartedAt = status.currentToolStartedAt;
|
|
333
|
-
job.currentPath = status.currentPath;
|
|
334
|
-
job.turnCount = status.turnCount ?? job.turnCount;
|
|
335
|
-
job.toolCount = status.toolCount ?? job.toolCount;
|
|
336
|
-
job.steering = status.steering ?? job.steering;
|
|
337
|
-
job.mode = status.mode;
|
|
338
|
-
job.parentWorkflowRunId = status.parentWorkflowRunId ?? job.parentWorkflowRunId;
|
|
339
|
-
job.workflowKey = status.workflowKey ?? job.workflowKey;
|
|
340
|
-
job.workflow = status.workflow ?? job.workflow;
|
|
341
|
-
job.currentStep = status.currentStep ?? job.currentStep;
|
|
342
|
-
job.chainStepCount = status.chainStepCount ?? job.chainStepCount;
|
|
343
|
-
job.startedAt = status.startedAt ?? job.startedAt;
|
|
344
|
-
if (status.lastUpdate !== undefined) job.updatedAt = status.lastUpdate;
|
|
345
|
-
if (status.steps?.length) {
|
|
346
|
-
const groups = normalizeParallelGroups(status.parallelGroups, status.steps.length, status.chainStepCount ?? status.steps.length);
|
|
347
|
-
job.parallelGroups = groups.length ? groups : job.parallelGroups;
|
|
348
|
-
job.hasParallelGroups = groups.length > 0 || job.hasParallelGroups;
|
|
349
|
-
const activeGroup = status.currentStep !== undefined
|
|
350
|
-
? groups.find((group) => status.currentStep! >= group.start && status.currentStep! < group.start + group.count)
|
|
351
|
-
: undefined;
|
|
352
|
-
const visibleSteps = activeGroup
|
|
353
|
-
? status.steps.slice(activeGroup.start, activeGroup.start + activeGroup.count).map((step, index) => ({ ...step, index: activeGroup.start + index }))
|
|
354
|
-
: status.steps.map((step, index) => ({ ...step, index }));
|
|
355
|
-
job.activeParallelGroup = Boolean(activeGroup);
|
|
356
|
-
job.agents = visibleSteps.map((step) => step.agent);
|
|
357
|
-
job.steps = visibleSteps;
|
|
358
|
-
refreshNestedProjection();
|
|
359
|
-
job.stepsTotal = visibleSteps.length;
|
|
360
|
-
job.runningSteps = visibleSteps.filter((step) => step.status === "running").length;
|
|
361
|
-
job.completedSteps = visibleSteps.filter((step) => step.status === "complete" || step.status === "completed").length;
|
|
362
|
-
if (status.state === "complete") job.completedSteps = visibleSteps.length;
|
|
363
|
-
}
|
|
364
|
-
job.sessionDir = status.sessionDir ?? job.sessionDir;
|
|
365
|
-
job.outputFile = status.outputFile ?? job.outputFile;
|
|
366
|
-
job.totalTokens = status.totalTokens ?? job.totalTokens;
|
|
367
|
-
job.timeoutMs = status.timeoutMs ?? job.timeoutMs;
|
|
368
|
-
job.deadlineAt = status.deadlineAt ?? job.deadlineAt;
|
|
369
|
-
job.timedOut = status.timedOut ?? job.timedOut;
|
|
370
|
-
job.stopped = status.stopped ?? job.stopped;
|
|
371
|
-
job.turnBudget = status.turnBudget ?? job.turnBudget;
|
|
372
|
-
job.turnBudgetExceeded = status.turnBudgetExceeded ?? job.turnBudgetExceeded;
|
|
373
|
-
job.wrapUpRequested = status.wrapUpRequested ?? job.wrapUpRequested;
|
|
374
|
-
job.sessionFile = status.sessionFile ?? job.sessionFile;
|
|
375
|
-
if (job.status === "complete" || job.status === "failed" || job.status === "paused" || job.status === "stopped") {
|
|
376
|
-
rememberFleetJob(state, job);
|
|
377
|
-
if (!nestedRefreshFailed && !hasLiveNestedDescendants(job.nestedChildren) && (previousStatus !== job.status || !state.cleanupTimers.has(job.asyncId))) {
|
|
378
|
-
scheduleCleanup(job.asyncId);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
if (widgetRenderKey(job) !== widgetStateBefore) widgetChanged = true;
|
|
382
|
-
continue;
|
|
383
|
-
}
|
|
384
|
-
if (job.status === "queued") {
|
|
385
|
-
job.status = "running";
|
|
386
|
-
job.updatedAt = Date.now();
|
|
387
|
-
}
|
|
388
|
-
} catch (error) {
|
|
389
|
-
if (job.status !== "failed") {
|
|
390
|
-
console.error(`Failed to read async status for '${job.asyncDir}':`, error);
|
|
391
|
-
job.status = "failed";
|
|
392
|
-
job.updatedAt = Date.now();
|
|
393
|
-
}
|
|
394
|
-
rememberFleetJob(state, job);
|
|
395
|
-
if (!hasLiveNestedDescendants(job.nestedChildren) && !state.cleanupTimers.has(job.asyncId)) {
|
|
396
|
-
scheduleCleanup(job.asyncId);
|
|
397
|
-
}
|
|
532
|
+
const now = Date.now();
|
|
533
|
+
if (now >= nextLivenessAt) {
|
|
534
|
+
nextLivenessAt = now + livenessIntervalMs;
|
|
535
|
+
let widgetChanged = false;
|
|
536
|
+
for (const job of state.asyncJobs.values()) {
|
|
537
|
+
watchJob(job);
|
|
538
|
+
if (refreshJob(job)) widgetChanged = true;
|
|
398
539
|
}
|
|
399
|
-
if (
|
|
540
|
+
if (widgetChanged) rerenderLastWidget();
|
|
400
541
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
}, pollIntervalMs);
|
|
542
|
+
if (runningJobIds.size > 0) requestLastWidgetRender();
|
|
543
|
+
}, Math.min(POLL_INTERVAL_MS, livenessIntervalMs));
|
|
404
544
|
state.poller.unref?.();
|
|
405
545
|
};
|
|
406
546
|
|
|
@@ -442,7 +582,10 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
442
582
|
workflowKey: info.workflowKey,
|
|
443
583
|
controlEventCursor: 0,
|
|
444
584
|
});
|
|
445
|
-
|
|
585
|
+
const job = state.asyncJobs.get(info.id)!;
|
|
586
|
+
rememberFleetJob(state, job);
|
|
587
|
+
watchJob(job);
|
|
588
|
+
scheduleJobRefresh(info.id, 0);
|
|
446
589
|
ensurePoller();
|
|
447
590
|
rerenderLastWidget();
|
|
448
591
|
};
|
|
@@ -456,9 +599,14 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
456
599
|
let nestedRefreshFailed = false;
|
|
457
600
|
if (job) {
|
|
458
601
|
job.status = result.state ?? (result.success ? "complete" : "failed");
|
|
602
|
+
runningJobIds.delete(asyncId);
|
|
459
603
|
job.stopped = result.stopped ?? job.stopped;
|
|
460
604
|
job.updatedAt = Date.now();
|
|
461
|
-
if (result.asyncDir
|
|
605
|
+
if (result.asyncDir && result.asyncDir !== job.asyncDir) {
|
|
606
|
+
closeJobWatcher(asyncId);
|
|
607
|
+
job.asyncDir = result.asyncDir;
|
|
608
|
+
watchJob(job);
|
|
609
|
+
}
|
|
462
610
|
try {
|
|
463
611
|
updateAsyncJobNestedProjection(job);
|
|
464
612
|
} catch (error) {
|
|
@@ -471,10 +619,20 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
471
619
|
if (!nestedRefreshFailed && !hasLiveNestedDescendants(job?.nestedChildren)) scheduleCleanup(asyncId);
|
|
472
620
|
};
|
|
473
621
|
|
|
622
|
+
const dispose = () => {
|
|
623
|
+
if (state.poller) clearInterval(state.poller);
|
|
624
|
+
state.poller = null;
|
|
625
|
+
rootWatcher?.close();
|
|
626
|
+
rootWatcher = undefined;
|
|
627
|
+
for (const asyncId of jobWatchers.keys()) closeJobWatcher(asyncId);
|
|
628
|
+
for (const timer of refreshTimers.values()) clearTimeout(timer);
|
|
629
|
+
refreshTimers.clear();
|
|
630
|
+
runningJobIds.clear();
|
|
631
|
+
};
|
|
632
|
+
|
|
474
633
|
const resetJobs = (ctx?: ExtensionContext) => {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
}
|
|
634
|
+
dispose();
|
|
635
|
+
for (const timer of state.cleanupTimers.values()) clearTimeout(timer);
|
|
478
636
|
state.cleanupTimers.clear();
|
|
479
637
|
state.asyncJobs.clear();
|
|
480
638
|
state.fleetJobs?.clear();
|
|
@@ -500,12 +658,14 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
500
658
|
for (const run of runs) {
|
|
501
659
|
const job = summaryToJob(run);
|
|
502
660
|
state.asyncJobs.set(run.id, job);
|
|
661
|
+
if (job.status === "running") runningJobIds.add(job.asyncId);
|
|
503
662
|
rememberFleetJob(state, job);
|
|
663
|
+
watchJob(job);
|
|
504
664
|
}
|
|
505
665
|
if (runs.length === 0) return;
|
|
506
666
|
ensurePoller();
|
|
507
667
|
rerenderLastWidget();
|
|
508
668
|
};
|
|
509
669
|
|
|
510
|
-
return { ensurePoller, refreshWidget, handleStarted, handleComplete, resetJobs, restoreActiveJobs };
|
|
670
|
+
return { ensurePoller, refreshWidget, handleStarted, handleComplete, resetJobs, restoreActiveJobs, dispose };
|
|
511
671
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { DIRS, type AcceptanceInput, type AsyncStatus, type SteeringRecoveryDescriptor } from "../../shared/types.ts";
|
|
3
|
+
import { DIRS, type AcceptanceInput, type AsyncStatus, type ResolvedTurnBudget, type SteeringRecoveryDescriptor } from "../../shared/types.ts";
|
|
4
4
|
import type { AgentConfig } from "../../agents/agents.ts";
|
|
5
5
|
import { validateAcceptanceInput } from "../shared/acceptance.ts";
|
|
6
6
|
import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
|
|
@@ -273,6 +273,23 @@ function normalizeRecoveryAcceptance(value: unknown, descriptorPath: string): Ac
|
|
|
273
273
|
return value as AcceptanceInput;
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
+
function normalizeRecoveryTurnBudget(value: unknown, descriptorPath: string): ResolvedTurnBudget {
|
|
277
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
278
|
+
const {
|
|
279
|
+
outcome: _outcome,
|
|
280
|
+
turnCount: _turnCount,
|
|
281
|
+
wrapUpRequestedAtTurn: _wrapUpRequestedAtTurn,
|
|
282
|
+
terminationDeferredAtTurn: _terminationDeferredAtTurn,
|
|
283
|
+
exceededAtTurn: _exceededAtTurn,
|
|
284
|
+
...publicTurnBudget
|
|
285
|
+
} = value as Record<string, unknown>;
|
|
286
|
+
value = publicTurnBudget;
|
|
287
|
+
}
|
|
288
|
+
const result = resolveTurnBudgetConfig(value, "recoveryDescriptor.initialTurnBudget");
|
|
289
|
+
if (result.error || !result.turnBudget) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${result.error ?? "recoveryDescriptor.initialTurnBudget is invalid."}`);
|
|
290
|
+
return result.turnBudget;
|
|
291
|
+
}
|
|
292
|
+
|
|
276
293
|
export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): SteeringRecoveryDescriptor | undefined {
|
|
277
294
|
if (!asyncDir) return undefined;
|
|
278
295
|
const descriptorPath = path.join(asyncDir, "recovery-descriptor.json");
|
|
@@ -329,8 +346,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
329
346
|
}
|
|
330
347
|
if (parsed.absoluteDeadlineAt !== undefined && (!Number.isFinite(parsed.absoluteDeadlineAt) || (parsed.absoluteDeadlineAt as number) <= 0)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': absoluteDeadlineAt must be a positive timestamp.`);
|
|
331
348
|
if (parsed.initialTurnBudget !== undefined) {
|
|
332
|
-
|
|
333
|
-
if (result.error) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${result.error}`);
|
|
349
|
+
parsed.initialTurnBudget = normalizeRecoveryTurnBudget(parsed.initialTurnBudget, descriptorPath);
|
|
334
350
|
}
|
|
335
351
|
if (parsed.initialToolBudget !== undefined) {
|
|
336
352
|
const result = validateToolBudgetConfig(parsed.initialToolBudget, "recoveryDescriptor.initialToolBudget");
|
|
@@ -4,13 +4,14 @@ import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "
|
|
|
4
4
|
import { formatActivityLabel, formatParallelOutcome } from "../../shared/status-format.ts";
|
|
5
5
|
import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type CostSummary, type Details, type LaunchResolvedChildExtensionsV1, type RuntimeAcknowledgedChildExtensionsV1, type NestedRunSummary, type SteeringStatus, type SubagentRunMode, type TokenUsage, type TurnBudgetState, type UsageBudgetState, type ChainCheckpointState } from "../../shared/types.ts";
|
|
6
6
|
import type { ResolvedSubagentCapabilityCeiling, SubagentCapabilityAudit } from "../shared/capability-ceiling.ts";
|
|
7
|
-
import { readStatus } from "../../shared/utils.ts";
|
|
8
|
-
import { attachRootChildrenToSteps, buildNestedRouteIndex, type NestedRoute, projectNestedEvents } from "../shared/nested-events.ts";
|
|
7
|
+
import { pruneStatusCacheForAsyncRoot, readStatus } from "../../shared/utils.ts";
|
|
8
|
+
import { attachRootChildrenToSteps, buildNestedRouteIndex, findNestedRouteForRootId, type NestedRoute, projectNestedEvents } from "../shared/nested-events.ts";
|
|
9
9
|
import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
|
|
10
10
|
import { flatToLogicalStepIndex, normalizeParallelGroups } from "./parallel-groups.ts";
|
|
11
11
|
import { contextModeLabel, summarizeContextModes, type ContextMode, type ContextSummary } from "../shared/context-mode.ts";
|
|
12
12
|
import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-reconciler.ts";
|
|
13
13
|
import { readProcessTerminal, sanitizeProcessTerminal } from "./process-terminal.ts";
|
|
14
|
+
import { ACTIVE_RUN_INDEX_DIR, isActiveAsyncState, readActiveRunIndex, updateActiveRunIndex } from "./active-run-index.ts";
|
|
14
15
|
|
|
15
16
|
interface AsyncRunStepSummary {
|
|
16
17
|
index: number;
|
|
@@ -160,7 +161,7 @@ type TargetedAsyncRunResolution =
|
|
|
160
161
|
* accepting a path whose canonical location escaped the async root.
|
|
161
162
|
*/
|
|
162
163
|
export function resolveTargetedAsyncRun(asyncDirRoot: string, id: string, sessionId?: string): TargetedAsyncRunResolution {
|
|
163
|
-
if (!id || id === "." || id === ".." || path.basename(id) !== id) return { kind: "reject" };
|
|
164
|
+
if (!id || id === "." || id === ".." || id === ACTIVE_RUN_INDEX_DIR || path.basename(id) !== id) return { kind: "reject" };
|
|
164
165
|
const asyncDir = path.join(asyncDirRoot, id);
|
|
165
166
|
let entryStat: fs.Stats;
|
|
166
167
|
try {
|
|
@@ -370,6 +371,13 @@ function sortRuns(runs: AsyncRunSummary[]): AsyncRunSummary[] {
|
|
|
370
371
|
|
|
371
372
|
export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions = {}): AsyncRunSummary[] {
|
|
372
373
|
let entries: string[];
|
|
374
|
+
let scannedCompleteRoot = false;
|
|
375
|
+
let usedActiveIndex = false;
|
|
376
|
+
const activeOnly = options.runId === undefined
|
|
377
|
+
&& options.entryLimit === undefined
|
|
378
|
+
&& options.states !== undefined
|
|
379
|
+
&& options.states.length > 0
|
|
380
|
+
&& options.states.every(isActiveAsyncState);
|
|
373
381
|
try {
|
|
374
382
|
if (options.runId !== undefined) {
|
|
375
383
|
const resolution = resolveTargetedAsyncRun(asyncDirRoot, options.runId, options.sessionId);
|
|
@@ -381,8 +389,16 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
381
389
|
&& resolveTargetedAsyncRun(asyncDirRoot, entry, options.sessionId).kind === "exact"
|
|
382
390
|
)
|
|
383
391
|
: [];
|
|
392
|
+
} else if (activeOnly) {
|
|
393
|
+
entries = (readActiveRunIndex(asyncDirRoot) ?? []).filter((entry) => {
|
|
394
|
+
if (resolveTargetedAsyncRun(asyncDirRoot, entry).kind === "exact") return true;
|
|
395
|
+
updateActiveRunIndex(path.join(asyncDirRoot, entry), "failed");
|
|
396
|
+
return false;
|
|
397
|
+
});
|
|
398
|
+
usedActiveIndex = true;
|
|
384
399
|
} else {
|
|
385
|
-
entries = fs.readdirSync(asyncDirRoot).filter((entry) => isAsyncRunDir(asyncDirRoot, entry));
|
|
400
|
+
entries = fs.readdirSync(asyncDirRoot).filter((entry) => entry !== ACTIVE_RUN_INDEX_DIR && isAsyncRunDir(asyncDirRoot, entry));
|
|
401
|
+
scannedCompleteRoot = true;
|
|
386
402
|
}
|
|
387
403
|
} catch (error) {
|
|
388
404
|
if (isNotFoundError(error)) return [];
|
|
@@ -392,6 +408,7 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
392
408
|
}
|
|
393
409
|
|
|
394
410
|
if (options.entryLimit !== undefined) {
|
|
411
|
+
scannedCompleteRoot = false;
|
|
395
412
|
const limit = Math.max(0, Math.floor(options.entryLimit));
|
|
396
413
|
entries = entries
|
|
397
414
|
.map((entry) => {
|
|
@@ -410,6 +427,8 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
410
427
|
.map((candidate) => candidate.entry);
|
|
411
428
|
}
|
|
412
429
|
|
|
430
|
+
if (scannedCompleteRoot) pruneStatusCacheForAsyncRoot(asyncDirRoot, entries);
|
|
431
|
+
|
|
413
432
|
const allowedStates = options.states ? new Set(options.states) : undefined;
|
|
414
433
|
const runs: AsyncRunSummary[] = [];
|
|
415
434
|
// Route resolution for every run shares a single index built from the
|
|
@@ -419,6 +438,7 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
419
438
|
// entirely when no active runs match.
|
|
420
439
|
let nestedRouteIndex: Map<string, NestedRoute> | undefined;
|
|
421
440
|
const resolveNestedRoute = (rootRunId: string): NestedRoute | undefined => {
|
|
441
|
+
if (usedActiveIndex) return findNestedRouteForRootId(rootRunId);
|
|
422
442
|
if (!nestedRouteIndex) nestedRouteIndex = buildNestedRouteIndex();
|
|
423
443
|
return nestedRouteIndex.get(rootRunId);
|
|
424
444
|
};
|
|
@@ -428,7 +448,15 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
428
448
|
? undefined
|
|
429
449
|
: reconcileAsyncRun(asyncDir, { resultsDir: options.resultsDir, kill: options.kill, now: options.now });
|
|
430
450
|
const status = (reconciliation?.status ?? readStatus(asyncDir)) as (AsyncStatus & { cwd?: string }) | null;
|
|
431
|
-
if (!status)
|
|
451
|
+
if (!status) {
|
|
452
|
+
if (usedActiveIndex) updateActiveRunIndex(asyncDir, "failed");
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
if (status.displayDismissedAt !== undefined) {
|
|
456
|
+
if (usedActiveIndex) updateActiveRunIndex(asyncDir, "complete");
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
if (usedActiveIndex && !isActiveAsyncState(status.state)) updateActiveRunIndex(asyncDir, status.state);
|
|
432
460
|
// Filter before the nested-route lookup: the lookup builds an index over
|
|
433
461
|
// the nested-events directory, so deferring it for filtered-out runs keeps
|
|
434
462
|
// restoration at load from scanning that directory when no active runs
|