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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-crew",
3
- "version": "0.11.43",
3
+ "version": "0.11.44",
4
4
  "description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
5
5
  "author": "baphuongna",
6
6
  "license": "MIT",
@@ -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
- return isDisplayActiveRun(run, agents);
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 { hasStaleAsyncProcess, isActiveRunStatus, isLikelyOrphanedActiveRun } from "../../runtime/process-status.ts";
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 stuck = detectStuckTasks(run, now);
380
- allStuck.push(...stuck);
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 { hasFreshTaskHeartbeatEvidence, isActiveRunStatus, 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";
@@ -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
- .filter((item) => isDisplayActiveRun(item.run, item.agents));
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;
@@ -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 runs
65
- .map((run) => {
66
- try {
67
- // U7 (spec 2026-10-09): prefer the committed-state read when the
68
- // cache exposes it. `readForRender` returns the SAME cached
69
- // snapshot `get` would (identical render — parity), but a lapsed
70
- // TTL schedules the coalesced ASYNC refresh (80ms batch → stamp
71
- // check → rebuild only when files changed) instead of waiting for
72
- // an external poller to notice. Structural check: legacy callers
73
- // and test doubles may implement only `get`.
74
- const committed = snapshotCache as
75
- | { readForRender?: (runId: string) => RunUiSnapshot | undefined; get?: (runId: string) => RunUiSnapshot | undefined }
76
- | undefined;
77
- const snapshot =
78
- typeof committed?.readForRender === "function" ? committed.readForRender(run.runId) : snapshotCache?.get(run.runId);
79
- if (snapshot) {
80
- return {
81
- run: snapshot.manifest,
82
- agents: snapshot.agents,
83
- snapshot,
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
- // P0-6: render from snapshots only — never read disk on every
87
- // render tick. When the snapshot cache is provided but hasn't
88
- // populated yet (first frame after `updateCrewWidget`), drop the
89
- // run from the result so the widget paints "(loading…)" instead
90
- // of calling `agentsFor(run) → readCrewAgents`. Legacy callers
91
- // (no snapshotCache) keep the disk-read fallback for tests/dev.
92
- if (snapshotCache) return null;
93
- return { run, agents: agentsFor(run) };
94
- } catch {
95
- if (snapshotCache) return null;
96
- return { run, agents: agentsFor(run) };
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
  /**