pi-crew 0.11.43 → 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 +30 -0
- package/dist/index.mjs +379 -379
- 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 +16 -0
- package/src/ui/agents-jobs-browser.ts +2 -3
- package/src/ui/powerbar-publisher.ts +6 -2
- package/src/ui/run-dashboard.ts +1 -19
- 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",
|
|
@@ -119,6 +119,22 @@ export function hasFreshTaskHeartbeatEvidence(
|
|
|
119
119
|
});
|
|
120
120
|
}
|
|
121
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
|
+
|
|
122
138
|
export function isLikelyOrphanedActiveRun(
|
|
123
139
|
run: TeamRunManifest,
|
|
124
140
|
agents: CrewAgentRecord[] = [],
|
|
@@ -60,7 +60,7 @@ 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 {
|
|
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";
|
|
@@ -504,8 +504,7 @@ export class AgentsJobsBrowser {
|
|
|
504
504
|
// heartbeats are fresh (this loop already does per-run I/O — one more
|
|
505
505
|
// coalesced read is in-pattern). Terminal/async runs are never
|
|
506
506
|
// resurrected (isActiveRunStatus && !run.async).
|
|
507
|
-
const heartbeatLive =
|
|
508
|
-
isActiveRunStatus(run.status) && !run.async && hasFreshTaskHeartbeatEvidence(readLegacyRunTasks(run), nowMs);
|
|
507
|
+
const heartbeatLive = heartbeatLiveForDisplay(run, readLegacyRunTasks(run), nowMs);
|
|
509
508
|
if (isDisplayActiveRun(run, agents, nowMs) || heartbeatLive) runs.push({ run, agents });
|
|
510
509
|
} catch {
|
|
511
510
|
/* unreadable — skip */
|
|
@@ -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,12 +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 {
|
|
14
|
-
hasFreshTaskHeartbeatEvidence,
|
|
15
|
-
isActiveRunStatus,
|
|
16
|
-
isDisplayActiveRun,
|
|
17
|
-
isLikelyOrphanedActiveRun,
|
|
18
|
-
} from "../runtime/process-status.ts";
|
|
13
|
+
import { heartbeatLiveForDisplay, isActiveRunStatus, isDisplayActiveRun, isLikelyOrphanedActiveRun } from "../runtime/process-status.ts";
|
|
19
14
|
import type { ScheduledJob } from "../runtime/scheduling/scheduler.ts";
|
|
20
15
|
import type { TeamRunManifest, TeamTaskState, UsageState } from "../state/types.ts";
|
|
21
16
|
import { aggregateUsage } from "../state/usage.ts";
|
|
@@ -368,19 +363,6 @@ function taskForAgent(tasks: TeamTaskState[], agent: CrewAgentRecord): TeamTaskS
|
|
|
368
363
|
return tasks.find((task) => task.id === agent.taskId);
|
|
369
364
|
}
|
|
370
365
|
|
|
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
|
-
|
|
384
366
|
function modelForTask(task: TeamTaskState | undefined): string | undefined {
|
|
385
367
|
const attempts = task?.modelAttempts;
|
|
386
368
|
if (!attempts?.length) return undefined;
|
|
@@ -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
|
/**
|