pi-crew 0.11.42 → 0.11.43
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 +30 -0
- package/dist/index.mjs +483 -483
- package/package.json +1 -1
- package/src/runtime/process-status.ts +28 -1
- package/src/ui/agents-jobs-browser.ts +34 -4
- package/src/ui/run-dashboard.ts +68 -13
package/package.json
CHANGED
|
@@ -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,33 @@ 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
|
+
|
|
95
122
|
export function isLikelyOrphanedActiveRun(
|
|
96
123
|
run: TeamRunManifest,
|
|
97
124
|
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 { hasFreshTaskHeartbeatEvidence, isActiveRunStatus, 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,14 @@ 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 =
|
|
508
|
+
isActiveRunStatus(run.status) && !run.async && hasFreshTaskHeartbeatEvidence(readLegacyRunTasks(run), nowMs);
|
|
509
|
+
if (isDisplayActiveRun(run, agents, nowMs) || heartbeatLive) runs.push({ run, agents });
|
|
480
510
|
} catch {
|
|
481
511
|
/* unreadable — skip */
|
|
482
512
|
}
|
package/src/ui/run-dashboard.ts
CHANGED
|
@@ -10,7 +10,12 @@ 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 {
|
|
13
|
+
import {
|
|
14
|
+
hasFreshTaskHeartbeatEvidence,
|
|
15
|
+
isActiveRunStatus,
|
|
16
|
+
isDisplayActiveRun,
|
|
17
|
+
isLikelyOrphanedActiveRun,
|
|
18
|
+
} from "../runtime/process-status.ts";
|
|
14
19
|
import type { ScheduledJob } from "../runtime/scheduling/scheduler.ts";
|
|
15
20
|
import type { TeamRunManifest, TeamTaskState, UsageState } from "../state/types.ts";
|
|
16
21
|
import { aggregateUsage } from "../state/usage.ts";
|
|
@@ -363,6 +368,19 @@ function taskForAgent(tasks: TeamTaskState[], agent: CrewAgentRecord): TeamTaskS
|
|
|
363
368
|
return tasks.find((task) => task.id === agent.taskId);
|
|
364
369
|
}
|
|
365
370
|
|
|
371
|
+
/**
|
|
372
|
+
* Wave 15 (F2, GAP-0): tight heartbeat-liveness guard for ACTIVE display —
|
|
373
|
+
* mirrors the orphan predicate's own preconditions (active status, non-async)
|
|
374
|
+
* so a terminal or async run is never resurrected by a heartbeat. A healthy
|
|
375
|
+
* direct run mid-long-tool freezes manifest `updatedAt` and agent progress
|
|
376
|
+
* (event-driven writes) but keeps stamping `task.heartbeat.lastSeenAt`, so
|
|
377
|
+
* fresh evidence means the run is alive. Call sites OR this in ONLY — every
|
|
378
|
+
* non-stale branch and layout stays untouched.
|
|
379
|
+
*/
|
|
380
|
+
function heartbeatLiveForDisplay(run: TeamRunManifest, tasks: TeamTaskState[] | undefined, now = Date.now()): boolean {
|
|
381
|
+
return isActiveRunStatus(run.status) && !run.async && hasFreshTaskHeartbeatEvidence(tasks, now);
|
|
382
|
+
}
|
|
383
|
+
|
|
366
384
|
function modelForTask(task: TeamTaskState | undefined): string | undefined {
|
|
367
385
|
const attempts = task?.modelAttempts;
|
|
368
386
|
if (!attempts?.length) return undefined;
|
|
@@ -467,7 +485,9 @@ function runLabel(
|
|
|
467
485
|
resolve?: SnapshotResolver,
|
|
468
486
|
): string {
|
|
469
487
|
const agents = agentsFor(run, snapshotCache, resolve);
|
|
470
|
-
|
|
488
|
+
// Wave 15 (F2): a stale-manifest verdict is VETOED by fresh task-heartbeat
|
|
489
|
+
// evidence (lazy read — only consulted when the orphan predicate fires).
|
|
490
|
+
const stale = isLikelyOrphanedActiveRun(run, agents) && !heartbeatLiveForDisplay(run, readRunTasks(run, snapshotCache, resolve));
|
|
471
491
|
const running = agents.find((agent) => agent.status === "running");
|
|
472
492
|
const queued = agents.find((agent) => agent.status === "queued");
|
|
473
493
|
const step = stale
|
|
@@ -523,7 +543,12 @@ function resolveRuns(runs: TeamRunManifest[], snapshotCache?: RunSnapshotCache,
|
|
|
523
543
|
const snapshot = resolve ? resolve(run) : snapshotFor(run, snapshotCache);
|
|
524
544
|
const agents = snapshot?.agents ?? agentsFor(run, snapshotCache, resolve);
|
|
525
545
|
const displayRun = snapshot?.manifest ?? run;
|
|
526
|
-
|
|
546
|
+
// Wave 15 (F2): fresh task heartbeats veto the "stale" label.
|
|
547
|
+
const status: RunStatus =
|
|
548
|
+
isLikelyOrphanedActiveRun(displayRun, agents) &&
|
|
549
|
+
!heartbeatLiveForDisplay(displayRun, snapshot?.tasks ?? readRunTasks(run, snapshotCache, resolve))
|
|
550
|
+
? "stale"
|
|
551
|
+
: (displayRun.status as RunStatus);
|
|
527
552
|
map.set(run.runId, { manifest: run, snapshot, agents, status });
|
|
528
553
|
}
|
|
529
554
|
return map;
|
|
@@ -536,12 +561,19 @@ function groupedRuns(
|
|
|
536
561
|
): Array<{ label: string; run?: TeamRunManifest }> {
|
|
537
562
|
const resolved = resolveRuns(runs, snapshotCache, resolve);
|
|
538
563
|
const rows: Array<{ label: string; run?: TeamRunManifest }> = [];
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
)
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
564
|
+
// Wave 15 (F2): a run with fresh task-heartbeat evidence stays display-active
|
|
565
|
+
// even when the manifest froze mid-long-tool (tight guard — never resurrects
|
|
566
|
+
// terminal/async runs).
|
|
567
|
+
const displayActive = (run: TeamRunManifest): boolean => {
|
|
568
|
+
const entry = resolved.get(run.runId);
|
|
569
|
+
const displayRun = entry?.snapshot?.manifest ?? run;
|
|
570
|
+
return (
|
|
571
|
+
isDisplayActiveRun(displayRun, entry?.agents ?? []) ||
|
|
572
|
+
heartbeatLiveForDisplay(displayRun, entry?.snapshot?.tasks ?? readRunTasks(run, snapshotCache, resolve))
|
|
573
|
+
);
|
|
574
|
+
};
|
|
575
|
+
const active = runs.filter((run) => displayActive(run));
|
|
576
|
+
const rest = runs.filter((run) => !displayActive(run));
|
|
545
577
|
if (active.length) rows.push({ label: "Active" }, ...active.map((run) => ({ label: run.runId, run })));
|
|
546
578
|
if (rest.length) rows.push({ label: "Recent" }, ...rest.map((run) => ({ label: run.runId, run })));
|
|
547
579
|
return rows;
|
|
@@ -754,7 +786,10 @@ export class RunDashboard implements DashboardComponent {
|
|
|
754
786
|
const snapshot = resolve ? resolve(run) : snapshotFor(run, this.options.snapshotCache);
|
|
755
787
|
const displayRun = snapshot?.manifest ?? run;
|
|
756
788
|
const agents = snapshot?.agents ?? agentsFor(run, this.options.snapshotCache, resolve);
|
|
757
|
-
|
|
789
|
+
// Wave 15 (F2): fresh task heartbeats veto the "stale" label.
|
|
790
|
+
const stale =
|
|
791
|
+
isLikelyOrphanedActiveRun(displayRun, agents) &&
|
|
792
|
+
!heartbeatLiveForDisplay(displayRun, snapshot?.tasks ?? readRunTasks(run, this.options.snapshotCache, resolve));
|
|
758
793
|
const status: RunStatus = stale ? "stale" : (displayRun.status as RunStatus);
|
|
759
794
|
if (status === "running" || agents.some((agent) => agent.status === "running")) hasRunning = true;
|
|
760
795
|
return snapshot?.signature ?? `${displayRun.runId}:${displayRun.status}:${displayRun.updatedAt}:${status}`;
|
|
@@ -963,7 +998,15 @@ export class RunDashboard implements DashboardComponent {
|
|
|
963
998
|
const snap = snapshotOnce(rowItem.run);
|
|
964
999
|
const run = snap?.manifest ?? rowItem.run;
|
|
965
1000
|
const agents = snap?.agents ?? agentsFor(rowItem.run, this.options.snapshotCache, snapshotOnce);
|
|
966
|
-
|
|
1001
|
+
// Wave 15 (F2): fresh task heartbeats veto the "stale" label (status line).
|
|
1002
|
+
const status: RunStatus =
|
|
1003
|
+
isLikelyOrphanedActiveRun(run, agents) &&
|
|
1004
|
+
!heartbeatLiveForDisplay(
|
|
1005
|
+
run,
|
|
1006
|
+
snap?.tasks ?? readRunTasks(rowItem.run, this.options.snapshotCache, snapshotOnce),
|
|
1007
|
+
)
|
|
1008
|
+
? "stale"
|
|
1009
|
+
: (run.status as RunStatus);
|
|
967
1010
|
const label = runLabel(run, idx === this.selected, this.options.snapshotCache, budget, snapshotOnce);
|
|
968
1011
|
lines.push(row(applyStatusColor(this.theme, status, label)));
|
|
969
1012
|
}
|
|
@@ -977,7 +1020,15 @@ export class RunDashboard implements DashboardComponent {
|
|
|
977
1020
|
const snap = snapshotOnce(rowItem.run);
|
|
978
1021
|
const run = snap?.manifest ?? rowItem.run;
|
|
979
1022
|
const agents = snap?.agents ?? agentsFor(rowItem.run, this.options.snapshotCache, snapshotOnce);
|
|
980
|
-
|
|
1023
|
+
// Wave 15 (F2): fresh task heartbeats veto the "stale" label (windowed status line).
|
|
1024
|
+
const status: RunStatus =
|
|
1025
|
+
isLikelyOrphanedActiveRun(run, agents) &&
|
|
1026
|
+
!heartbeatLiveForDisplay(
|
|
1027
|
+
run,
|
|
1028
|
+
snap?.tasks ?? readRunTasks(rowItem.run, this.options.snapshotCache, snapshotOnce),
|
|
1029
|
+
)
|
|
1030
|
+
? "stale"
|
|
1031
|
+
: (run.status as RunStatus);
|
|
981
1032
|
const label = runLabel(run, gi === this.selected, this.options.snapshotCache, budget, snapshotOnce);
|
|
982
1033
|
lines.push(row(applyStatusColor(this.theme, status, label)));
|
|
983
1034
|
}
|
|
@@ -996,8 +1047,12 @@ export class RunDashboard implements DashboardComponent {
|
|
|
996
1047
|
const snap = snapshotOnce(selectedRun);
|
|
997
1048
|
const r = snap?.manifest ?? selectedRun;
|
|
998
1049
|
const agents = snap?.agents ?? agentsFor(selectedRun, this.options.snapshotCache, snapshotOnce);
|
|
999
|
-
const statusStr: RunStatus = isLikelyOrphanedActiveRun(r, agents) ? "stale" : (r.status as RunStatus);
|
|
1000
1050
|
const selectedTasks = snap?.tasks ?? readRunTasks(r, this.options.snapshotCache, snapshotOnce);
|
|
1051
|
+
// Wave 15 (F2): fresh task heartbeats veto the "stale" label (detail header).
|
|
1052
|
+
const statusStr: RunStatus =
|
|
1053
|
+
isLikelyOrphanedActiveRun(r, agents) && !heartbeatLiveForDisplay(r, selectedTasks)
|
|
1054
|
+
? "stale"
|
|
1055
|
+
: (r.status as RunStatus);
|
|
1001
1056
|
lines.push(section("run"));
|
|
1002
1057
|
lines.push(row(`${fg("accent", "▸")} ${truncate(sanitizeUntrustedLine(r.goal), budget - 4)}`));
|
|
1003
1058
|
// L-2: surface the failure/cancellation reason inline for terminal runs.
|