pi-crew 0.9.42 → 0.9.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +147 -0
  2. package/README.md +14 -0
  3. package/dist/build-meta.json +6692 -13022
  4. package/dist/index.mjs +54585 -65621
  5. package/dist/index.mjs.map +4 -4
  6. package/package.json +1 -1
  7. package/scripts/build-bundle.mjs +2 -1
  8. package/src/agents/agent-config.ts +1 -1
  9. package/src/config/config.ts +1 -1
  10. package/src/config/types.ts +0 -2
  11. package/src/extension/crew-vibes/config.ts +1 -1
  12. package/src/extension/crew-vibes/font-detect.ts +16 -3
  13. package/src/extension/cross-extension-rpc.ts +8 -4
  14. package/src/extension/registration/commands.ts +5 -1
  15. package/src/extension/registration/foreground-run-controller.ts +28 -0
  16. package/src/extension/team-tool.ts +5 -0
  17. package/src/prompt/prompt-runtime.ts +15 -0
  18. package/src/runtime/child-pi-constants.ts +42 -0
  19. package/src/runtime/child-pi-kill.ts +180 -0
  20. package/src/runtime/child-pi-spawn.ts +234 -0
  21. package/src/runtime/child-pi-steering.ts +128 -0
  22. package/src/runtime/child-pi-streams.ts +296 -0
  23. package/src/runtime/child-pi-transcript.ts +169 -0
  24. package/src/runtime/child-pi.ts +75 -837
  25. package/src/runtime/compact-stages/tail-capture-stage.ts +1 -1
  26. package/src/runtime/dwf-state-store.ts +5 -0
  27. package/src/runtime/dynamic-workflow-context.ts +78 -18
  28. package/src/runtime/dynamic-workflow-runner.ts +18 -2
  29. package/src/runtime/goal-evaluator.ts +59 -27
  30. package/src/runtime/goal-loop-runner.ts +40 -11
  31. package/src/runtime/live-agent-manager.ts +18 -0
  32. package/src/runtime/pi-json-output.ts +2 -1
  33. package/src/runtime/pi-spawn.ts +7 -1
  34. package/src/runtime/run-coalesced-task-group.ts +18 -10
  35. package/src/runtime/task-runner/prompt-builder.ts +2 -2
  36. package/src/runtime/team-runner.ts +6 -1
  37. package/src/state/event-log.ts +1 -0
  38. package/src/state/locks.ts +132 -49
  39. package/src/state/worker-atomic-writer.ts +1 -0
  40. package/src/utils/paths.ts +1 -1
  41. package/src/worktree/worktree-manager.ts +47 -19
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-crew",
3
- "version": "0.9.42",
3
+ "version": "0.9.44",
4
4
  "description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
5
5
  "author": "baphuongna",
6
6
  "license": "MIT",
@@ -55,7 +55,8 @@ const result = await build({
55
55
  "cli-highlight",
56
56
  "diff",
57
57
  "jiti",
58
- "typebox",
58
+ "@sinclair/typebox",
59
+ "acorn",
59
60
  ],
60
61
  // All node:* and Node-builtin modules are external by default for
61
62
  // platform=node, but list explicitly for clarity.
@@ -83,7 +83,7 @@ export interface AgentConfig {
83
83
  contextMode?: "fresh" | "fork";
84
84
  /** Maximum turns for this agent. Overrides runtime config if set. */
85
85
  maxTurns?: number;
86
- /** Cap on output tokens per API call. Set via PI_CREW_MAX_OUTPUT_TOKENS env in child. */
86
+ /** Cap on output tokens per API call. Set via PI_CREW_MAX_OUTPUT env in child. */
87
87
  maxTokens?: number;
88
88
  /** Effort level for this agent. Controls how much work the agent puts in. */
89
89
  effort?: "low" | "medium" | "high";
@@ -169,7 +169,7 @@ export function invalidateConfigCache(): void {
169
169
  }
170
170
 
171
171
  function resolveHomeDir(): string {
172
- const envValue = process.env.PI_TEAMS_HOME?.trim();
172
+ const envValue = (process.env.PI_TEAMS_HOME ?? process.env.PI_CREW_HOME)?.trim();
173
173
  const defaultHome = os.homedir();
174
174
  if (!envValue) return defaultHome;
175
175
  // FIX (Round 14): When PI_TEAMS_HOME is explicitly set, validate that
@@ -15,8 +15,6 @@ export interface PiTeamsAutonomousConfig {
15
15
  preferAsyncForLongTasks?: boolean;
16
16
  allowWorktreeSuggestion?: boolean;
17
17
  magicKeywords?: Record<string, string[]>;
18
- /** Mark certain bash commands as excludeFromContext to reduce context tokens. Default: false */
19
- excludeContextBash?: boolean;
20
18
  }
21
19
 
22
20
  export interface CrewLimitsConfig {
@@ -13,7 +13,7 @@ export const CAPACITY_STATUS_ID = "pi-crew-bar";
13
13
  export const PROVIDER_STATUS_ID = "pi-crew-bar";
14
14
 
15
15
  function resolveHome(): string {
16
- return process.env.PI_TEAMS_HOME?.trim() || process.env.HOME || process.env.USERPROFILE || "";
16
+ return (process.env.PI_TEAMS_HOME ?? process.env.PI_CREW_HOME)?.trim() || process.env.HOME || process.env.USERPROFILE || "";
17
17
  }
18
18
 
19
19
  export function configPath(): string {
@@ -33,18 +33,30 @@ export function hasCrewFontFile(): boolean {
33
33
  return _hasFontFile;
34
34
  }
35
35
 
36
+ let _isWebTerminal: boolean | null = null;
37
+
36
38
  /** Returns true when running in a web-based terminal (gotty, wetty, etc.)
37
39
  * where system fonts are not available for PUA rendering. */
38
40
  export function isWebTerminal(): boolean {
39
- if (process.env.GOTTY || process.env.WEBTERM) return true;
40
- if (process.env.TERM === "dumb") return true;
41
+ if (_isWebTerminal !== null) return _isWebTerminal;
42
+ if (process.env.GOTTY || process.env.WEBTERM) {
43
+ _isWebTerminal = true;
44
+ return true;
45
+ }
46
+ if (process.env.TERM === "dumb") {
47
+ _isWebTerminal = true;
48
+ return true;
49
+ }
41
50
  try {
42
51
  // Check current process and ancestors — gotty is often the grandparent
43
52
  // (gotty → tmux → pi), so /proc/self/cgroup may not contain it.
44
53
  let pid = process.pid;
45
54
  for (let i = 0; i < 6 && pid > 1; i++) {
46
55
  const cgroup = readFileSync(`/proc/${pid}/cgroup`, "utf8");
47
- if (cgroup.includes("gotty") || cgroup.includes("wetty")) return true;
56
+ if (cgroup.includes("gotty") || cgroup.includes("wetty")) {
57
+ _isWebTerminal = true;
58
+ return true;
59
+ }
48
60
  const match = cgroup.match(/\d+:.*:(.*)/);
49
61
  // Read PPid from status
50
62
  const status = readFileSync(`/proc/${pid}/status`, "utf8");
@@ -54,5 +66,6 @@ export function isWebTerminal(): boolean {
54
66
  } catch {
55
67
  // not Linux or /proc not available
56
68
  }
69
+ _isWebTerminal = false;
57
70
  return false;
58
71
  }
@@ -282,10 +282,14 @@ export function registerPiCrewRpc(
282
282
  }
283
283
  }, "pi-crew:rpc:status"),
284
284
  ),
285
- on(events, "pi-crew:live-control", (raw) => {
286
- const request = parseLiveControlRealtimeMessage(raw);
287
- if (request) publishLiveControlRealtime(request);
288
- }),
285
+ on(
286
+ events,
287
+ "pi-crew:live-control",
288
+ withHmacVerification((raw) => {
289
+ const request = parseLiveControlRealtimeMessage(raw);
290
+ if (request) publishLiveControlRealtime(request);
291
+ }, "pi-crew:live-control"),
292
+ ),
289
293
  on(
290
294
  events,
291
295
  "pi-crew:rpc:live-control",
@@ -1114,7 +1114,11 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
1114
1114
  const useProject = tokens.includes("--project");
1115
1115
  const varEntries = tokens
1116
1116
  .filter((t) => t.startsWith("--var=") || t.startsWith("--var "))
1117
- .map((t) => t.replace(/^--var(?:\s+|=)/, "").split("=", 2) as [string, string]);
1117
+ .map((t): [string, string] => {
1118
+ const s = t.replace(/^--var(?:\s+|=)/, "");
1119
+ const idx = s.indexOf("=");
1120
+ return idx === -1 ? [s, ""] : [s.slice(0, idx), s.slice(idx + 1)];
1121
+ });
1118
1122
  const templateId = tokens.find((t) => !t.startsWith("--") && !t.includes("="));
1119
1123
  if (!templateId) {
1120
1124
  await notifyCommandResult(
@@ -165,6 +165,7 @@ function startForegroundRunImpl(
165
165
  goal: loaded?.manifest.goal,
166
166
  status,
167
167
  taskCount: loaded?.tasks.length,
168
+ durationMs: loaded?.manifest.createdAt ? Date.now() - new Date(loaded.manifest.createdAt).getTime() : 0,
168
169
  timestamp: Date.now(),
169
170
  });
170
171
  const eventType =
@@ -183,6 +184,33 @@ function startForegroundRunImpl(
183
184
  status,
184
185
  taskCount: loaded?.tasks.length,
185
186
  goal: loaded?.manifest.goal,
187
+ durationMs: loaded?.manifest.createdAt ? Date.now() - new Date(loaded.manifest.createdAt).getTime() : 0,
188
+ });
189
+ }
190
+
191
+ // Emit per-task events so task-level metrics (crew.task.count,
192
+ // crew.task.duration_ms, crew.task.tokens_total) record actual values.
193
+ const terminalTaskStatuses = new Set(["completed", "failed", "needs_attention"]);
194
+ for (const task of loaded?.tasks ?? []) {
195
+ if (!terminalTaskStatuses.has(task.status)) continue;
196
+ const taskEventType =
197
+ task.status === "completed"
198
+ ? "crew.task.completed"
199
+ : task.status === "failed"
200
+ ? "crew.task.failed"
201
+ : "crew.task.needs_attention";
202
+ pi.events?.emit?.(taskEventType, {
203
+ runId,
204
+ team: loaded?.manifest.team,
205
+ workflow: loaded?.manifest.workflow,
206
+ taskId: task.id,
207
+ taskCount: loaded?.tasks.length,
208
+ role: task.role,
209
+ durationMs:
210
+ task.startedAt && task.finishedAt
211
+ ? new Date(task.finishedAt).getTime() - new Date(task.startedAt).getTime()
212
+ : 0,
213
+ tokens: (task.usage?.input ?? 0) + (task.usage?.output ?? 0),
186
214
  });
187
215
  }
188
216
  }
@@ -8,6 +8,7 @@ import { loadConfig, updateAutonomousConfig, updateConfig } from "../config/conf
8
8
  import type { executeTeamRun as _executeTeamRunFn } from "../runtime/team-runner.ts";
9
9
  import type { TeamToolParamsValue } from "../schema/team-tool-schema.ts";
10
10
  import { writeArtifact } from "../state/artifact-store.ts";
11
+ import { TEAM_TERMINAL_TASK_STATUSES } from "../state/contracts.ts";
11
12
  import { appendEvent, appendEventFireAndForget } from "../state/event-log.ts";
12
13
  import { withRunLock } from "../state/locks.ts";
13
14
  import { replayPendingMailboxMessages } from "../state/mailbox.ts";
@@ -501,6 +502,10 @@ export function handleSteer(params: TeamToolParamsValue, ctx: TeamContext): PiTe
501
502
  const task = loaded.tasks.find((t) => t.id === taskId);
502
503
  if (!task) return result(`Task '${taskId}' not found`, { action: "steer", status: "error" }, true);
503
504
  if (!task.pendingSteers) task.pendingSteers = [];
505
+ // T-S1: do not allow steering a task that has already reached a terminal status.
506
+ if (TEAM_TERMINAL_TASK_STATUSES.has(task.status)) {
507
+ return result(`Task '${taskId}' is ${task.status}; cannot steer.`, { action: "steer", status: "error" }, true);
508
+ }
504
509
  // HIGH-04: Cap pendingSteers array to prevent unbounded memory growth
505
510
  const MAX_PENDING_STEERS = 100;
506
511
  if (task.pendingSteers.length >= MAX_PENDING_STEERS) {
@@ -170,6 +170,21 @@ export default function registerPiTeamsPromptRuntime(pi: ExtensionAPI): void {
170
170
  if (typeof payload.max_tokens === "number" && payload.max_tokens > maxTokensCap) {
171
171
  payload.max_tokens = maxTokensCap;
172
172
  }
173
+ // Also cap newer field names used by some providers
174
+ if (typeof payload.max_completion_tokens === "number" && payload.max_completion_tokens > maxTokensCap) {
175
+ payload.max_completion_tokens = maxTokensCap;
176
+ }
177
+ if (typeof payload.max_output_tokens === "number" && payload.max_output_tokens > maxTokensCap) {
178
+ payload.max_output_tokens = maxTokensCap;
179
+ }
180
+ const generationConfig = payload.generationConfig as Record<string, unknown> | undefined;
181
+ if (
182
+ generationConfig &&
183
+ typeof generationConfig.max_output_tokens === "number" &&
184
+ generationConfig.max_output_tokens > maxTokensCap
185
+ ) {
186
+ generationConfig.max_output_tokens = maxTokensCap;
187
+ }
173
188
  });
174
189
  }
175
190
 
@@ -0,0 +1,42 @@
1
+ /**
2
+ * child-pi-constants.ts — Shared timing and capture constants for child-pi runtime.
3
+ *
4
+ * Extracted from child-pi.ts (H-7 decomposition, step 3). Zero behavior change.
5
+ *
6
+ * These constants are shared between runChildPi (in child-pi.ts) and the kill
7
+ * helpers (in child-pi-kill.ts). Centralizing them here avoids circular imports
8
+ * and makes the timing budget configurable from one place.
9
+ */
10
+
11
+ import { DEFAULT_CHILD_PI } from "../config/defaults.ts";
12
+
13
+ /** Post-exit window during which stdio is guarded against late writes. */
14
+ export const POST_EXIT_STDIO_GUARD_MS = DEFAULT_CHILD_PI.postExitStdioGuardMs;
15
+
16
+ /** Maximum time to wait for a final assistant event after the last stdout byte. */
17
+ export const FINAL_DRAIN_MS = DEFAULT_CHILD_PI.finalDrainMs;
18
+
19
+ /** Time after SIGTERM to escalate to SIGKILL. */
20
+ export const HARD_KILL_MS = DEFAULT_CHILD_PI.hardKillMs;
21
+
22
+ /** Maximum time with no output before the child is considered unresponsive. */
23
+ export const RESPONSE_TIMEOUT_MS = DEFAULT_CHILD_PI.responseTimeoutMs;
24
+
25
+ /**
26
+ * Maximum size (bytes) for the ChildPiLineObserver's line accumulation buffer.
27
+ * When exceeded, the buffer is force-flushed to prevent unbounded memory growth
28
+ * from chatty child processes that produce output without newlines.
29
+ */
30
+ export const MAX_LINE_BUFFER_BYTES = 1024 * 1024; // 1 MB
31
+
32
+ /** Maximum characters for assistant text fragments in compacted events. */
33
+ export const MAX_ASSISTANT_TEXT_CHARS = DEFAULT_CHILD_PI.maxAssistantTextChars;
34
+
35
+ /** Maximum characters for tool-result fragments in compacted events. */
36
+ export const MAX_TOOL_RESULT_CHARS = DEFAULT_CHILD_PI.maxToolResultChars;
37
+
38
+ /** Maximum characters for tool-input fragments in compacted events. */
39
+ export const MAX_TOOL_INPUT_CHARS = DEFAULT_CHILD_PI.maxToolInputChars;
40
+
41
+ /** Maximum characters for general compactable content (used by TruncationStage). */
42
+ export const MAX_COMPACT_CONTENT_CHARS = DEFAULT_CHILD_PI.maxCompactContentChars;
@@ -0,0 +1,180 @@
1
+ /**
2
+ * child-pi-kill.ts — Kill/escape path for child Pi worker processes.
3
+ *
4
+ * Extracted from child-pi.ts (H-7 decomposition, step 2). Zero behavior change.
5
+ *
6
+ * Responsibilities:
7
+ * - appendBoundedTail(): bounded concatenation with truncation marker.
8
+ * - killProcessPid(): send SIGTERM → SIGKILL escalation, with Windows taskkill fallback.
9
+ * - killProcessTree(): log+kill a child by pid, optionally attach exit listener to clear timer.
10
+ * - terminateActiveChildPiProcesses(): kill all known active children.
11
+ * - registerActiveChild()/unregisterActiveChild(): book-keeping for the active-children Map.
12
+ * - Periodic zombie reaper (setInterval) that drops dead entries.
13
+ *
14
+ * The activeChildrenMap and hardKillTimers Map are kept here (not re-exported)
15
+ * because they are tightly coupled to the kill lifecycle.
16
+ */
17
+
18
+ import type { ChildProcess } from "node:child_process";
19
+ import { spawn } from "node:child_process";
20
+ import { DEFAULT_CHILD_PI } from "../config/defaults.ts";
21
+ import { logInternalError } from "../utils/internal-error.ts";
22
+ import { HARD_KILL_MS } from "./child-pi-constants.ts";
23
+ import { TailCaptureStage } from "./compact-stages/tail-capture-stage.ts";
24
+
25
+ const MAX_CAPTURE_BYTES = DEFAULT_CHILD_PI.maxCaptureBytes;
26
+
27
+ // Active children bookkeeping. Mutated by registerActiveChild/unregisterActiveChild.
28
+ const activeChildProcesses = new Map<number, ChildProcess>();
29
+ const childHardKillTimers = new Map<number, NodeJS.Timeout>();
30
+
31
+ // Periodic cleanup of dead child process entries to prevent memory leaks.
32
+ // If a child process never emits exit/close (zombie), the entry would leak.
33
+ setInterval(() => {
34
+ for (const [pid, child] of activeChildProcesses) {
35
+ try {
36
+ process.kill(pid, 0); // Throws ESRCH if dead
37
+ } catch {
38
+ activeChildProcesses.delete(pid);
39
+ }
40
+ }
41
+ }, 60_000).unref();
42
+
43
+ /** Register a newly-spawned child so it can be tracked + killed on shutdown. */
44
+ export function registerActiveChild(pid: number, child: ChildProcess): void {
45
+ activeChildProcesses.set(pid, child);
46
+ }
47
+
48
+ /** Remove a child from the active set once it has exited (or before kill). */
49
+ export function unregisterActiveChild(pid: number): void {
50
+ activeChildProcesses.delete(pid);
51
+ }
52
+
53
+ /** Clear the SIGKILL escalation timer for a pid (called on early exit). */
54
+ export function clearHardKillTimer(pid: number | undefined): void {
55
+ if (pid === undefined) return;
56
+ const timer = childHardKillTimers.get(pid);
57
+ if (timer) {
58
+ clearTimeout(timer);
59
+ childHardKillTimers.delete(pid);
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Append `chunk` to `current`, keeping the result within `maxBytes`. When the
65
+ * cap is exceeded, returns the tail (most recent bytes) prefixed with a marker
66
+ * showing how much was dropped. Delegates to TailCaptureStage so the logic
67
+ * stays consistent with the rest of the compaction pipeline.
68
+ */
69
+ export function appendBoundedTail(current: string, chunk: string, maxBytes = MAX_CAPTURE_BYTES): string {
70
+ return new TailCaptureStage({
71
+ maxBytes,
72
+ marker: `[pi-crew captured output truncated to last ${Math.round(maxBytes / 1024)} KiB]`,
73
+ }).apply(current + chunk);
74
+ }
75
+
76
+ function spawnTaskkillSafe(pid: number): void {
77
+ // B6: spawn taskkill and attach an 'error' listener. spawn() emits ENOENT/EACCES
78
+ // via the 'error' event — unhandled these would become uncaughtException and
79
+ // can crash the parent as an uncaught exception. taskkill is a standard Windows
80
+ // utility and rarely fails to spawn, but be defensive.
81
+ try {
82
+ const taskkillChild = spawn("taskkill", ["/pid", String(pid), "/t", "/f"], {
83
+ stdio: "ignore",
84
+ detached: false,
85
+ });
86
+ taskkillChild.on("error", (err) => {
87
+ logInternalError("child-pi.taskkill-spawn-error", err instanceof Error ? err : new Error(String(err)), `pid=${pid}`);
88
+ });
89
+ taskkillChild.unref();
90
+ } catch (error) {
91
+ logInternalError("child-pi.taskkill-sync-error", error instanceof Error ? error : new Error(String(error)), `pid=${pid}`);
92
+ }
93
+ }
94
+
95
+ export function killProcessPid(pid: number): void {
96
+ if (!Number.isInteger(pid) || pid <= 0) return;
97
+ try {
98
+ if (process.platform === "win32") {
99
+ // 3.8: Windows path uses taskkill /T /F (force kill the entire tree).
100
+ // taskkill itself can silently fail (PID gone, permission denied, etc.)
101
+ // so verify after 2s and log a warning if the process is still alive.
102
+ spawnTaskkillSafe(pid);
103
+ const verifyTimer = setTimeout(() => {
104
+ try {
105
+ process.kill(pid, 0); // throws ESRCH when dead
106
+ // Still alive — log and retry once.
107
+ logInternalError(
108
+ "child-pi.taskkill-stuck",
109
+ new Error(`process ${pid} still alive 2s after taskkill /T /F; retrying`),
110
+ `pid=${pid}`,
111
+ "error",
112
+ );
113
+ try {
114
+ spawnTaskkillSafe(pid);
115
+ } catch {
116
+ /* best-effort */
117
+ }
118
+ } catch {
119
+ // ESRCH or EPERM — process is gone. OK.
120
+ }
121
+ }, 2000);
122
+ verifyTimer.unref();
123
+ return;
124
+ }
125
+ try {
126
+ process.kill(-pid, "SIGTERM");
127
+ } catch (error) {
128
+ logInternalError("child-pi.sigterm", error, `pid=${pid}`);
129
+ try {
130
+ process.kill(pid, "SIGTERM");
131
+ } catch (fallbackError) {
132
+ logInternalError("child-pi.sigterm-absolute", fallbackError, `pid=${pid}`);
133
+ }
134
+ }
135
+ clearHardKillTimer(pid);
136
+ const hardKillTimer = setTimeout(() => {
137
+ try {
138
+ process.kill(-pid, "SIGKILL");
139
+ } catch (error) {
140
+ logInternalError("child-pi.sigkill", error, `pid=${pid}`);
141
+ try {
142
+ process.kill(pid, "SIGKILL");
143
+ } catch (fallbackError) {
144
+ logInternalError("child-pi.sigkill-absolute", fallbackError, `pid=${pid}`);
145
+ }
146
+ }
147
+ childHardKillTimers.delete(pid);
148
+ }, HARD_KILL_MS);
149
+ hardKillTimer.unref();
150
+ childHardKillTimers.set(pid, hardKillTimer);
151
+ } catch (error) {
152
+ logInternalError("child-pi.kill-process-pid", error, `pid=${pid}`);
153
+ }
154
+ }
155
+
156
+ export function killProcessTree(pid: number | undefined, child?: ChildProcess): void {
157
+ // Phase-0 diagnostic (HB-003a): capture who invoked killProcessTree so the
158
+ // exit-null race has a provenance trail. .stack is best-effort (may be undefined
159
+ // under deep async), so we take a snapshot lazily.
160
+ try {
161
+ const callerStack = new Error("killProcessTree caller").stack ?? "(no stack)";
162
+ logInternalError(
163
+ "child-pi.kill-process-tree-invoked",
164
+ new Error(`pid=${pid} called from:\n${callerStack.split("\n").slice(0, 8).join("\n")}`),
165
+ `pid=${pid}`,
166
+ );
167
+ } catch {
168
+ /* diagnostic best-effort */
169
+ }
170
+ if (!pid || !Number.isInteger(pid) || pid <= 0) return;
171
+ if (child && child.exitCode !== null) return;
172
+ killProcessPid(pid);
173
+ child?.once("exit", () => clearHardKillTimer(pid));
174
+ }
175
+
176
+ export function terminateActiveChildPiProcesses(): number {
177
+ const entries = [...activeChildProcesses.entries()];
178
+ for (const [pid, child] of entries) killProcessTree(pid, child);
179
+ return entries.length;
180
+ }