pi-crew 0.11.42 → 0.11.44
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 +60 -0
- package/dist/index.mjs +493 -493
- package/package.json +1 -1
- package/src/extension/session-summary.ts +32 -2
- package/src/extension/team-tool/health-monitor.ts +19 -6
- package/src/runtime/process-status.ts +44 -1
- package/src/ui/agents-jobs-browser.ts +33 -4
- package/src/ui/powerbar-publisher.ts +6 -2
- package/src/ui/run-dashboard.ts +50 -13
- package/src/ui/widget/widget-model.ts +49 -36
package/package.json
CHANGED
|
@@ -1,9 +1,34 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
1
2
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import { readCrewAgents } from "../runtime/crew-agent-records.ts";
|
|
3
|
-
import { isDisplayActiveRun } from "../runtime/process-status.ts";
|
|
4
|
+
import { heartbeatLiveForDisplay, isDisplayActiveRun } from "../runtime/process-status.ts";
|
|
5
|
+
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
6
|
+
import { readJsonFileCoalesced } from "../utils/file-coalescer.ts";
|
|
4
7
|
import { extractSessionId } from "../utils/session-utils.ts";
|
|
5
8
|
import { listRuns } from "./run-index.ts";
|
|
6
9
|
|
|
10
|
+
/** Tasks-read TTL for the one-shot toast's heartbeat check (coalesced, live-run-sidebar pattern). */
|
|
11
|
+
const SUMMARY_TASKS_READ_TTL_MS = 1000;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Wave 16 (residual render wiring, GAP-4): coalesced tasks.json read for the
|
|
15
|
+
* active-runs toast — clones agents-jobs-browser's readLegacyRunTasks. Returns
|
|
16
|
+
* undefined on unreadable so a broken tasks.json can NEVER fabricate liveness
|
|
17
|
+
* evidence (fail-closed).
|
|
18
|
+
*/
|
|
19
|
+
function readSummaryRunTasks(run: TeamRunManifest): TeamTaskState[] | undefined {
|
|
20
|
+
const parse = (): TeamTaskState[] => {
|
|
21
|
+
if (!existsSync(run.tasksPath)) return [];
|
|
22
|
+
const parsed = JSON.parse(readFileSync(run.tasksPath, "utf-8")) as unknown;
|
|
23
|
+
return Array.isArray(parsed) ? (parsed as TeamTaskState[]) : [];
|
|
24
|
+
};
|
|
25
|
+
try {
|
|
26
|
+
return readJsonFileCoalesced(run.tasksPath, SUMMARY_TASKS_READ_TTL_MS, parse);
|
|
27
|
+
} catch {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
7
32
|
export function notifyActiveRuns(ctx: ExtensionContext): void {
|
|
8
33
|
const sid = extractSessionId(ctx);
|
|
9
34
|
const active = listRuns(ctx.cwd)
|
|
@@ -15,7 +40,12 @@ export function notifyActiveRuns(ctx: ExtensionContext): void {
|
|
|
15
40
|
// Use the same display filter as the widget/powerbar — runs without
|
|
16
41
|
// real agent evidence (e.g. integration test fixtures) must not appear.
|
|
17
42
|
const agents = readCrewAgents(run);
|
|
18
|
-
|
|
43
|
+
if (isDisplayActiveRun(run, agents)) return true;
|
|
44
|
+
// Wave 16: this is a ONE-SHOT toast, so the lazy coalesced tasks read is
|
|
45
|
+
// in-pattern I/O — fetched ONLY when the display predicate is about to
|
|
46
|
+
// call the run inactive, so a manifest frozen mid-long-tool (fresh task
|
|
47
|
+
// heartbeats, no agent progress) still gets advertised.
|
|
48
|
+
return heartbeatLiveForDisplay(run, readSummaryRunTasks(run));
|
|
19
49
|
})
|
|
20
50
|
.slice(0, 5);
|
|
21
51
|
if (active.length === 0) return;
|
|
@@ -5,7 +5,12 @@ import * as fs from "node:fs";
|
|
|
5
5
|
import * as os from "node:os";
|
|
6
6
|
import * as path from "node:path";
|
|
7
7
|
import { readCrewAgents } from "../../runtime/crew-agent-records.ts";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
hasFreshTaskHeartbeatEvidence,
|
|
10
|
+
hasStaleAsyncProcess,
|
|
11
|
+
isActiveRunStatus,
|
|
12
|
+
isLikelyOrphanedActiveRun,
|
|
13
|
+
} from "../../runtime/process-status.ts";
|
|
9
14
|
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
10
15
|
import { loadTasksWithRecovery } from "../../state/stores/state-store.ts";
|
|
11
16
|
import type { TeamRunManifest, TeamTaskState } from "../../state/types.ts";
|
|
@@ -374,10 +379,15 @@ export function buildHealthReport(
|
|
|
374
379
|
continue;
|
|
375
380
|
}
|
|
376
381
|
|
|
377
|
-
// 3. Stuck task detection for active runs
|
|
382
|
+
// 3. Stuck task detection for active runs. Wave 16 (residual render
|
|
383
|
+
// wiring): the tasks loaded here are HOISTED — step 6 reuses them so the
|
|
384
|
+
// orphan guard consults fresh task heartbeats with ZERO extra disk reads
|
|
385
|
+
// (parity with run-dashboard's stale predicate).
|
|
386
|
+
let tasks: TeamTaskState[] | undefined;
|
|
378
387
|
if (isActiveRunStatus(run.status)) {
|
|
379
|
-
const
|
|
380
|
-
|
|
388
|
+
const loaded = readRunTasks(run.stateRoot);
|
|
389
|
+
tasks = loaded;
|
|
390
|
+
allStuck.push(...stuckTasksFromRecords(loaded, run.runId, now));
|
|
381
391
|
}
|
|
382
392
|
|
|
383
393
|
// 4. Only check orphan status for active runs
|
|
@@ -393,9 +403,12 @@ export function buildHealthReport(
|
|
|
393
403
|
continue;
|
|
394
404
|
}
|
|
395
405
|
|
|
396
|
-
// 6. Orphaned: non-async active run with no recent update
|
|
406
|
+
// 6. Orphaned: non-async active run with no recent update. Wave 16: a
|
|
407
|
+
// run whose manifest froze mid-long-tool (worker busy, event-driven
|
|
408
|
+
// writes paused) keeps stamping task heartbeats — fresh evidence
|
|
409
|
+
// suppresses the stale-no-progress label (run-dashboard parity).
|
|
397
410
|
const agents = readCrewAgents(run as TeamRunManifest);
|
|
398
|
-
if (isLikelyOrphanedActiveRun(run as TeamRunManifest, agents, now)) {
|
|
411
|
+
if (isLikelyOrphanedActiveRun(run as TeamRunManifest, agents, now) && !hasFreshTaskHeartbeatEvidence(tasks, now)) {
|
|
399
412
|
orphaned.push({
|
|
400
413
|
runId: run.runId,
|
|
401
414
|
reason: "stale-no-progress",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TeamRunManifest } from "../state/types.ts";
|
|
1
|
+
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
2
2
|
import type { CrewAgentRecord } from "./crew-agent-runtime.ts";
|
|
3
3
|
|
|
4
4
|
export { hasAsyncStartMarker } from "./async-marker.ts";
|
|
@@ -92,6 +92,49 @@ export function hasRecentAgentActivityEvidence(agent: CrewAgentRecord, now = Dat
|
|
|
92
92
|
return Number.isFinite(at) && now - at < livenessMs;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Wave 15 (F1, GAP-0): does any running/waiting task carry FRESH live
|
|
97
|
+
* evidence — a `heartbeat.lastSeenAt` (with `alive !== false`) or the
|
|
98
|
+
* `agentProgress.lastActivityAt` fallback — within `ttlMs`? Behavioral
|
|
99
|
+
* mirror of the watchdog's private helper (foreground-watchdog.ts) and
|
|
100
|
+
* hasRecentActiveEvidence (stale-reconciler.ts), so watchdog, reconciler
|
|
101
|
+
* and display stop growing a third divergent private copy. PURE (GAP-4:
|
|
102
|
+
* the shared display predicates keep their signatures — call sites that
|
|
103
|
+
* already hold the tasks OR this in). Default TTL 5 min keeps the constant
|
|
104
|
+
* family aligned (AGENT_ACTIVITY_LIVENESS_MS = TASK_HEARTBEAT_EVIDENCE_MS
|
|
105
|
+
* = ACTIVE_EVIDENCE_TTL_MS). Undefined/empty/all-terminal ⇒ false.
|
|
106
|
+
*/
|
|
107
|
+
export function hasFreshTaskHeartbeatEvidence(
|
|
108
|
+
tasks: TeamTaskState[] | undefined,
|
|
109
|
+
now = Date.now(),
|
|
110
|
+
ttlMs = AGENT_ACTIVITY_LIVENESS_MS,
|
|
111
|
+
): boolean {
|
|
112
|
+
if (!Array.isArray(tasks)) return false;
|
|
113
|
+
return tasks.some((task) => {
|
|
114
|
+
if (task.status !== "running" && task.status !== "waiting") return false;
|
|
115
|
+
const heartbeatAt = task.heartbeat?.lastSeenAt ? new Date(task.heartbeat.lastSeenAt).getTime() : Number.NaN;
|
|
116
|
+
if (task.heartbeat?.alive !== false && Number.isFinite(heartbeatAt) && now - heartbeatAt <= ttlMs) return true;
|
|
117
|
+
const activityAt = task.agentProgress?.lastActivityAt ? new Date(task.agentProgress.lastActivityAt).getTime() : Number.NaN;
|
|
118
|
+
return Number.isFinite(activityAt) && now - activityAt <= ttlMs;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Wave 16 (residual render wiring, GAP-4): tight heartbeat-liveness guard for
|
|
124
|
+
* ACTIVE display — mirrors the orphan predicate's own preconditions (active
|
|
125
|
+
* status, non-async) so a terminal or async run is never resurrected by a
|
|
126
|
+
* heartbeat. A healthy direct run mid-long-tool freezes manifest `updatedAt`
|
|
127
|
+
* and agent progress (event-driven writes) but keeps stamping
|
|
128
|
+
* `task.heartbeat.lastSeenAt`, so fresh evidence means the run is alive.
|
|
129
|
+
* Single home for the guard that run-dashboard previously kept private and
|
|
130
|
+
* agents-jobs-browser inlined; call sites OR this in ONLY — every non-stale
|
|
131
|
+
* branch and layout stays untouched. PURE: takes the tasks the caller already
|
|
132
|
+
* holds in scope (no disk read here).
|
|
133
|
+
*/
|
|
134
|
+
export function heartbeatLiveForDisplay(run: TeamRunManifest, tasks: TeamTaskState[] | undefined, now = Date.now()): boolean {
|
|
135
|
+
return isActiveRunStatus(run.status) && !run.async && hasFreshTaskHeartbeatEvidence(tasks, now);
|
|
136
|
+
}
|
|
137
|
+
|
|
95
138
|
export function isLikelyOrphanedActiveRun(
|
|
96
139
|
run: TeamRunManifest,
|
|
97
140
|
agents: CrewAgentRecord[] = [],
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
* dashboard overlay, so headless sessions never reach it.
|
|
50
50
|
*/
|
|
51
51
|
|
|
52
|
-
import { existsSync, readdirSync } from "node:fs";
|
|
52
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
53
53
|
import { join } from "node:path";
|
|
54
54
|
import { fileURLToPath } from "node:url";
|
|
55
55
|
import { matchesKey } from "@earendil-works/pi-tui";
|
|
@@ -60,13 +60,14 @@ import type { CrewAgentRecord } from "../runtime/crew-agent-runtime.ts";
|
|
|
60
60
|
import type { LiveAgentHandle } from "../runtime/live-session/live-agent-manager.ts";
|
|
61
61
|
import type { ManifestCache } from "../runtime/manifest-cache.ts";
|
|
62
62
|
import { createManifestCache } from "../runtime/manifest-cache.ts";
|
|
63
|
-
import { isDisplayActiveRun } from "../runtime/process-status.ts";
|
|
63
|
+
import { heartbeatLiveForDisplay, isDisplayActiveRun } from "../runtime/process-status.ts";
|
|
64
64
|
import type { ScheduledJob } from "../runtime/scheduling/scheduler.ts";
|
|
65
65
|
import { surfaceGateEnvSnapshot } from "../runtime/surface/resolve-surface.ts";
|
|
66
66
|
import { getTaskUsage } from "../runtime/usage-tracker.ts";
|
|
67
67
|
import { readJsonFile } from "../state/atomic-write.ts";
|
|
68
68
|
import { loadRunManifestById } from "../state/stores/state-store.ts";
|
|
69
|
-
import type { TeamRunManifest } from "../state/types.ts";
|
|
69
|
+
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
70
|
+
import { readJsonFileCoalesced } from "../utils/file-coalescer.ts";
|
|
70
71
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
71
72
|
import { formatRelativeTime } from "../utils/relative-time.ts";
|
|
72
73
|
import { pad, sanitizeLine, truncate, visibleWidth } from "../utils/visual.ts";
|
|
@@ -202,6 +203,28 @@ function watchAgentTranscriptScript(): string | undefined {
|
|
|
202
203
|
return candidates.find((c) => existsSync(c));
|
|
203
204
|
}
|
|
204
205
|
|
|
206
|
+
/** Tasks-read TTL for the legacy-discovery heartbeat check (coalesced, live-run-sidebar pattern). */
|
|
207
|
+
const LEGACY_TASKS_READ_TTL_MS = 1000;
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Wave 15 (F2, GAP-0): coalesced tasks.json read for the legacy-discovery
|
|
211
|
+
* loop — mirrors run-dashboard/live-run-sidebar's readRunTasks. Returns
|
|
212
|
+
* undefined on unreadable so a broken tasks.json can NEVER fabricate
|
|
213
|
+
* liveness evidence (fail-closed).
|
|
214
|
+
*/
|
|
215
|
+
function readLegacyRunTasks(run: TeamRunManifest): TeamTaskState[] | undefined {
|
|
216
|
+
const parse = (): TeamTaskState[] => {
|
|
217
|
+
if (!existsSync(run.tasksPath)) return [];
|
|
218
|
+
const parsed = JSON.parse(readFileSync(run.tasksPath, "utf-8")) as unknown;
|
|
219
|
+
return Array.isArray(parsed) ? (parsed as TeamTaskState[]) : [];
|
|
220
|
+
};
|
|
221
|
+
try {
|
|
222
|
+
return readJsonFileCoalesced(run.tasksPath, LEGACY_TASKS_READ_TTL_MS, parse);
|
|
223
|
+
} catch {
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
205
228
|
/** tok/s for record-based entries (no live handle in this process). */
|
|
206
229
|
function recordTokPerSec(record: CrewAgentRecord, nowMs: number): number | undefined {
|
|
207
230
|
if (record.status !== "running") return undefined;
|
|
@@ -476,7 +499,13 @@ export class AgentsJobsBrowser {
|
|
|
476
499
|
if (run.status !== "running" && run.status !== "queued") continue;
|
|
477
500
|
try {
|
|
478
501
|
const agents = readCrewAgents(run);
|
|
479
|
-
|
|
502
|
+
// Wave 15 (F2, GAP-0): tight heartbeat guard — a legacy-uncovered run
|
|
503
|
+
// whose manifest froze mid-long-tool stays listed while its task
|
|
504
|
+
// heartbeats are fresh (this loop already does per-run I/O — one more
|
|
505
|
+
// coalesced read is in-pattern). Terminal/async runs are never
|
|
506
|
+
// resurrected (isActiveRunStatus && !run.async).
|
|
507
|
+
const heartbeatLive = heartbeatLiveForDisplay(run, readLegacyRunTasks(run), nowMs);
|
|
508
|
+
if (isDisplayActiveRun(run, agents, nowMs) || heartbeatLive) runs.push({ run, agents });
|
|
480
509
|
} catch {
|
|
481
510
|
/* unreadable — skip */
|
|
482
511
|
}
|
|
@@ -5,7 +5,7 @@ import { readCrewAgents } from "../runtime/crew-agent-records.ts";
|
|
|
5
5
|
import { listLiveAgents, listLiveAgentsByWorkspace } from "../runtime/live-session/live-agent-manager.ts";
|
|
6
6
|
import type { ManifestCache } from "../runtime/manifest-cache.ts";
|
|
7
7
|
import { isPlanApprovalPending } from "../runtime/plan-approval.ts";
|
|
8
|
-
import { isDisplayActiveRun } from "../runtime/process-status.ts";
|
|
8
|
+
import { heartbeatLiveForDisplay, isDisplayActiveRun } from "../runtime/process-status.ts";
|
|
9
9
|
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
10
10
|
import { aggregateUsage } from "../state/usage.ts";
|
|
11
11
|
import { readJsonFileCoalesced } from "../utils/file-coalescer.ts";
|
|
@@ -307,7 +307,11 @@ class PowerbarPublisher {
|
|
|
307
307
|
}
|
|
308
308
|
return { run, agents, tasks: readTasks(run.tasksPath), snapshot };
|
|
309
309
|
})
|
|
310
|
-
|
|
310
|
+
// Wave 16 (residual render wiring): item.tasks is already in scope
|
|
311
|
+
// (snapshot.tasks | readTasks fallback) — OR-ing the heartbeat guard in
|
|
312
|
+
// adds ZERO extra disk reads; a frozen-manifest run stays in the
|
|
313
|
+
// powerbar while its task heartbeats stay fresh.
|
|
314
|
+
.filter((item) => isDisplayActiveRun(item.run, item.agents) || heartbeatLiveForDisplay(item.run, item.tasks));
|
|
311
315
|
if (!active.length) {
|
|
312
316
|
this.#lastActiveKey = undefined;
|
|
313
317
|
this.#lastProgressKey = undefined;
|
package/src/ui/run-dashboard.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { readCrewAgents } from "../runtime/crew-agent-records.ts";
|
|
|
10
10
|
import type { CrewAgentRecord } from "../runtime/crew-agent-runtime.ts";
|
|
11
11
|
import { getLiveAgentContextPercent } from "../runtime/live-session/live-agent-manager.ts";
|
|
12
12
|
import { isPlanApprovalPending } from "../runtime/plan-approval.ts";
|
|
13
|
-
import { isActiveRunStatus, isDisplayActiveRun, isLikelyOrphanedActiveRun } from "../runtime/process-status.ts";
|
|
13
|
+
import { heartbeatLiveForDisplay, isActiveRunStatus, isDisplayActiveRun, isLikelyOrphanedActiveRun } from "../runtime/process-status.ts";
|
|
14
14
|
import type { ScheduledJob } from "../runtime/scheduling/scheduler.ts";
|
|
15
15
|
import type { TeamRunManifest, TeamTaskState, UsageState } from "../state/types.ts";
|
|
16
16
|
import { aggregateUsage } from "../state/usage.ts";
|
|
@@ -467,7 +467,9 @@ function runLabel(
|
|
|
467
467
|
resolve?: SnapshotResolver,
|
|
468
468
|
): string {
|
|
469
469
|
const agents = agentsFor(run, snapshotCache, resolve);
|
|
470
|
-
|
|
470
|
+
// Wave 15 (F2): a stale-manifest verdict is VETOED by fresh task-heartbeat
|
|
471
|
+
// evidence (lazy read — only consulted when the orphan predicate fires).
|
|
472
|
+
const stale = isLikelyOrphanedActiveRun(run, agents) && !heartbeatLiveForDisplay(run, readRunTasks(run, snapshotCache, resolve));
|
|
471
473
|
const running = agents.find((agent) => agent.status === "running");
|
|
472
474
|
const queued = agents.find((agent) => agent.status === "queued");
|
|
473
475
|
const step = stale
|
|
@@ -523,7 +525,12 @@ function resolveRuns(runs: TeamRunManifest[], snapshotCache?: RunSnapshotCache,
|
|
|
523
525
|
const snapshot = resolve ? resolve(run) : snapshotFor(run, snapshotCache);
|
|
524
526
|
const agents = snapshot?.agents ?? agentsFor(run, snapshotCache, resolve);
|
|
525
527
|
const displayRun = snapshot?.manifest ?? run;
|
|
526
|
-
|
|
528
|
+
// Wave 15 (F2): fresh task heartbeats veto the "stale" label.
|
|
529
|
+
const status: RunStatus =
|
|
530
|
+
isLikelyOrphanedActiveRun(displayRun, agents) &&
|
|
531
|
+
!heartbeatLiveForDisplay(displayRun, snapshot?.tasks ?? readRunTasks(run, snapshotCache, resolve))
|
|
532
|
+
? "stale"
|
|
533
|
+
: (displayRun.status as RunStatus);
|
|
527
534
|
map.set(run.runId, { manifest: run, snapshot, agents, status });
|
|
528
535
|
}
|
|
529
536
|
return map;
|
|
@@ -536,12 +543,19 @@ function groupedRuns(
|
|
|
536
543
|
): Array<{ label: string; run?: TeamRunManifest }> {
|
|
537
544
|
const resolved = resolveRuns(runs, snapshotCache, resolve);
|
|
538
545
|
const rows: Array<{ label: string; run?: TeamRunManifest }> = [];
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
)
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
546
|
+
// Wave 15 (F2): a run with fresh task-heartbeat evidence stays display-active
|
|
547
|
+
// even when the manifest froze mid-long-tool (tight guard — never resurrects
|
|
548
|
+
// terminal/async runs).
|
|
549
|
+
const displayActive = (run: TeamRunManifest): boolean => {
|
|
550
|
+
const entry = resolved.get(run.runId);
|
|
551
|
+
const displayRun = entry?.snapshot?.manifest ?? run;
|
|
552
|
+
return (
|
|
553
|
+
isDisplayActiveRun(displayRun, entry?.agents ?? []) ||
|
|
554
|
+
heartbeatLiveForDisplay(displayRun, entry?.snapshot?.tasks ?? readRunTasks(run, snapshotCache, resolve))
|
|
555
|
+
);
|
|
556
|
+
};
|
|
557
|
+
const active = runs.filter((run) => displayActive(run));
|
|
558
|
+
const rest = runs.filter((run) => !displayActive(run));
|
|
545
559
|
if (active.length) rows.push({ label: "Active" }, ...active.map((run) => ({ label: run.runId, run })));
|
|
546
560
|
if (rest.length) rows.push({ label: "Recent" }, ...rest.map((run) => ({ label: run.runId, run })));
|
|
547
561
|
return rows;
|
|
@@ -754,7 +768,10 @@ export class RunDashboard implements DashboardComponent {
|
|
|
754
768
|
const snapshot = resolve ? resolve(run) : snapshotFor(run, this.options.snapshotCache);
|
|
755
769
|
const displayRun = snapshot?.manifest ?? run;
|
|
756
770
|
const agents = snapshot?.agents ?? agentsFor(run, this.options.snapshotCache, resolve);
|
|
757
|
-
|
|
771
|
+
// Wave 15 (F2): fresh task heartbeats veto the "stale" label.
|
|
772
|
+
const stale =
|
|
773
|
+
isLikelyOrphanedActiveRun(displayRun, agents) &&
|
|
774
|
+
!heartbeatLiveForDisplay(displayRun, snapshot?.tasks ?? readRunTasks(run, this.options.snapshotCache, resolve));
|
|
758
775
|
const status: RunStatus = stale ? "stale" : (displayRun.status as RunStatus);
|
|
759
776
|
if (status === "running" || agents.some((agent) => agent.status === "running")) hasRunning = true;
|
|
760
777
|
return snapshot?.signature ?? `${displayRun.runId}:${displayRun.status}:${displayRun.updatedAt}:${status}`;
|
|
@@ -963,7 +980,15 @@ export class RunDashboard implements DashboardComponent {
|
|
|
963
980
|
const snap = snapshotOnce(rowItem.run);
|
|
964
981
|
const run = snap?.manifest ?? rowItem.run;
|
|
965
982
|
const agents = snap?.agents ?? agentsFor(rowItem.run, this.options.snapshotCache, snapshotOnce);
|
|
966
|
-
|
|
983
|
+
// Wave 15 (F2): fresh task heartbeats veto the "stale" label (status line).
|
|
984
|
+
const status: RunStatus =
|
|
985
|
+
isLikelyOrphanedActiveRun(run, agents) &&
|
|
986
|
+
!heartbeatLiveForDisplay(
|
|
987
|
+
run,
|
|
988
|
+
snap?.tasks ?? readRunTasks(rowItem.run, this.options.snapshotCache, snapshotOnce),
|
|
989
|
+
)
|
|
990
|
+
? "stale"
|
|
991
|
+
: (run.status as RunStatus);
|
|
967
992
|
const label = runLabel(run, idx === this.selected, this.options.snapshotCache, budget, snapshotOnce);
|
|
968
993
|
lines.push(row(applyStatusColor(this.theme, status, label)));
|
|
969
994
|
}
|
|
@@ -977,7 +1002,15 @@ export class RunDashboard implements DashboardComponent {
|
|
|
977
1002
|
const snap = snapshotOnce(rowItem.run);
|
|
978
1003
|
const run = snap?.manifest ?? rowItem.run;
|
|
979
1004
|
const agents = snap?.agents ?? agentsFor(rowItem.run, this.options.snapshotCache, snapshotOnce);
|
|
980
|
-
|
|
1005
|
+
// Wave 15 (F2): fresh task heartbeats veto the "stale" label (windowed status line).
|
|
1006
|
+
const status: RunStatus =
|
|
1007
|
+
isLikelyOrphanedActiveRun(run, agents) &&
|
|
1008
|
+
!heartbeatLiveForDisplay(
|
|
1009
|
+
run,
|
|
1010
|
+
snap?.tasks ?? readRunTasks(rowItem.run, this.options.snapshotCache, snapshotOnce),
|
|
1011
|
+
)
|
|
1012
|
+
? "stale"
|
|
1013
|
+
: (run.status as RunStatus);
|
|
981
1014
|
const label = runLabel(run, gi === this.selected, this.options.snapshotCache, budget, snapshotOnce);
|
|
982
1015
|
lines.push(row(applyStatusColor(this.theme, status, label)));
|
|
983
1016
|
}
|
|
@@ -996,8 +1029,12 @@ export class RunDashboard implements DashboardComponent {
|
|
|
996
1029
|
const snap = snapshotOnce(selectedRun);
|
|
997
1030
|
const r = snap?.manifest ?? selectedRun;
|
|
998
1031
|
const agents = snap?.agents ?? agentsFor(selectedRun, this.options.snapshotCache, snapshotOnce);
|
|
999
|
-
const statusStr: RunStatus = isLikelyOrphanedActiveRun(r, agents) ? "stale" : (r.status as RunStatus);
|
|
1000
1032
|
const selectedTasks = snap?.tasks ?? readRunTasks(r, this.options.snapshotCache, snapshotOnce);
|
|
1033
|
+
// Wave 15 (F2): fresh task heartbeats veto the "stale" label (detail header).
|
|
1034
|
+
const statusStr: RunStatus =
|
|
1035
|
+
isLikelyOrphanedActiveRun(r, agents) && !heartbeatLiveForDisplay(r, selectedTasks)
|
|
1036
|
+
? "stale"
|
|
1037
|
+
: (r.status as RunStatus);
|
|
1001
1038
|
lines.push(section("run"));
|
|
1002
1039
|
lines.push(row(`${fg("accent", "▸")} ${truncate(sanitizeUntrustedLine(r.goal), budget - 4)}`));
|
|
1003
1040
|
// L-2: surface the failure/cancellation reason inline for terminal runs.
|
|
@@ -9,7 +9,7 @@ import { readCrewAgents } from "../../runtime/crew-agent-records.ts";
|
|
|
9
9
|
import type { CrewAgentRecord } from "../../runtime/crew-agent-runtime.ts";
|
|
10
10
|
import { evictStaleLiveAgentHandles } from "../../runtime/live-session/live-agent-manager.ts";
|
|
11
11
|
import type { ManifestCache } from "../../runtime/manifest-cache.ts";
|
|
12
|
-
import { isDisplayActiveRun } from "../../runtime/process-status.ts";
|
|
12
|
+
import { heartbeatLiveForDisplay, isDisplayActiveRun } from "../../runtime/process-status.ts";
|
|
13
13
|
import { reconcileAllStaleRuns } from "../../runtime/recovery/crash-recovery.ts";
|
|
14
14
|
import type { TeamRunManifest } from "../../state/types.ts";
|
|
15
15
|
import { goalFlagSuffix } from "../goal-flag.ts";
|
|
@@ -61,42 +61,55 @@ export function activeWidgetRuns(
|
|
|
61
61
|
// workspaceId stays in scope: crash recovery below is still session-scoped.
|
|
62
62
|
const runs = preloadedManifests ?? (manifestCache ? manifestCache.list(20) : listRecentRuns(cwd, 20));
|
|
63
63
|
|
|
64
|
-
return
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
64
|
+
return (
|
|
65
|
+
runs
|
|
66
|
+
.map((run) => {
|
|
67
|
+
try {
|
|
68
|
+
// U7 (spec 2026-10-09): prefer the committed-state read when the
|
|
69
|
+
// cache exposes it. `readForRender` returns the SAME cached
|
|
70
|
+
// snapshot `get` would (identical render — parity), but a lapsed
|
|
71
|
+
// TTL schedules the coalesced ASYNC refresh (80ms batch → stamp
|
|
72
|
+
// check → rebuild only when files changed) instead of waiting for
|
|
73
|
+
// an external poller to notice. Structural check: legacy callers
|
|
74
|
+
// and test doubles may implement only `get`.
|
|
75
|
+
const committed = snapshotCache as
|
|
76
|
+
| {
|
|
77
|
+
readForRender?: (runId: string) => RunUiSnapshot | undefined;
|
|
78
|
+
get?: (runId: string) => RunUiSnapshot | undefined;
|
|
79
|
+
}
|
|
80
|
+
| undefined;
|
|
81
|
+
const snapshot =
|
|
82
|
+
typeof committed?.readForRender === "function" ? committed.readForRender(run.runId) : snapshotCache?.get(run.runId);
|
|
83
|
+
if (snapshot) {
|
|
84
|
+
return {
|
|
85
|
+
run: snapshot.manifest,
|
|
86
|
+
agents: snapshot.agents,
|
|
87
|
+
snapshot,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// P0-6: render from snapshots only — never read disk on every
|
|
91
|
+
// render tick. When the snapshot cache is provided but hasn't
|
|
92
|
+
// populated yet (first frame after `updateCrewWidget`), drop the
|
|
93
|
+
// run from the result so the widget paints "(loading…)" instead
|
|
94
|
+
// of calling `agentsFor(run) → readCrewAgents`. Legacy callers
|
|
95
|
+
// (no snapshotCache) keep the disk-read fallback for tests/dev.
|
|
96
|
+
if (snapshotCache) return null;
|
|
97
|
+
return { run, agents: agentsFor(run) };
|
|
98
|
+
} catch {
|
|
99
|
+
if (snapshotCache) return null;
|
|
100
|
+
return { run, agents: agentsFor(run) };
|
|
85
101
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
})
|
|
99
|
-
.filter((item): item is WidgetRun => item !== null && isDisplayActiveRun(item.run, item.agents));
|
|
102
|
+
})
|
|
103
|
+
// Wave 16 (residual render wiring): the dock's high-frequency tick NEVER
|
|
104
|
+
// gains a disk read here — only the snapshot the render path already
|
|
105
|
+
// holds. OR-in ONLY: a run hidden because its manifest froze mid-long-tool
|
|
106
|
+
// stays visible while its task heartbeats are fresh (GAP-4 mix-A).
|
|
107
|
+
.filter(
|
|
108
|
+
(item): item is WidgetRun =>
|
|
109
|
+
item !== null &&
|
|
110
|
+
(isDisplayActiveRun(item.run, item.agents, now) || heartbeatLiveForDisplay(item.run, item.snapshot?.tasks, now)),
|
|
111
|
+
)
|
|
112
|
+
);
|
|
100
113
|
}
|
|
101
114
|
|
|
102
115
|
/**
|