pi-crew 0.1.11 → 0.1.12

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.11",
3
+ "version": "0.1.12",
4
4
  "description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
5
5
  "author": "baphuongna",
6
6
  "license": "MIT",
@@ -15,6 +15,7 @@ import { clearPiCrewPowerbar, registerPiCrewPowerbarSegments, updatePiCrewPowerb
15
15
  import { DurableTextViewer, DurableTranscriptViewer } from "../ui/transcript-viewer.ts";
16
16
  import { loadRunManifestById } from "../state/state-store.ts";
17
17
  import { readCrewAgents } from "../runtime/crew-agent-records.ts";
18
+ import { terminateActiveChildPiProcesses } from "../runtime/child-pi.ts";
18
19
 
19
20
  function parseRunArgs(args: string): TeamToolParamsValue {
20
21
  const tokens = args.match(/"[^"]*"|'[^']*'|\S+/g)?.map((token) => token.replace(/^['"]|['"]$/g, "")) ?? [];
@@ -103,6 +104,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
103
104
  let currentCtx: ExtensionContext | undefined;
104
105
  let rpcHandle: PiCrewRpcHandle | undefined;
105
106
  const widgetState: CrewWidgetState = { frame: 0 };
107
+ const foregroundControllers = new Set<AbortController>();
106
108
  registerAutonomousPolicy(pi);
107
109
  rpcHandle = registerPiCrewRpc((pi as unknown as { events?: Parameters<typeof registerPiCrewRpc>[0] }).events, () => currentCtx);
108
110
 
@@ -123,6 +125,8 @@ export function registerPiTeams(pi: ExtensionAPI): void {
123
125
  widgetState.interval.unref?.();
124
126
  });
125
127
  pi.on("session_shutdown", () => {
128
+ for (const controller of foregroundControllers) controller.abort();
129
+ terminateActiveChildPiProcesses();
126
130
  stopAsyncRunNotifier(notifierState);
127
131
  stopCrewWidget(currentCtx, widgetState, currentCtx ? loadConfig(currentCtx.cwd).config.ui : undefined);
128
132
  clearPiCrewPowerbar(pi.events);
@@ -137,12 +141,21 @@ export function registerPiTeams(pi: ExtensionAPI): void {
137
141
  description: "Coordinate Pi teams. Use proactively for complex multi-file work, planning, implementation, tests, reviews, security audits, research, async/background runs, and worktree-isolated execution. Use action='recommend' when unsure which team/workflow to choose. Destructive actions require explicit user confirmation.",
138
142
  promptSnippet: "Use the team tool proactively for coordinated multi-agent work. If unsure, call { action: 'recommend', goal } first, then run or plan with the suggested team/workflow.",
139
143
  parameters: TeamToolParams as never,
140
- async execute(_id, params, _signal, _onUpdate, ctx) {
141
- const output = await handleTeamTool(params as TeamToolParamsValue, ctx);
142
- const config = loadConfig(ctx.cwd).config.ui;
143
- updateCrewWidget(ctx, widgetState, config);
144
- updatePiCrewPowerbar(pi.events, ctx.cwd, config);
145
- return output;
144
+ async execute(_id, params, signal, _onUpdate, ctx) {
145
+ const controller = new AbortController();
146
+ foregroundControllers.add(controller);
147
+ const abort = (): void => controller.abort();
148
+ signal?.addEventListener("abort", abort, { once: true });
149
+ try {
150
+ const output = await handleTeamTool(params as TeamToolParamsValue, { ...ctx, signal: controller.signal });
151
+ const config = loadConfig(ctx.cwd).config.ui;
152
+ updateCrewWidget(ctx, widgetState, config);
153
+ updatePiCrewPowerbar(pi.events, ctx.cwd, config);
154
+ return output;
155
+ } finally {
156
+ signal?.removeEventListener("abort", abort);
157
+ foregroundControllers.delete(controller);
158
+ }
146
159
  },
147
160
  };
148
161
 
@@ -12,7 +12,7 @@ function isFinished(run: TeamRunManifest): boolean {
12
12
  }
13
13
 
14
14
  export function pruneFinishedRuns(cwd: string, keep: number): PruneRunsResult {
15
- const finished = listRuns(cwd).filter(isFinished).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
15
+ const finished = listRuns(cwd).filter((run) => run.cwd === cwd && isFinished(run)).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
16
16
  const kept = finished.slice(0, keep).map((run) => run.runId);
17
17
  const removed: string[] = [];
18
18
  for (const run of finished.slice(keep)) {
@@ -56,6 +56,7 @@ type TeamContext = Pick<ExtensionContext, "cwd"> & Partial<Pick<ExtensionContext
56
56
  modelRegistry?: unknown;
57
57
  sessionManager?: { getBranch?: () => unknown[] };
58
58
  events?: { emit?: (event: string, data: unknown) => void };
59
+ signal?: AbortSignal;
59
60
  };
60
61
 
61
62
  function result(text: string, details: TeamToolDetails, isError = false): PiTeamsToolResult {
@@ -306,7 +307,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
306
307
  const runtime = await resolveCrewRuntime(effectiveRunConfig(loadedConfig.config, params.config));
307
308
  const executeWorkers = runtime.kind === "child-process";
308
309
  const executedConfig = effectiveRunConfig(loadedConfig.config, params.config);
309
- const executed = await executeTeamRun({ manifest: updatedManifest, tasks, team, workflow, agents, executeWorkers, limits: executedConfig.limits, runtime, runtimeConfig: executedConfig.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry });
310
+ const executed = await executeTeamRun({ manifest: updatedManifest, tasks, team, workflow, agents, executeWorkers, limits: executedConfig.limits, runtime, runtimeConfig: executedConfig.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry, signal: ctx.signal });
310
311
  const text = [
311
312
  `Created pi-crew run ${executed.manifest.runId}.`,
312
313
  `Team: ${team.name}`,
@@ -429,7 +430,7 @@ export async function handleResume(params: TeamToolParamsValue, ctx: TeamContext
429
430
  const loadedConfig = loadConfig(ctx.cwd);
430
431
  const runtime = await resolveCrewRuntime(loadedConfig.config);
431
432
  const executeWorkers = runtime.kind === "child-process";
432
- const executed = await executeTeamRun({ manifest: loaded.manifest, tasks: resetTasks, team, workflow, agents: allAgents(discoverAgents(ctx.cwd)), executeWorkers, limits: loadedConfig.config.limits, runtime, runtimeConfig: loadedConfig.config.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry });
433
+ const executed = await executeTeamRun({ manifest: loaded.manifest, tasks: resetTasks, team, workflow, agents: allAgents(discoverAgents(ctx.cwd)), executeWorkers, limits: loadedConfig.config.limits, runtime, runtimeConfig: loadedConfig.config.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry, signal: ctx.signal });
433
434
  return result([`Resumed run ${executed.manifest.runId}.`, `Status: ${executed.manifest.status}`, `Tasks: ${executed.tasks.length}`, `Artifacts: ${executed.manifest.artifactsRoot}`].join("\n"), { action: "resume", status: executed.manifest.status === "failed" ? "error" : "ok", runId: executed.manifest.runId, artifactsRoot: executed.manifest.artifactsRoot }, executed.manifest.status === "failed");
434
435
  });
435
436
  }
@@ -1,4 +1,4 @@
1
- import { spawn } from "node:child_process";
1
+ import { spawn, type ChildProcess } from "node:child_process";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import type { AgentConfig } from "../agents/agent-config.ts";
@@ -8,6 +8,29 @@ import { getPiSpawnCommand } from "./pi-spawn.ts";
8
8
  const POST_EXIT_STDIO_GUARD_MS = 3000;
9
9
  const FINAL_DRAIN_MS = 5000;
10
10
  const HARD_KILL_MS = 3000;
11
+ const activeChildProcesses = new Map<number, ChildProcess>();
12
+
13
+ function killProcessTree(pid: number | undefined): void {
14
+ if (!pid || !Number.isInteger(pid) || pid <= 0) return;
15
+ try {
16
+ if (process.platform === "win32") {
17
+ spawn("taskkill", ["/pid", String(pid), "/t", "/f"], { stdio: "ignore", windowsHide: true });
18
+ return;
19
+ }
20
+ try { process.kill(-pid, "SIGTERM"); } catch { process.kill(pid, "SIGTERM"); }
21
+ setTimeout(() => {
22
+ try { process.kill(-pid, "SIGKILL"); } catch { try { process.kill(pid, "SIGKILL"); } catch {} }
23
+ }, HARD_KILL_MS).unref?.();
24
+ } catch {
25
+ // Ignore shutdown races.
26
+ }
27
+ }
28
+
29
+ export function terminateActiveChildPiProcesses(): number {
30
+ const pids = [...activeChildProcesses.keys()];
31
+ for (const pid of pids) killProcessTree(pid);
32
+ return pids.length;
33
+ }
11
34
 
12
35
  export interface ChildPiRunInput {
13
36
  cwd: string;
@@ -118,7 +141,9 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
118
141
  cwd: input.cwd,
119
142
  env: { ...process.env, ...built.env },
120
143
  stdio: ["ignore", "pipe", "pipe"],
144
+ detached: process.platform !== "win32",
121
145
  });
146
+ if (child.pid) activeChildProcesses.set(child.pid, child);
122
147
  let stdout = "";
123
148
  let stderr = "";
124
149
  let settled = false;
@@ -167,6 +192,7 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
167
192
  };
168
193
 
169
194
  const abort = (): void => {
195
+ killProcessTree(child.pid);
170
196
  try {
171
197
  child.kill(process.platform === "win32" ? undefined : "SIGTERM");
172
198
  } catch {
@@ -187,6 +213,7 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
187
213
  settle({ exitCode: null, stdout, stderr, error: error.message });
188
214
  });
189
215
  child.on("exit", () => {
216
+ if (child.pid) activeChildProcesses.delete(child.pid);
190
217
  childExited = true;
191
218
  clearFinalDrainTimers();
192
219
  postExitGuard = setTimeout(() => {
@@ -196,6 +223,7 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
196
223
  postExitGuard.unref?.();
197
224
  });
198
225
  child.on("close", (exitCode) => {
226
+ if (child.pid) activeChildProcesses.delete(child.pid);
199
227
  settle({ exitCode, stdout, stderr, ...(forcedFinalDrain && !stderr.trim() ? { error: `Child Pi did not exit within ${finalDrainMs}ms after final assistant message; termination was requested.` } : {}) });
200
228
  });
201
229
  });
@@ -38,6 +38,13 @@ export function isLikelyOrphanedActiveRun(run: TeamRunManifest, agents: CrewAgen
38
38
  return agents.every((agent) => agent.status === "queued" && !agent.completedAt && !agent.progress);
39
39
  }
40
40
 
41
+ function hasDurableActiveAgentEvidence(agent: CrewAgentRecord): boolean {
42
+ if (agent.status !== "running" && agent.status !== "queued") return false;
43
+ return Boolean(agent.statusPath || agent.eventsPath || agent.outputPath || agent.progress || agent.toolUses || agent.jsonEvents);
44
+ }
45
+
41
46
  export function isDisplayActiveRun(run: TeamRunManifest, agents: CrewAgentRecord[] = [], now = Date.now()): boolean {
42
- return isActiveRunStatus(run.status) && !isLikelyOrphanedActiveRun(run, agents, now);
47
+ if (!isActiveRunStatus(run.status) || isLikelyOrphanedActiveRun(run, agents, now)) return false;
48
+ if (agents.length === 0) return true;
49
+ return agents.some(hasDurableActiveAgentEvidence);
43
50
  }