pi-crew 0.1.9 → 0.1.10

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.1.9",
3
+ "version": "0.1.10",
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,14 @@
1
+ import type { CrewAgentRecord } from "./crew-agent-runtime.ts";
2
+ import type { TeamRunManifest } from "../state/types.ts";
3
+
1
4
  export interface ProcessLiveness {
2
5
  pid?: number;
3
6
  alive: boolean;
4
7
  detail: string;
5
8
  }
6
9
 
10
+ const ORPHANED_ACTIVE_RUN_MS = 10 * 60 * 1000;
11
+
7
12
  export function checkProcessLiveness(pid: number | undefined): ProcessLiveness {
8
13
  if (pid === undefined || !Number.isInteger(pid) || pid <= 0) {
9
14
  return { pid, alive: false, detail: "no pid recorded" };
@@ -23,3 +28,16 @@ export function checkProcessLiveness(pid: number | undefined): ProcessLiveness {
23
28
  export function isActiveRunStatus(status: string): boolean {
24
29
  return status === "queued" || status === "planning" || status === "running";
25
30
  }
31
+
32
+ export function isLikelyOrphanedActiveRun(run: TeamRunManifest, agents: CrewAgentRecord[] = [], now = Date.now(), staleMs = ORPHANED_ACTIVE_RUN_MS): boolean {
33
+ if (!isActiveRunStatus(run.status)) return false;
34
+ if (run.async?.pid !== undefined) return false;
35
+ const updatedAt = new Date(run.updatedAt).getTime();
36
+ if (!Number.isFinite(updatedAt) || now - updatedAt < staleMs) return false;
37
+ if (agents.length === 0) return run.summary === "Creating workflow prompts and placeholder results.";
38
+ return agents.every((agent) => agent.status === "queued" && !agent.completedAt && !agent.progress);
39
+ }
40
+
41
+ export function isDisplayActiveRun(run: TeamRunManifest, agents: CrewAgentRecord[] = [], now = Date.now()): boolean {
42
+ return isActiveRunStatus(run.status) && !isLikelyOrphanedActiveRun(run, agents, now);
43
+ }
@@ -1,6 +1,6 @@
1
1
  import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
2
  import { listRuns } from "../extension/run-index.ts";
3
- import { isActiveRunStatus } from "../runtime/process-status.ts";
3
+ import { isDisplayActiveRun, isLikelyOrphanedActiveRun } from "../runtime/process-status.ts";
4
4
  import { readCrewAgents } from "../runtime/crew-agent-records.ts";
5
5
  import type { CrewAgentRecord } from "../runtime/crew-agent-runtime.ts";
6
6
  import type { TeamRunManifest } from "../state/types.ts";
@@ -62,6 +62,7 @@ function agentStats(agent: CrewAgentRecord): string {
62
62
  }
63
63
 
64
64
  function runStep(run: TeamRunManifest, agents: CrewAgentRecord[]): string {
65
+ if (isLikelyOrphanedActiveRun(run, agents)) return "stale";
65
66
  const running = agents.find((agent) => agent.status === "running");
66
67
  if (running) return running.taskId;
67
68
  const queued = agents.find((agent) => agent.status === "queued");
@@ -69,10 +70,15 @@ function runStep(run: TeamRunManifest, agents: CrewAgentRecord[]): string {
69
70
  return run.status;
70
71
  }
71
72
 
73
+ function agentsFor(run: TeamRunManifest): CrewAgentRecord[] {
74
+ try { return readCrewAgents(run); } catch { return []; }
75
+ }
76
+
72
77
  export function buildCrewWidgetLines(cwd: string, frame = 0, maxLines = 8): string[] {
73
78
  const runs = listRuns(cwd).slice(0, 20);
74
- const activeRuns = runs.filter((run) => isActiveRunStatus(run.status));
75
- const recentRuns = runs.filter((run) => !isActiveRunStatus(run.status)).slice(0, 3);
79
+ const runAgents = new Map(runs.map((run) => [run.runId, agentsFor(run)]));
80
+ const activeRuns = runs.filter((run) => isDisplayActiveRun(run, runAgents.get(run.runId) ?? []));
81
+ const recentRuns = runs.filter((run) => !isDisplayActiveRun(run, runAgents.get(run.runId) ?? [])).slice(0, 3);
76
82
  const shownRuns = [...activeRuns, ...recentRuns];
77
83
  if (!shownRuns.length) return [];
78
84
  const runningGlyph = SPINNER[frame % SPINNER.length] ?? "⠋";
@@ -80,13 +86,13 @@ export function buildCrewWidgetLines(cwd: string, frame = 0, maxLines = 8): stri
80
86
  const activeCount = activeRuns.length;
81
87
  lines.push(`${activeCount ? "●" : "○"} pi-crew · active=${activeCount} recent=${recentRuns.length} · /team-dashboard`);
82
88
  for (const run of shownRuns) {
83
- let agents: CrewAgentRecord[] = [];
84
- try { agents = readCrewAgents(run); } catch { agents = []; }
89
+ const agents = runAgents.get(run.runId) ?? [];
90
+ const stale = isLikelyOrphanedActiveRun(run, agents);
85
91
  const counts = new Map<string, number>();
86
92
  for (const agent of agents) counts.set(agent.status, (counts.get(agent.status) ?? 0) + 1);
87
- const countText = [...counts.entries()].map(([status, count]) => `${status}:${count}`).join(" ") || run.status;
88
- lines.push(`${glyph(run.status, runningGlyph)} ${run.runId.slice(-8)} ${run.team}/${run.workflow ?? "none"} · ${runStep(run, agents)} · ${countText}`);
89
- for (const agent of agents.filter((item) => item.status === "running" || item.status === "queued").slice(0, 2)) {
93
+ const countText = stale ? "stale queued run" : [...counts.entries()].map(([status, count]) => `${status}:${count}`).join(" ") || run.status;
94
+ lines.push(`${glyph(stale ? "failed" : run.status, runningGlyph)} ${run.runId.slice(-8)} ${run.team}/${run.workflow ?? "none"} · ${runStep(run, agents)} · ${countText}`);
95
+ for (const agent of (stale ? [] : agents.filter((item) => item.status === "running" || item.status === "queued").slice(0, 2))) {
90
96
  const stats = agentStats(agent);
91
97
  lines.push(` ${glyph(agent.status, runningGlyph)} ${agent.taskId} ${agent.role}→${agent.agent} · ${agentActivity(agent)}${stats ? ` · ${stats}` : ""}`);
92
98
  }
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
2
2
  import type { TeamRunManifest } from "../state/types.ts";
3
3
  import { readCrewAgents } from "../runtime/crew-agent-records.ts";
4
4
  import type { CrewAgentRecord } from "../runtime/crew-agent-runtime.ts";
5
- import { isActiveRunStatus } from "../runtime/process-status.ts";
5
+ import { isDisplayActiveRun, isLikelyOrphanedActiveRun } from "../runtime/process-status.ts";
6
6
 
7
7
  interface DashboardComponent {
8
8
  invalidate(): void;
@@ -51,7 +51,7 @@ function padVisible(value: string, width: number): string {
51
51
 
52
52
  function statusIcon(status: string): string {
53
53
  if (status === "completed") return "✓";
54
- if (status === "failed") return "✗";
54
+ if (status === "failed" || status === "stale") return "✗";
55
55
  if (status === "cancelled") return "!";
56
56
  if (status === "running") return "▶";
57
57
  if (status === "blocked") return "■";
@@ -104,19 +104,24 @@ function readAgentPreview(run: TeamRunManifest, maxLines = 5): string[] {
104
104
  }
105
105
  }
106
106
 
107
+ function agentsFor(run: TeamRunManifest): CrewAgentRecord[] {
108
+ try { return readCrewAgents(run); } catch { return []; }
109
+ }
110
+
107
111
  function runLabel(run: TeamRunManifest, selected: boolean): string {
108
- let agents: CrewAgentRecord[] = [];
109
- try { agents = readCrewAgents(run); } catch { agents = []; }
112
+ const agents = agentsFor(run);
113
+ const stale = isLikelyOrphanedActiveRun(run, agents);
110
114
  const running = agents.find((agent) => agent.status === "running");
111
115
  const queued = agents.find((agent) => agent.status === "queued");
112
- const step = running ? `step ${running.taskId}` : queued ? `queued ${queued.taskId}` : `agents ${agents.length}`;
116
+ const step = stale ? "orphaned queued run" : running ? `step ${running.taskId}` : queued ? `queued ${queued.taskId}` : `agents ${agents.length}`;
117
+ const status = stale ? "stale" : run.status;
113
118
  const marker = selected ? "›" : " ";
114
- return `${marker} ${statusIcon(run.status)} ${run.runId.slice(-8)} ${run.status} | ${run.team}/${run.workflow ?? "none"} | ${step} | ${run.goal}`;
119
+ return `${marker} ${statusIcon(status)} ${run.runId.slice(-8)} ${status} | ${run.team}/${run.workflow ?? "none"} | ${step} | ${run.goal}`;
115
120
  }
116
121
 
117
122
  function groupedRuns(runs: TeamRunManifest[]): Array<{ label: string; run?: TeamRunManifest }> {
118
- const active = runs.filter((run) => isActiveRunStatus(run.status));
119
- const recent = runs.filter((run) => !isActiveRunStatus(run.status));
123
+ const active = runs.filter((run) => isDisplayActiveRun(run, agentsFor(run)));
124
+ const recent = runs.filter((run) => !isDisplayActiveRun(run, agentsFor(run)));
120
125
  const rows: Array<{ label: string; run?: TeamRunManifest }> = [];
121
126
  if (active.length) rows.push({ label: "Active" }, ...active.map((run) => ({ label: run.runId, run })));
122
127
  if (recent.length) rows.push({ label: "Recent" }, ...recent.map((run) => ({ label: run.runId, run })));