pi-subagents 0.47.0 → 0.48.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 +37 -0
- package/README.md +2 -0
- package/agents/reviewer.md +3 -4
- package/docs/configuration.md +46 -2
- package/docs/observability.md +1 -1
- package/docs/tool-reference.md +1 -1
- package/package.json +1 -1
- package/src/agents/agents.ts +9 -3
- package/src/extension/config.ts +16 -0
- package/src/extension/doctor.ts +40 -0
- package/src/extension/index.ts +30 -11
- package/src/extension/public-execution.ts +5 -0
- package/src/extension/rpc.ts +7 -9
- package/src/extension/schemas.ts +4 -4
- package/src/intercom/intercom-bridge.ts +4 -1
- package/src/intercom/native-supervisor-channel.ts +102 -4
- package/src/missions/lifecycle.ts +4 -7
- package/src/missions/store.ts +4 -4
- package/src/missions/workflow-state.ts +6 -2
- package/src/runs/background/active-async-capacity.ts +374 -0
- package/src/runs/background/active-run-index.ts +46 -0
- package/src/runs/background/async-execution.ts +117 -29
- package/src/runs/background/async-job-tracker.ts +279 -134
- package/src/runs/background/async-resume.ts +7 -1
- package/src/runs/background/async-status.ts +33 -4
- package/src/runs/background/chain-append.ts +33 -15
- package/src/runs/background/control-channel.ts +55 -17
- package/src/runs/background/owned-process-tree.ts +104 -0
- package/src/runs/background/process-terminal.ts +17 -3
- package/src/runs/background/result-watcher.ts +87 -6
- package/src/runs/background/run-status.ts +23 -1
- package/src/runs/background/scheduled-runs.ts +23 -0
- package/src/runs/background/stale-run-reconciler.ts +10 -3
- package/src/runs/background/subagent-runner.ts +75 -33
- 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 +37 -2
- package/src/runs/foreground/execution.ts +90 -19
- package/src/runs/foreground/foreground-control.ts +12 -0
- package/src/runs/foreground/prompt-audit.ts +171 -0
- package/src/runs/foreground/subagent-executor.ts +648 -195
- package/src/runs/shared/acceptance.ts +13 -4
- package/src/runs/shared/llm-intent-arbiter.ts +286 -0
- package/src/runs/shared/parallel-utils.ts +2 -0
- package/src/runs/shared/pi-args.ts +44 -1
- package/src/runs/shared/run-fanout-budget.ts +280 -0
- package/src/runs/shared/single-output.ts +4 -2
- package/src/runs/shared/subagent-prompt-runtime.ts +24 -5
- package/src/runs/shared/task-intent.ts +19 -3
- package/src/runs/shared/worktree.ts +17 -5
- package/src/shared/artifacts.ts +1 -1
- package/src/shared/file-coalescer.ts +9 -0
- package/src/shared/types.ts +97 -1
- package/src/shared/utils.ts +3 -1
- package/src/tui/fleet-status.ts +7 -5
- package/src/tui/fleet.ts +225 -12
- package/src/workflows/scripted-workflow.ts +26 -6
|
@@ -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,11 +55,17 @@ 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())) => {
|
|
60
70
|
if (state.widgetsSuspended) return;
|
|
61
71
|
renderWidget(ctx, options.widgetEnabled === false ? [] : jobs);
|
|
@@ -96,6 +106,14 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
96
106
|
throw error;
|
|
97
107
|
}
|
|
98
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
|
+
};
|
|
99
117
|
const summaryToJob = (run: AsyncRunSummary): AsyncJobState => {
|
|
100
118
|
const groups = normalizeParallelGroups(run.parallelGroups, run.steps.length, run.chainStepCount ?? run.steps.length);
|
|
101
119
|
const activeGroup = run.currentStep !== undefined
|
|
@@ -144,6 +162,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
144
162
|
totalTokens: run.totalTokens,
|
|
145
163
|
sessionFile: run.sessionFile,
|
|
146
164
|
controlEventCursor: restoredControlEventCursor(run.asyncDir),
|
|
165
|
+
nestedRoute: findNestedRouteForJob(run.id),
|
|
147
166
|
nestedChildren: run.nestedChildren,
|
|
148
167
|
parentWorkflowRunId: run.parentWorkflowRunId,
|
|
149
168
|
workflowKey: run.workflowKey,
|
|
@@ -160,6 +179,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
160
179
|
cancelCleanup(asyncId);
|
|
161
180
|
const timer = setTimeout(() => {
|
|
162
181
|
state.cleanupTimers.delete(asyncId);
|
|
182
|
+
closeJobWatcher(asyncId);
|
|
163
183
|
state.asyncJobs.delete(asyncId);
|
|
164
184
|
rerenderLastWidget();
|
|
165
185
|
}, completionRetentionMs);
|
|
@@ -282,140 +302,245 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
282
302
|
}
|
|
283
303
|
};
|
|
284
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
|
+
|
|
285
519
|
const ensurePoller = () => {
|
|
520
|
+
watchAsyncRoot();
|
|
286
521
|
if (state.poller) return;
|
|
522
|
+
nextLivenessAt = Date.now() + livenessIntervalMs;
|
|
287
523
|
state.poller = setInterval(() => {
|
|
288
524
|
if (state.asyncJobs.size === 0) {
|
|
289
525
|
rerenderLastWidget([]);
|
|
290
|
-
if (state.poller)
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
526
|
+
if (state.poller) clearInterval(state.poller);
|
|
527
|
+
state.poller = null;
|
|
528
|
+
rootWatcher?.close();
|
|
529
|
+
rootWatcher = undefined;
|
|
294
530
|
return;
|
|
295
531
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
updateAsyncJobNestedProjection(job);
|
|
304
|
-
} catch (error) {
|
|
305
|
-
nestedRefreshFailed = true;
|
|
306
|
-
console.error(`Failed to refresh nested async descendants for '${job.asyncDir}':`, error);
|
|
307
|
-
}
|
|
308
|
-
};
|
|
309
|
-
const reconcileNestedDescendants = () => {
|
|
310
|
-
try {
|
|
311
|
-
if (job.nestedRoute) reconcileNestedAsyncDescendants(job.nestedRoute, { resultsDir, kill: options.kill, now: options.now });
|
|
312
|
-
} catch (error) {
|
|
313
|
-
nestedRefreshFailed = true;
|
|
314
|
-
console.error(`Failed to refresh nested async descendants for '${job.asyncDir}':`, error);
|
|
315
|
-
}
|
|
316
|
-
refreshNestedProjection();
|
|
317
|
-
};
|
|
318
|
-
try {
|
|
319
|
-
emitNewControlEvents(job);
|
|
320
|
-
reconcileNestedDescendants();
|
|
321
|
-
const reconciliation = reconcileAsyncRun(job.asyncDir, {
|
|
322
|
-
resultsDir,
|
|
323
|
-
kill: options.kill,
|
|
324
|
-
now: options.now,
|
|
325
|
-
startedRun: {
|
|
326
|
-
runId: job.asyncId,
|
|
327
|
-
pid: job.pid,
|
|
328
|
-
sessionId: job.sessionId,
|
|
329
|
-
mode: job.mode,
|
|
330
|
-
agents: job.agents,
|
|
331
|
-
chainStepCount: job.chainStepCount,
|
|
332
|
-
parallelGroups: job.parallelGroups,
|
|
333
|
-
startedAt: job.startedAt,
|
|
334
|
-
sessionFile: job.sessionFile,
|
|
335
|
-
},
|
|
336
|
-
});
|
|
337
|
-
const status = reconciliation.status ?? readStatus(job.asyncDir);
|
|
338
|
-
if (status) {
|
|
339
|
-
const previousStatus = job.status;
|
|
340
|
-
job.status = status.state;
|
|
341
|
-
if (job.status !== "complete" && job.status !== "failed" && job.status !== "paused" && job.status !== "stopped") cancelCleanup(job.asyncId);
|
|
342
|
-
job.sessionId = status.sessionId ?? job.sessionId;
|
|
343
|
-
job.activityState = status.activityState;
|
|
344
|
-
job.lastActivityAt = status.lastActivityAt ?? job.lastActivityAt;
|
|
345
|
-
job.currentTool = status.currentTool;
|
|
346
|
-
job.currentToolStartedAt = status.currentToolStartedAt;
|
|
347
|
-
job.currentPath = status.currentPath;
|
|
348
|
-
job.turnCount = status.turnCount ?? job.turnCount;
|
|
349
|
-
job.toolCount = status.toolCount ?? job.toolCount;
|
|
350
|
-
job.steering = status.steering ?? job.steering;
|
|
351
|
-
job.mode = status.mode;
|
|
352
|
-
job.parentWorkflowRunId = status.parentWorkflowRunId ?? job.parentWorkflowRunId;
|
|
353
|
-
job.workflowKey = status.workflowKey ?? job.workflowKey;
|
|
354
|
-
job.workflow = status.workflow ?? job.workflow;
|
|
355
|
-
job.currentStep = status.currentStep ?? job.currentStep;
|
|
356
|
-
job.chainStepCount = status.chainStepCount ?? job.chainStepCount;
|
|
357
|
-
job.startedAt = status.startedAt ?? job.startedAt;
|
|
358
|
-
if (status.lastUpdate !== undefined) job.updatedAt = status.lastUpdate;
|
|
359
|
-
if (status.steps?.length) {
|
|
360
|
-
const groups = normalizeParallelGroups(status.parallelGroups, status.steps.length, status.chainStepCount ?? status.steps.length);
|
|
361
|
-
job.parallelGroups = groups.length ? groups : job.parallelGroups;
|
|
362
|
-
job.hasParallelGroups = groups.length > 0 || job.hasParallelGroups;
|
|
363
|
-
const activeGroup = status.currentStep !== undefined
|
|
364
|
-
? groups.find((group) => status.currentStep! >= group.start && status.currentStep! < group.start + group.count)
|
|
365
|
-
: undefined;
|
|
366
|
-
const visibleSteps = activeGroup
|
|
367
|
-
? status.steps.slice(activeGroup.start, activeGroup.start + activeGroup.count).map((step, index) => ({ ...step, index: activeGroup.start + index }))
|
|
368
|
-
: status.steps.map((step, index) => ({ ...step, index }));
|
|
369
|
-
job.activeParallelGroup = Boolean(activeGroup);
|
|
370
|
-
job.agents = visibleSteps.map((step) => step.agent);
|
|
371
|
-
job.steps = visibleSteps;
|
|
372
|
-
refreshNestedProjection();
|
|
373
|
-
job.stepsTotal = visibleSteps.length;
|
|
374
|
-
job.runningSteps = visibleSteps.filter((step) => step.status === "running").length;
|
|
375
|
-
job.completedSteps = visibleSteps.filter((step) => step.status === "complete" || step.status === "completed").length;
|
|
376
|
-
if (status.state === "complete") job.completedSteps = visibleSteps.length;
|
|
377
|
-
}
|
|
378
|
-
job.sessionDir = status.sessionDir ?? job.sessionDir;
|
|
379
|
-
job.outputFile = status.outputFile ?? job.outputFile;
|
|
380
|
-
job.totalTokens = status.totalTokens ?? job.totalTokens;
|
|
381
|
-
job.timeoutMs = status.timeoutMs ?? job.timeoutMs;
|
|
382
|
-
job.deadlineAt = status.deadlineAt ?? job.deadlineAt;
|
|
383
|
-
job.timedOut = status.timedOut ?? job.timedOut;
|
|
384
|
-
job.stopped = status.stopped ?? job.stopped;
|
|
385
|
-
job.turnBudget = status.turnBudget ?? job.turnBudget;
|
|
386
|
-
job.turnBudgetExceeded = status.turnBudgetExceeded ?? job.turnBudgetExceeded;
|
|
387
|
-
job.wrapUpRequested = status.wrapUpRequested ?? job.wrapUpRequested;
|
|
388
|
-
job.sessionFile = status.sessionFile ?? job.sessionFile;
|
|
389
|
-
if (job.status === "complete" || job.status === "failed" || job.status === "paused" || job.status === "stopped") {
|
|
390
|
-
rememberFleetJob(state, job);
|
|
391
|
-
if (!nestedRefreshFailed && !hasLiveNestedDescendants(job.nestedChildren) && (previousStatus !== job.status || !state.cleanupTimers.has(job.asyncId))) {
|
|
392
|
-
scheduleCleanup(job.asyncId);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
if (widgetRenderKey(job) !== widgetStateBefore) widgetChanged = true;
|
|
396
|
-
continue;
|
|
397
|
-
}
|
|
398
|
-
if (job.status === "queued") {
|
|
399
|
-
job.status = "running";
|
|
400
|
-
job.updatedAt = Date.now();
|
|
401
|
-
}
|
|
402
|
-
} catch (error) {
|
|
403
|
-
if (job.status !== "failed") {
|
|
404
|
-
console.error(`Failed to read async status for '${job.asyncDir}':`, error);
|
|
405
|
-
job.status = "failed";
|
|
406
|
-
job.updatedAt = Date.now();
|
|
407
|
-
}
|
|
408
|
-
rememberFleetJob(state, job);
|
|
409
|
-
if (!hasLiveNestedDescendants(job.nestedChildren) && !state.cleanupTimers.has(job.asyncId)) {
|
|
410
|
-
scheduleCleanup(job.asyncId);
|
|
411
|
-
}
|
|
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;
|
|
412
539
|
}
|
|
413
|
-
if (
|
|
540
|
+
if (widgetChanged) rerenderLastWidget();
|
|
414
541
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
else if (Array.from(state.asyncJobs.values()).some((job) => job.status === "running")) requestLastWidgetRender();
|
|
418
|
-
}, pollIntervalMs);
|
|
542
|
+
if (runningJobIds.size > 0) requestLastWidgetRender();
|
|
543
|
+
}, Math.min(POLL_INTERVAL_MS, livenessIntervalMs));
|
|
419
544
|
state.poller.unref?.();
|
|
420
545
|
};
|
|
421
546
|
|
|
@@ -457,7 +582,10 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
457
582
|
workflowKey: info.workflowKey,
|
|
458
583
|
controlEventCursor: 0,
|
|
459
584
|
});
|
|
460
|
-
|
|
585
|
+
const job = state.asyncJobs.get(info.id)!;
|
|
586
|
+
rememberFleetJob(state, job);
|
|
587
|
+
watchJob(job);
|
|
588
|
+
scheduleJobRefresh(info.id, 0);
|
|
461
589
|
ensurePoller();
|
|
462
590
|
rerenderLastWidget();
|
|
463
591
|
};
|
|
@@ -471,9 +599,14 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
471
599
|
let nestedRefreshFailed = false;
|
|
472
600
|
if (job) {
|
|
473
601
|
job.status = result.state ?? (result.success ? "complete" : "failed");
|
|
602
|
+
runningJobIds.delete(asyncId);
|
|
474
603
|
job.stopped = result.stopped ?? job.stopped;
|
|
475
604
|
job.updatedAt = Date.now();
|
|
476
|
-
if (result.asyncDir
|
|
605
|
+
if (result.asyncDir && result.asyncDir !== job.asyncDir) {
|
|
606
|
+
closeJobWatcher(asyncId);
|
|
607
|
+
job.asyncDir = result.asyncDir;
|
|
608
|
+
watchJob(job);
|
|
609
|
+
}
|
|
477
610
|
try {
|
|
478
611
|
updateAsyncJobNestedProjection(job);
|
|
479
612
|
} catch (error) {
|
|
@@ -486,10 +619,20 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
486
619
|
if (!nestedRefreshFailed && !hasLiveNestedDescendants(job?.nestedChildren)) scheduleCleanup(asyncId);
|
|
487
620
|
};
|
|
488
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
|
+
|
|
489
633
|
const resetJobs = (ctx?: ExtensionContext) => {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
}
|
|
634
|
+
dispose();
|
|
635
|
+
for (const timer of state.cleanupTimers.values()) clearTimeout(timer);
|
|
493
636
|
state.cleanupTimers.clear();
|
|
494
637
|
state.asyncJobs.clear();
|
|
495
638
|
state.fleetJobs?.clear();
|
|
@@ -515,12 +658,14 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
515
658
|
for (const run of runs) {
|
|
516
659
|
const job = summaryToJob(run);
|
|
517
660
|
state.asyncJobs.set(run.id, job);
|
|
661
|
+
if (job.status === "running") runningJobIds.add(job.asyncId);
|
|
518
662
|
rememberFleetJob(state, job);
|
|
663
|
+
watchJob(job);
|
|
519
664
|
}
|
|
520
665
|
if (runs.length === 0) return;
|
|
521
666
|
ensurePoller();
|
|
522
667
|
rerenderLastWidget();
|
|
523
668
|
};
|
|
524
669
|
|
|
525
|
-
return { ensurePoller, refreshWidget, handleStarted, handleComplete, resetJobs, restoreActiveJobs };
|
|
670
|
+
return { ensurePoller, refreshWidget, handleStarted, handleComplete, resetJobs, restoreActiveJobs, dispose };
|
|
526
671
|
}
|
|
@@ -5,6 +5,7 @@ import type { AgentConfig } from "../../agents/agents.ts";
|
|
|
5
5
|
import { validateAcceptanceInput } from "../shared/acceptance.ts";
|
|
6
6
|
import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
|
|
7
7
|
import { intersectSubagentCapabilityCeilings, parseSubagentCapabilityCeiling, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
|
|
8
|
+
import { validateRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
|
|
8
9
|
import { resolveTurnBudgetConfig } from "../shared/turn-budget.ts";
|
|
9
10
|
import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
|
|
10
11
|
|
|
@@ -307,7 +308,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
307
308
|
"subagentOnlyExtensions", "mcpDirectTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritSkills", "skills",
|
|
308
309
|
"skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "structuredOutputSchema", "acceptance", "sessionDir", "artifactConfig",
|
|
309
310
|
"artifactsDir", "maxOutput", "controlConfig", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
|
|
310
|
-
"launchResolvedExtensions",
|
|
311
|
+
"launchResolvedExtensions", "runFanoutBudget",
|
|
311
312
|
]);
|
|
312
313
|
for (const field of Object.keys(parsed)) {
|
|
313
314
|
if (!allowedFields.has(field)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': unknown field '${field}'.`);
|
|
@@ -317,6 +318,11 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
317
318
|
if (typeof parsed[field] !== "string" || !(parsed[field] as string).trim()) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must be a non-empty string.`);
|
|
318
319
|
}
|
|
319
320
|
if (parsed.version !== 1) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': version must be 1.`);
|
|
321
|
+
try {
|
|
322
|
+
parsed.runFanoutBudget = validateRunFanoutBudgetDescriptor(parsed.runFanoutBudget);
|
|
323
|
+
} catch (error) {
|
|
324
|
+
throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${error instanceof Error ? error.message : String(error)}`);
|
|
325
|
+
}
|
|
320
326
|
if (parsed.capabilityCeiling !== undefined) parsed.capabilityCeiling = parseSubagentCapabilityCeiling(parsed.capabilityCeiling, `async recovery descriptor '${descriptorPath}' capabilityCeiling`);
|
|
321
327
|
if (parsed.agentContract !== undefined) {
|
|
322
328
|
if (!parsed.agentContract || typeof parsed.agentContract !== "object" || Array.isArray(parsed.agentContract)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': agentContract must be an object.`);
|
|
@@ -5,12 +5,14 @@ import { formatActivityLabel, formatParallelOutcome } from "../../shared/status-
|
|
|
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
7
|
import { pruneStatusCacheForAsyncRoot, readStatus } from "../../shared/utils.ts";
|
|
8
|
-
import { attachRootChildrenToSteps, buildNestedRouteIndex, type NestedRoute, projectNestedEvents } from "../shared/nested-events.ts";
|
|
8
|
+
import { attachRootChildrenToSteps, buildNestedRouteIndex, findNestedRouteForRootId, type NestedRoute, projectNestedEvents } from "../shared/nested-events.ts";
|
|
9
9
|
import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
|
|
10
|
+
import { formatRunFanoutBudget, getRunFanoutBudgetSnapshot, readRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
|
|
10
11
|
import { flatToLogicalStepIndex, normalizeParallelGroups } from "./parallel-groups.ts";
|
|
11
12
|
import { contextModeLabel, summarizeContextModes, type ContextMode, type ContextSummary } from "../shared/context-mode.ts";
|
|
12
13
|
import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-reconciler.ts";
|
|
13
14
|
import { readProcessTerminal, sanitizeProcessTerminal } from "./process-terminal.ts";
|
|
15
|
+
import { ACTIVE_RUN_INDEX_DIR, isActiveAsyncState, readActiveRunIndex, releaseActiveRunIndex, updateActiveRunIndex } from "./active-run-index.ts";
|
|
14
16
|
|
|
15
17
|
interface AsyncRunStepSummary {
|
|
16
18
|
index: number;
|
|
@@ -105,6 +107,7 @@ export interface AsyncRunSummary {
|
|
|
105
107
|
nestedChildren?: NestedRunSummary[];
|
|
106
108
|
nestedWarnings?: string[];
|
|
107
109
|
processTerminal?: AsyncStatus["processTerminal"];
|
|
110
|
+
runFanoutBudget?: AsyncStatus["runFanoutBudget"];
|
|
108
111
|
launchResolvedExtensions?: LaunchResolvedChildExtensionsV1;
|
|
109
112
|
runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
|
|
110
113
|
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
@@ -160,7 +163,7 @@ type TargetedAsyncRunResolution =
|
|
|
160
163
|
* accepting a path whose canonical location escaped the async root.
|
|
161
164
|
*/
|
|
162
165
|
export function resolveTargetedAsyncRun(asyncDirRoot: string, id: string, sessionId?: string): TargetedAsyncRunResolution {
|
|
163
|
-
if (!id || id === "." || id === ".." || path.basename(id) !== id) return { kind: "reject" };
|
|
166
|
+
if (!id || id === "." || id === ".." || id === ACTIVE_RUN_INDEX_DIR || path.basename(id) !== id) return { kind: "reject" };
|
|
164
167
|
const asyncDir = path.join(asyncDirRoot, id);
|
|
165
168
|
let entryStat: fs.Stats;
|
|
166
169
|
try {
|
|
@@ -221,6 +224,8 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
221
224
|
const { activityState, lastActivityAt } = deriveAsyncActivityState(asyncDir, status);
|
|
222
225
|
const processTerminal = readProcessTerminal(asyncDir, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId })
|
|
223
226
|
?? sanitizeProcessTerminal(status.processTerminal, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId }, path.join(asyncDir, "status.json"));
|
|
227
|
+
const runFanoutBudgetDescriptor = readRunFanoutBudgetDescriptor(asyncDir);
|
|
228
|
+
const runFanoutBudget = runFanoutBudgetDescriptor ? getRunFanoutBudgetSnapshot(runFanoutBudgetDescriptor) : status.runFanoutBudget;
|
|
224
229
|
const steps = status.steps ?? [];
|
|
225
230
|
const chainStepCount = status.chainStepCount ?? steps.length;
|
|
226
231
|
const parallelGroups = normalizeParallelGroups(status.parallelGroups, steps.length, chainStepCount);
|
|
@@ -326,6 +331,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
326
331
|
...(nestedChildren.length ? { nestedChildren } : {}),
|
|
327
332
|
...(nestedWarnings.length ? { nestedWarnings } : {}),
|
|
328
333
|
...(processTerminal ? { processTerminal } : {}),
|
|
334
|
+
...(runFanoutBudget ? { runFanoutBudget } : {}),
|
|
329
335
|
...(status.launchContractDigest ? { launchContractDigest: status.launchContractDigest } : {}),
|
|
330
336
|
...(status.launchResolvedExtensions ? { launchResolvedExtensions: status.launchResolvedExtensions } : {}),
|
|
331
337
|
...(status.runtimeAcknowledgedExtensions ? { runtimeAcknowledgedExtensions: status.runtimeAcknowledgedExtensions } : {}),
|
|
@@ -371,6 +377,12 @@ function sortRuns(runs: AsyncRunSummary[]): AsyncRunSummary[] {
|
|
|
371
377
|
export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions = {}): AsyncRunSummary[] {
|
|
372
378
|
let entries: string[];
|
|
373
379
|
let scannedCompleteRoot = false;
|
|
380
|
+
let usedActiveIndex = false;
|
|
381
|
+
const activeOnly = options.runId === undefined
|
|
382
|
+
&& options.entryLimit === undefined
|
|
383
|
+
&& options.states !== undefined
|
|
384
|
+
&& options.states.length > 0
|
|
385
|
+
&& options.states.every(isActiveAsyncState);
|
|
374
386
|
try {
|
|
375
387
|
if (options.runId !== undefined) {
|
|
376
388
|
const resolution = resolveTargetedAsyncRun(asyncDirRoot, options.runId, options.sessionId);
|
|
@@ -382,8 +394,15 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
382
394
|
&& resolveTargetedAsyncRun(asyncDirRoot, entry, options.sessionId).kind === "exact"
|
|
383
395
|
)
|
|
384
396
|
: [];
|
|
397
|
+
} else if (activeOnly) {
|
|
398
|
+
entries = (readActiveRunIndex(asyncDirRoot) ?? []).filter((entry) => {
|
|
399
|
+
if (resolveTargetedAsyncRun(asyncDirRoot, entry).kind === "exact") return true;
|
|
400
|
+
updateActiveRunIndex(path.join(asyncDirRoot, entry), "failed");
|
|
401
|
+
return false;
|
|
402
|
+
});
|
|
403
|
+
usedActiveIndex = true;
|
|
385
404
|
} else {
|
|
386
|
-
entries = fs.readdirSync(asyncDirRoot).filter((entry) => isAsyncRunDir(asyncDirRoot, entry));
|
|
405
|
+
entries = fs.readdirSync(asyncDirRoot).filter((entry) => entry !== ACTIVE_RUN_INDEX_DIR && isAsyncRunDir(asyncDirRoot, entry));
|
|
387
406
|
scannedCompleteRoot = true;
|
|
388
407
|
}
|
|
389
408
|
} catch (error) {
|
|
@@ -424,6 +443,7 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
424
443
|
// entirely when no active runs match.
|
|
425
444
|
let nestedRouteIndex: Map<string, NestedRoute> | undefined;
|
|
426
445
|
const resolveNestedRoute = (rootRunId: string): NestedRoute | undefined => {
|
|
446
|
+
if (usedActiveIndex) return findNestedRouteForRootId(rootRunId);
|
|
427
447
|
if (!nestedRouteIndex) nestedRouteIndex = buildNestedRouteIndex();
|
|
428
448
|
return nestedRouteIndex.get(rootRunId);
|
|
429
449
|
};
|
|
@@ -433,7 +453,15 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
|
|
|
433
453
|
? undefined
|
|
434
454
|
: reconcileAsyncRun(asyncDir, { resultsDir: options.resultsDir, kill: options.kill, now: options.now });
|
|
435
455
|
const status = (reconciliation?.status ?? readStatus(asyncDir)) as (AsyncStatus & { cwd?: string }) | null;
|
|
436
|
-
if (!status)
|
|
456
|
+
if (!status) {
|
|
457
|
+
if (usedActiveIndex) updateActiveRunIndex(asyncDir, "failed");
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
if (usedActiveIndex && !isActiveAsyncState(status.state)) {
|
|
461
|
+
const processTerminal = readProcessTerminal(asyncDir, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId });
|
|
462
|
+
if (processTerminal?.state === "observed") releaseActiveRunIndex(asyncDir);
|
|
463
|
+
}
|
|
464
|
+
if (status.displayDismissedAt !== undefined) continue;
|
|
437
465
|
// Filter before the nested-route lookup: the lookup builds an index over
|
|
438
466
|
// the nested-events directory, so deferring it for filtered-out runs keeps
|
|
439
467
|
// restoration at load from scanning that directory when no active runs
|
|
@@ -536,6 +564,7 @@ export function formatAsyncRunList(runs: AsyncRunSummary[], heading = "Active as
|
|
|
536
564
|
const attached = new Set(run.steps.flatMap((step) => step.children?.map((child) => child.id) ?? []));
|
|
537
565
|
const unattached = run.nestedChildren?.filter((child) => !attached.has(child.id)) ?? [];
|
|
538
566
|
lines.push(...formatNestedRunStatusLines(unattached, { indent: " ", maxLines: 12 }));
|
|
567
|
+
if (run.runFanoutBudget) lines.push(` ${formatRunFanoutBudget(run.runFanoutBudget)}`);
|
|
539
568
|
if (run.error) lines.push(` Error: ${run.error}`);
|
|
540
569
|
for (const warning of run.nestedWarnings ?? []) lines.push(` Warning: ${warning}`);
|
|
541
570
|
const outputPath = formatAsyncRunOutputPath(run);
|