pi-crew 0.2.6 → 0.2.8

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.
@@ -6,10 +6,10 @@ systemPromptMode: replace
6
6
  inheritProjectContext: true
7
7
  inheritSkills: false
8
8
  tools: read, grep, find, ls, bash
9
- maxTurns: 6
9
+ maxTurns: 15
10
10
  ---
11
11
 
12
- You are a verification specialist. Your job is to run tests ONCE, cache the results, then analyze against findings. You have at most **6 turns**.
12
+ You are a verification specialist. Your job is to run tests ONCE, cache the results, then analyze against findings. You have at most **15 turns**.
13
13
 
14
14
  ## Strategy
15
15
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-crew",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
5
5
  "author": "baphuongna",
6
6
  "license": "MIT",
@@ -14,7 +14,7 @@ export const DEFAULT_CHILD_PI = {
14
14
 
15
15
  export const DEFAULT_LIVE_SESSION = {
16
16
  /** Maximum wall-clock time for a single live-session task before abort (ms). */
17
- responseTimeoutMs: 5 * 60_000,
17
+ responseTimeoutMs: 10 * 60_000, // 10 minutes - increased from 5min for complex verification
18
18
  /** Maximum yield reminder attempts before accepting no-yield. */
19
19
  maxYieldRetries: 3,
20
20
  /** Polling interval for session idle check during yield enforcement (ms). */
@@ -58,6 +58,36 @@ function startInterruptGuard(manifest: { runId: string; stateRoot: string; event
58
58
  return () => clearInterval(interval);
59
59
  }
60
60
 
61
+ /**
62
+ * CRITICAL: Node.js v24 throws on unhandled rejections by default.
63
+ * Without this handler, any unhandled promise rejection (e.g., from cleanupTempDir,
64
+ * terminateLiveAgentsForRun, or other async cleanup) will crash the background runner
65
+ * BEFORE async.completed is written to the event log.
66
+ * This causes the async notifier to falsely detect a stuck run after quietMs expires.
67
+ */
68
+ function setupUnhandledRejectionGuard(state: { cwd?: string; runId?: string; eventsPath?: string }): void {
69
+ process.on("unhandledRejection", (reason, promise) => {
70
+ const message = reason instanceof Error ? reason.message : String(reason);
71
+ console.error("[background-runner] UNHANDLED REJECTION:", reason);
72
+ try {
73
+ // Try to write async.failed event if we have the necessary state
74
+ if (state.eventsPath && state.runId) {
75
+ appendEvent(state.eventsPath, {
76
+ type: "async.failed",
77
+ runId: state.runId,
78
+ message: `Unhandled rejection: ${message}`,
79
+ data: { reason: String(reason), handled: false },
80
+ });
81
+ }
82
+ } catch (appendErr) {
83
+ console.error("[background-runner] Failed to write async.failed event:", appendErr);
84
+ }
85
+ process.exitCode = 1;
86
+ // Give async operations a moment to flush before exit
87
+ setTimeout(() => process.exit(1), 100);
88
+ });
89
+ }
90
+
61
91
  async function main(): Promise<void> {
62
92
  // Scrub macOS malloc vars BEFORE anything else — must be clean for all child processes
63
93
  scrubProcessEnv();
@@ -73,6 +103,12 @@ async function main(): Promise<void> {
73
103
  const loaded = loadRunManifestById(cwd, runId);
74
104
  if (!loaded) throw new Error(`Run '${runId}' not found.`);
75
105
  let { manifest, tasks } = loaded;
106
+
107
+ // Setup unhandled rejection guard EARLY — must be before any async operations
108
+ // that might produce unhandled rejections during cleanup.
109
+ const rejectionGuardState = { cwd, runId, eventsPath: loaded.manifest.eventsPath };
110
+ setupUnhandledRejectionGuard(rejectionGuardState);
111
+
76
112
  appendEvent(manifest.eventsPath, { type: "async.started", runId: manifest.runId, data: { pid: process.pid } });
77
113
  writeAsyncStartMarker(manifest, { pid: process.pid, startedAt: new Date().toISOString() });
78
114
  const stopInterruptGuard = startInterruptGuard(manifest);
@@ -8,7 +8,13 @@ export interface ProcessLiveness {
8
8
  detail: string;
9
9
  }
10
10
 
11
- const ORPHANED_ACTIVE_RUN_MS = 10 * 60 * 1000;
11
+ /**
12
+ * How long (ms) a foreground team run with status "running" but no active agents
13
+ * survives before being flagged as orphaned. Reduced from 10min to 2min to
14
+ * improve UX: stuck foreground runs (e.g. planner with 0 tool calls) no longer
15
+ * linger for 10min before the dashboard shows them as stale.
16
+ */
17
+ const ORPHANED_ACTIVE_RUN_MS = 2 * 60 * 1000;
12
18
  /** How long a completed run stays visible in the widget after completion. */
13
19
  const COMPLETED_VISIBILITY_GRACE_MS = 8000;
14
20
  /** Maximum age (ms) for an active run before it's considered stale.
@@ -39,13 +45,35 @@ export function isFinishedRunStatus(status: string): boolean {
39
45
  return status === "completed" || status === "failed" || status === "cancelled" || status === "blocked";
40
46
  }
41
47
 
48
+ /**
49
+ * Secondary threshold: runs that have been "running" for more than this without
50
+ * any active agents (all queued, no progress) are also considered orphaned.
51
+ * This catches foreground team runs where the planner got stuck with 0 tool calls.
52
+ */
53
+ const ORPHANED_NO_PROGRESS_MS = 5 * 60 * 1000;
54
+
42
55
  export function isLikelyOrphanedActiveRun(run: TeamRunManifest, agents: CrewAgentRecord[] = [], now = Date.now(), staleMs = ORPHANED_ACTIVE_RUN_MS): boolean {
43
56
  if (!isActiveRunStatus(run.status)) return false;
44
57
  if (run.async?.pid !== undefined) return false;
45
58
  const updatedAt = new Date(run.updatedAt).getTime();
46
- if (!Number.isFinite(updatedAt) || now - updatedAt < staleMs) return false;
47
- if (agents.length === 0) return run.summary === "Creating workflow prompts and placeholder results.";
48
- return agents.every((agent) => agent.status === "queued" && !agent.completedAt && !agent.progress);
59
+ if (!Number.isFinite(updatedAt)) return false;
60
+
61
+ // Primary check: run hasn't been updated in a while
62
+ if (now - updatedAt >= staleMs) {
63
+ if (agents.length === 0) return run.summary === "Creating workflow prompts and placeholder results.";
64
+ return agents.every((agent) => agent.status === "queued" && !agent.completedAt && !agent.progress);
65
+ }
66
+
67
+ // Secondary check: run has been running without any progress for too long
68
+ if (now - updatedAt >= ORPHANED_NO_PROGRESS_MS) {
69
+ // If no agent is "running" or has made progress, the run is likely stuck
70
+ const hasActiveAgent = agents.some((agent) => agent.status === "running" || agent.progress || agent.toolUses);
71
+ if (!hasActiveAgent && agents.length > 0) {
72
+ return true;
73
+ }
74
+ }
75
+
76
+ return false;
49
77
  }
50
78
 
51
79
  function hasDurableActiveAgentEvidence(agent: CrewAgentRecord): boolean {