@caupulican/pi-agent-core 0.81.32 → 0.81.35

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.
@@ -1,10 +1,22 @@
1
+ /**
2
+ * Process-tree kill primitives.
3
+ *
4
+ * WHY: node's ChildProcess.killed flag is set when a signal is sent, not when the
5
+ * process exits. Graceful escalation therefore follows the spawned child's exit
6
+ * event and a one-shot deadline; it never polls PID liveness.
7
+ *
8
+ * Group kill (-pid) requires the target to be a process-group leader (spawned with
9
+ * detached: true). Both functions fall back to single-pid signaling otherwise.
10
+ */
11
+ import { type ChildProcess } from "node:child_process";
1
12
  export declare function isProcessAlive(pid: number): boolean;
2
13
  export interface KillTreeOptions {
3
- /** How long to wait for SIGTERM to work before SIGKILL. Default 5000ms. */
14
+ /** How long to wait for the child's exit event after SIGTERM before SIGKILL. Default 5000ms. */
4
15
  graceMs?: number;
5
16
  }
6
- /** Graceful tree kill: SIGTERM poll liveness → SIGKILL. */
7
- export declare function killTree(pid: number, opts?: KillTreeOptions): Promise<"already_dead" | "terminated" | "killed">;
8
- /** Immediate tree kill (SIGKILL / taskkill). Port of coding-agent's killProcessTree. */
17
+ export type KillTreeOutcome = "already_dead" | "terminated" | "killed";
18
+ /** Graceful tree kill: SIGTERM child exit event or one-shot deadline → SIGKILL. */
19
+ export declare function killTree(child: ChildProcess, opts?: KillTreeOptions): Promise<KillTreeOutcome>;
20
+ /** Immediate tree kill (SIGKILL / synchronous taskkill). */
9
21
  export declare function killTreeNow(pid: number): void;
10
22
  //# sourceMappingURL=process-tree.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"process-tree.d.ts","sourceRoot":"","sources":["../../src/reliability/process-tree.ts"],"names":[],"mappings":"AAaA,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAQnD;AAgBD,MAAM,WAAW,eAAe;IAC/B,2EAA2E;IAC3E,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,iEAA6D;AAC7D,wBAAsB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,GAAG,YAAY,GAAG,QAAQ,CAAC,CAiBrH;AAED,wFAAwF;AACxF,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAU7C","sourcesContent":["/**\n * Process-tree kill primitives.\n *\n * WHY: node's ChildProcess.killed flag is set the moment a signal is SENT, not when\n * the process dies gating SIGKILL escalation on it (as the legacy exec path did)\n * makes escalation dead code, so a SIGTERM-trapping child survives forever. These\n * primitives escalate based on actual liveness probes instead.\n *\n * Group kill (-pid) requires the target to be a process-group leader (spawned with\n * detached: true). Both functions fall back to single-pid signaling otherwise.\n */\nimport { spawn } from \"node:child_process\";\n\nexport function isProcessAlive(pid: number): boolean {\n\ttry {\n\t\tprocess.kill(pid, 0);\n\t\treturn true;\n\t} catch (err) {\n\t\t// EPERM means it exists but we lack permission — still alive.\n\t\treturn (err as NodeJS.ErrnoException).code === \"EPERM\";\n\t}\n}\n\nfunction signalTree(pid: number, signal: NodeJS.Signals): boolean {\n\ttry {\n\t\tprocess.kill(-pid, signal);\n\t\treturn true;\n\t} catch {\n\t\ttry {\n\t\t\tprocess.kill(pid, signal);\n\t\t\treturn true;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n}\n\nexport interface KillTreeOptions {\n\t/** How long to wait for SIGTERM to work before SIGKILL. Default 5000ms. */\n\tgraceMs?: number;\n}\n\n/** Graceful tree kill: SIGTERM → poll liveness → SIGKILL. */\nexport async function killTree(pid: number, opts?: KillTreeOptions): Promise<\"already_dead\" | \"terminated\" | \"killed\"> {\n\tif (process.platform === \"win32\") {\n\t\tif (!isProcessAlive(pid)) return \"already_dead\";\n\t\tkillTreeNow(pid);\n\t\treturn \"killed\";\n\t}\n\tif (!isProcessAlive(pid)) return \"already_dead\";\n\tsignalTree(pid, \"SIGTERM\");\n\tconst graceMs = opts?.graceMs ?? 5000;\n\tconst deadline = Date.now() + graceMs;\n\twhile (Date.now() < deadline) {\n\t\tif (!isProcessAlive(pid)) return \"terminated\";\n\t\tawait new Promise((resolve) => setTimeout(resolve, Math.min(50, graceMs)));\n\t}\n\tif (!isProcessAlive(pid)) return \"terminated\";\n\tsignalTree(pid, \"SIGKILL\");\n\treturn \"killed\";\n}\n\n/** Immediate tree kill (SIGKILL / taskkill). Port of coding-agent's killProcessTree. */\nexport function killTreeNow(pid: number): void {\n\tif (process.platform === \"win32\") {\n\t\ttry {\n\t\t\tspawn(\"taskkill\", [\"/F\", \"/T\", \"/PID\", String(pid)], { stdio: \"ignore\", detached: true, windowsHide: true });\n\t\t} catch {\n\t\t\t// Ignore errors if taskkill fails.\n\t\t}\n\t} else {\n\t\tsignalTree(pid, \"SIGKILL\");\n\t}\n}\n"]}
1
+ {"version":3,"file":"process-tree.d.ts","sourceRoot":"","sources":["../../src/reliability/process-tree.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,KAAK,YAAY,EAAa,MAAM,oBAAoB,CAAC;AAIlE,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAQnD;AAoBD,MAAM,WAAW,eAAe;IAC/B,gGAAgG;IAChG,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,eAAe,GAAG,cAAc,GAAG,YAAY,GAAG,QAAQ,CAAC;AAEvE,yFAAqF;AACrF,wBAAgB,QAAQ,CAAC,KAAK,EAAE,YAAY,EAAE,IAAI,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC,CAmE9F;AAED,4DAA4D;AAC5D,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAU7C","sourcesContent":["/**\n * Process-tree kill primitives.\n *\n * WHY: node's ChildProcess.killed flag is set when a signal is sent, not when the\n * process exits. Graceful escalation therefore follows the spawned child's exit\n * event and a one-shot deadline; it never polls PID liveness.\n *\n * Group kill (-pid) requires the target to be a process-group leader (spawned with\n * detached: true). Both functions fall back to single-pid signaling otherwise.\n */\nimport { type ChildProcess, spawnSync } from \"node:child_process\";\n\nconst KILL_ACKNOWLEDGEMENT_MS = 1000;\n\nexport function isProcessAlive(pid: number): boolean {\n\ttry {\n\t\tprocess.kill(pid, 0);\n\t\treturn true;\n\t} catch (err) {\n\t\t// EPERM means it exists but we lack permission — still alive.\n\t\treturn (err as NodeJS.ErrnoException).code === \"EPERM\";\n\t}\n}\n\nfunction signalTree(pid: number, signal: NodeJS.Signals): boolean {\n\ttry {\n\t\tprocess.kill(-pid, signal);\n\t\treturn true;\n\t} catch {\n\t\ttry {\n\t\t\tprocess.kill(pid, signal);\n\t\t\treturn true;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n}\n\nfunction isChildTerminal(child: ChildProcess): boolean {\n\treturn child.exitCode !== null || child.signalCode !== null;\n}\n\nexport interface KillTreeOptions {\n\t/** How long to wait for the child's exit event after SIGTERM before SIGKILL. Default 5000ms. */\n\tgraceMs?: number;\n}\n\nexport type KillTreeOutcome = \"already_dead\" | \"terminated\" | \"killed\";\n\n/** Graceful tree kill: SIGTERM → child exit event or one-shot deadline → SIGKILL. */\nexport function killTree(child: ChildProcess, opts?: KillTreeOptions): Promise<KillTreeOutcome> {\n\tconst pid = child.pid;\n\tif (pid === undefined || isChildTerminal(child)) return Promise.resolve(\"already_dead\");\n\n\treturn new Promise((resolve) => {\n\t\tlet settled = false;\n\t\tlet escalated = false;\n\t\tlet escalationTimer: NodeJS.Timeout | undefined;\n\t\tlet acknowledgementTimer: NodeJS.Timeout | undefined;\n\n\t\tconst cleanup = () => {\n\t\t\tif (escalationTimer) clearTimeout(escalationTimer);\n\t\t\tif (acknowledgementTimer) clearTimeout(acknowledgementTimer);\n\t\t\tchild.removeListener(\"exit\", onExit);\n\t\t\tchild.removeListener(\"error\", onError);\n\t\t};\n\t\tconst settle = (outcome: KillTreeOutcome) => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tcleanup();\n\t\t\tresolve(outcome);\n\t\t};\n\t\tconst onExit = () => settle(escalated ? \"killed\" : \"terminated\");\n\t\tconst onError = () => settle(escalated ? \"killed\" : \"already_dead\");\n\n\t\tchild.once(\"exit\", onExit);\n\t\tchild.once(\"error\", onError);\n\t\tif (isChildTerminal(child)) {\n\t\t\tsettle(\"already_dead\");\n\t\t\treturn;\n\t\t}\n\n\t\tif (process.platform === \"win32\") {\n\t\t\tescalated = true;\n\t\t\tkillTreeNow(pid);\n\t\t\tacknowledgementTimer = setTimeout(() => {\n\t\t\t\tchild.unref();\n\t\t\t\tsettle(\"killed\");\n\t\t\t}, KILL_ACKNOWLEDGEMENT_MS);\n\t\t\tacknowledgementTimer.unref();\n\t\t\treturn;\n\t\t}\n\n\t\tif (!signalTree(pid, \"SIGTERM\")) {\n\t\t\tsettle(\"already_dead\");\n\t\t\treturn;\n\t\t}\n\t\tconst graceMs = Math.max(0, opts?.graceMs ?? 5000);\n\t\tescalationTimer = setTimeout(() => {\n\t\t\tescalationTimer = undefined;\n\t\t\tif (isChildTerminal(child)) {\n\t\t\t\tsettle(\"terminated\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tescalated = true;\n\t\t\tif (!signalTree(pid, \"SIGKILL\")) {\n\t\t\t\tsettle(\"terminated\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tacknowledgementTimer = setTimeout(() => {\n\t\t\t\tchild.unref();\n\t\t\t\tsettle(\"killed\");\n\t\t\t}, KILL_ACKNOWLEDGEMENT_MS);\n\t\t\tacknowledgementTimer.unref();\n\t\t}, graceMs);\n\t\tescalationTimer.unref();\n\t});\n}\n\n/** Immediate tree kill (SIGKILL / synchronous taskkill). */\nexport function killTreeNow(pid: number): void {\n\tif (process.platform === \"win32\") {\n\t\tspawnSync(\"taskkill\", [\"/F\", \"/T\", \"/PID\", String(pid)], {\n\t\t\tstdio: \"ignore\",\n\t\t\ttimeout: 10_000,\n\t\t\twindowsHide: true,\n\t\t});\n\t} else {\n\t\tsignalTree(pid, \"SIGKILL\");\n\t}\n}\n"]}
@@ -1,15 +1,15 @@
1
1
  /**
2
2
  * Process-tree kill primitives.
3
3
  *
4
- * WHY: node's ChildProcess.killed flag is set the moment a signal is SENT, not when
5
- * the process dies gating SIGKILL escalation on it (as the legacy exec path did)
6
- * makes escalation dead code, so a SIGTERM-trapping child survives forever. These
7
- * primitives escalate based on actual liveness probes instead.
4
+ * WHY: node's ChildProcess.killed flag is set when a signal is sent, not when the
5
+ * process exits. Graceful escalation therefore follows the spawned child's exit
6
+ * event and a one-shot deadline; it never polls PID liveness.
8
7
  *
9
8
  * Group kill (-pid) requires the target to be a process-group leader (spawned with
10
9
  * detached: true). Both functions fall back to single-pid signaling otherwise.
11
10
  */
12
- import { spawn } from "node:child_process";
11
+ import { spawnSync } from "node:child_process";
12
+ const KILL_ACKNOWLEDGEMENT_MS = 1000;
13
13
  export function isProcessAlive(pid) {
14
14
  try {
15
15
  process.kill(pid, 0);
@@ -35,38 +35,85 @@ function signalTree(pid, signal) {
35
35
  }
36
36
  }
37
37
  }
38
- /** Graceful tree kill: SIGTERM → poll liveness → SIGKILL. */
39
- export async function killTree(pid, opts) {
40
- if (process.platform === "win32") {
41
- if (!isProcessAlive(pid))
42
- return "already_dead";
43
- killTreeNow(pid);
44
- return "killed";
45
- }
46
- if (!isProcessAlive(pid))
47
- return "already_dead";
48
- signalTree(pid, "SIGTERM");
49
- const graceMs = opts?.graceMs ?? 5000;
50
- const deadline = Date.now() + graceMs;
51
- while (Date.now() < deadline) {
52
- if (!isProcessAlive(pid))
53
- return "terminated";
54
- await new Promise((resolve) => setTimeout(resolve, Math.min(50, graceMs)));
55
- }
56
- if (!isProcessAlive(pid))
57
- return "terminated";
58
- signalTree(pid, "SIGKILL");
59
- return "killed";
38
+ function isChildTerminal(child) {
39
+ return child.exitCode !== null || child.signalCode !== null;
60
40
  }
61
- /** Immediate tree kill (SIGKILL / taskkill). Port of coding-agent's killProcessTree. */
62
- export function killTreeNow(pid) {
63
- if (process.platform === "win32") {
64
- try {
65
- spawn("taskkill", ["/F", "/T", "/PID", String(pid)], { stdio: "ignore", detached: true, windowsHide: true });
41
+ /** Graceful tree kill: SIGTERM child exit event or one-shot deadline → SIGKILL. */
42
+ export function killTree(child, opts) {
43
+ const pid = child.pid;
44
+ if (pid === undefined || isChildTerminal(child))
45
+ return Promise.resolve("already_dead");
46
+ return new Promise((resolve) => {
47
+ let settled = false;
48
+ let escalated = false;
49
+ let escalationTimer;
50
+ let acknowledgementTimer;
51
+ const cleanup = () => {
52
+ if (escalationTimer)
53
+ clearTimeout(escalationTimer);
54
+ if (acknowledgementTimer)
55
+ clearTimeout(acknowledgementTimer);
56
+ child.removeListener("exit", onExit);
57
+ child.removeListener("error", onError);
58
+ };
59
+ const settle = (outcome) => {
60
+ if (settled)
61
+ return;
62
+ settled = true;
63
+ cleanup();
64
+ resolve(outcome);
65
+ };
66
+ const onExit = () => settle(escalated ? "killed" : "terminated");
67
+ const onError = () => settle(escalated ? "killed" : "already_dead");
68
+ child.once("exit", onExit);
69
+ child.once("error", onError);
70
+ if (isChildTerminal(child)) {
71
+ settle("already_dead");
72
+ return;
66
73
  }
67
- catch {
68
- // Ignore errors if taskkill fails.
74
+ if (process.platform === "win32") {
75
+ escalated = true;
76
+ killTreeNow(pid);
77
+ acknowledgementTimer = setTimeout(() => {
78
+ child.unref();
79
+ settle("killed");
80
+ }, KILL_ACKNOWLEDGEMENT_MS);
81
+ acknowledgementTimer.unref();
82
+ return;
83
+ }
84
+ if (!signalTree(pid, "SIGTERM")) {
85
+ settle("already_dead");
86
+ return;
69
87
  }
88
+ const graceMs = Math.max(0, opts?.graceMs ?? 5000);
89
+ escalationTimer = setTimeout(() => {
90
+ escalationTimer = undefined;
91
+ if (isChildTerminal(child)) {
92
+ settle("terminated");
93
+ return;
94
+ }
95
+ escalated = true;
96
+ if (!signalTree(pid, "SIGKILL")) {
97
+ settle("terminated");
98
+ return;
99
+ }
100
+ acknowledgementTimer = setTimeout(() => {
101
+ child.unref();
102
+ settle("killed");
103
+ }, KILL_ACKNOWLEDGEMENT_MS);
104
+ acknowledgementTimer.unref();
105
+ }, graceMs);
106
+ escalationTimer.unref();
107
+ });
108
+ }
109
+ /** Immediate tree kill (SIGKILL / synchronous taskkill). */
110
+ export function killTreeNow(pid) {
111
+ if (process.platform === "win32") {
112
+ spawnSync("taskkill", ["/F", "/T", "/PID", String(pid)], {
113
+ stdio: "ignore",
114
+ timeout: 10_000,
115
+ windowsHide: true,
116
+ });
70
117
  }
71
118
  else {
72
119
  signalTree(pid, "SIGKILL");
@@ -1 +1 @@
1
- {"version":3,"file":"process-tree.js","sourceRoot":"","sources":["../../src/reliability/process-tree.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAE3C,MAAM,UAAU,cAAc,CAAC,GAAW,EAAW;IACpD,IAAI,CAAC;QACJ,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACrB,OAAO,IAAI,CAAC;IACb,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,gEAA8D;QAC9D,OAAQ,GAA6B,CAAC,IAAI,KAAK,OAAO,CAAC;IACxD,CAAC;AAAA,CACD;AAED,SAAS,UAAU,CAAC,GAAW,EAAE,MAAsB,EAAW;IACjE,IAAI,CAAC;QACJ,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QAC3B,OAAO,IAAI,CAAC;IACb,CAAC;IAAC,MAAM,CAAC;QACR,IAAI,CAAC;YACJ,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;YAC1B,OAAO,IAAI,CAAC;QACb,CAAC;QAAC,MAAM,CAAC;YACR,OAAO,KAAK,CAAC;QACd,CAAC;IACF,CAAC;AAAA,CACD;AAOD,iEAA6D;AAC7D,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,GAAW,EAAE,IAAsB,EAAqD;IACtH,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QAClC,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC;YAAE,OAAO,cAAc,CAAC;QAChD,WAAW,CAAC,GAAG,CAAC,CAAC;QACjB,OAAO,QAAQ,CAAC;IACjB,CAAC;IACD,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC;QAAE,OAAO,cAAc,CAAC;IAChD,UAAU,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;IAC3B,MAAM,OAAO,GAAG,IAAI,EAAE,OAAO,IAAI,IAAI,CAAC;IACtC,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC;IACtC,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,EAAE,CAAC;QAC9B,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC;YAAE,OAAO,YAAY,CAAC;QAC9C,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IAC5E,CAAC;IACD,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC;QAAE,OAAO,YAAY,CAAC;IAC9C,UAAU,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;IAC3B,OAAO,QAAQ,CAAC;AAAA,CAChB;AAED,wFAAwF;AACxF,MAAM,UAAU,WAAW,CAAC,GAAW,EAAQ;IAC9C,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QAClC,IAAI,CAAC;YACJ,KAAK,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC;QAC9G,CAAC;QAAC,MAAM,CAAC;YACR,mCAAmC;QACpC,CAAC;IACF,CAAC;SAAM,CAAC;QACP,UAAU,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;IAC5B,CAAC;AAAA,CACD","sourcesContent":["/**\n * Process-tree kill primitives.\n *\n * WHY: node's ChildProcess.killed flag is set the moment a signal is SENT, not when\n * the process dies gating SIGKILL escalation on it (as the legacy exec path did)\n * makes escalation dead code, so a SIGTERM-trapping child survives forever. These\n * primitives escalate based on actual liveness probes instead.\n *\n * Group kill (-pid) requires the target to be a process-group leader (spawned with\n * detached: true). Both functions fall back to single-pid signaling otherwise.\n */\nimport { spawn } from \"node:child_process\";\n\nexport function isProcessAlive(pid: number): boolean {\n\ttry {\n\t\tprocess.kill(pid, 0);\n\t\treturn true;\n\t} catch (err) {\n\t\t// EPERM means it exists but we lack permission — still alive.\n\t\treturn (err as NodeJS.ErrnoException).code === \"EPERM\";\n\t}\n}\n\nfunction signalTree(pid: number, signal: NodeJS.Signals): boolean {\n\ttry {\n\t\tprocess.kill(-pid, signal);\n\t\treturn true;\n\t} catch {\n\t\ttry {\n\t\t\tprocess.kill(pid, signal);\n\t\t\treturn true;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n}\n\nexport interface KillTreeOptions {\n\t/** How long to wait for SIGTERM to work before SIGKILL. Default 5000ms. */\n\tgraceMs?: number;\n}\n\n/** Graceful tree kill: SIGTERM → poll liveness → SIGKILL. */\nexport async function killTree(pid: number, opts?: KillTreeOptions): Promise<\"already_dead\" | \"terminated\" | \"killed\"> {\n\tif (process.platform === \"win32\") {\n\t\tif (!isProcessAlive(pid)) return \"already_dead\";\n\t\tkillTreeNow(pid);\n\t\treturn \"killed\";\n\t}\n\tif (!isProcessAlive(pid)) return \"already_dead\";\n\tsignalTree(pid, \"SIGTERM\");\n\tconst graceMs = opts?.graceMs ?? 5000;\n\tconst deadline = Date.now() + graceMs;\n\twhile (Date.now() < deadline) {\n\t\tif (!isProcessAlive(pid)) return \"terminated\";\n\t\tawait new Promise((resolve) => setTimeout(resolve, Math.min(50, graceMs)));\n\t}\n\tif (!isProcessAlive(pid)) return \"terminated\";\n\tsignalTree(pid, \"SIGKILL\");\n\treturn \"killed\";\n}\n\n/** Immediate tree kill (SIGKILL / taskkill). Port of coding-agent's killProcessTree. */\nexport function killTreeNow(pid: number): void {\n\tif (process.platform === \"win32\") {\n\t\ttry {\n\t\t\tspawn(\"taskkill\", [\"/F\", \"/T\", \"/PID\", String(pid)], { stdio: \"ignore\", detached: true, windowsHide: true });\n\t\t} catch {\n\t\t\t// Ignore errors if taskkill fails.\n\t\t}\n\t} else {\n\t\tsignalTree(pid, \"SIGKILL\");\n\t}\n}\n"]}
1
+ {"version":3,"file":"process-tree.js","sourceRoot":"","sources":["../../src/reliability/process-tree.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Process-tree kill primitives.\n *\n * WHY: node's ChildProcess.killed flag is set when a signal is sent, not when the\n * process exits. Graceful escalation therefore follows the spawned child's exit\n * event and a one-shot deadline; it never polls PID liveness.\n *\n * Group kill (-pid) requires the target to be a process-group leader (spawned with\n * detached: true). Both functions fall back to single-pid signaling otherwise.\n */\nimport { type ChildProcess, spawnSync } from \"node:child_process\";\n\nconst KILL_ACKNOWLEDGEMENT_MS = 1000;\n\nexport function isProcessAlive(pid: number): boolean {\n\ttry {\n\t\tprocess.kill(pid, 0);\n\t\treturn true;\n\t} catch (err) {\n\t\t// EPERM means it exists but we lack permission — still alive.\n\t\treturn (err as NodeJS.ErrnoException).code === \"EPERM\";\n\t}\n}\n\nfunction signalTree(pid: number, signal: NodeJS.Signals): boolean {\n\ttry {\n\t\tprocess.kill(-pid, signal);\n\t\treturn true;\n\t} catch {\n\t\ttry {\n\t\t\tprocess.kill(pid, signal);\n\t\t\treturn true;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n}\n\nfunction isChildTerminal(child: ChildProcess): boolean {\n\treturn child.exitCode !== null || child.signalCode !== null;\n}\n\nexport interface KillTreeOptions {\n\t/** How long to wait for the child's exit event after SIGTERM before SIGKILL. Default 5000ms. */\n\tgraceMs?: number;\n}\n\nexport type KillTreeOutcome = \"already_dead\" | \"terminated\" | \"killed\";\n\n/** Graceful tree kill: SIGTERM → child exit event or one-shot deadline → SIGKILL. */\nexport function killTree(child: ChildProcess, opts?: KillTreeOptions): Promise<KillTreeOutcome> {\n\tconst pid = child.pid;\n\tif (pid === undefined || isChildTerminal(child)) return Promise.resolve(\"already_dead\");\n\n\treturn new Promise((resolve) => {\n\t\tlet settled = false;\n\t\tlet escalated = false;\n\t\tlet escalationTimer: NodeJS.Timeout | undefined;\n\t\tlet acknowledgementTimer: NodeJS.Timeout | undefined;\n\n\t\tconst cleanup = () => {\n\t\t\tif (escalationTimer) clearTimeout(escalationTimer);\n\t\t\tif (acknowledgementTimer) clearTimeout(acknowledgementTimer);\n\t\t\tchild.removeListener(\"exit\", onExit);\n\t\t\tchild.removeListener(\"error\", onError);\n\t\t};\n\t\tconst settle = (outcome: KillTreeOutcome) => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tcleanup();\n\t\t\tresolve(outcome);\n\t\t};\n\t\tconst onExit = () => settle(escalated ? \"killed\" : \"terminated\");\n\t\tconst onError = () => settle(escalated ? \"killed\" : \"already_dead\");\n\n\t\tchild.once(\"exit\", onExit);\n\t\tchild.once(\"error\", onError);\n\t\tif (isChildTerminal(child)) {\n\t\t\tsettle(\"already_dead\");\n\t\t\treturn;\n\t\t}\n\n\t\tif (process.platform === \"win32\") {\n\t\t\tescalated = true;\n\t\t\tkillTreeNow(pid);\n\t\t\tacknowledgementTimer = setTimeout(() => {\n\t\t\t\tchild.unref();\n\t\t\t\tsettle(\"killed\");\n\t\t\t}, KILL_ACKNOWLEDGEMENT_MS);\n\t\t\tacknowledgementTimer.unref();\n\t\t\treturn;\n\t\t}\n\n\t\tif (!signalTree(pid, \"SIGTERM\")) {\n\t\t\tsettle(\"already_dead\");\n\t\t\treturn;\n\t\t}\n\t\tconst graceMs = Math.max(0, opts?.graceMs ?? 5000);\n\t\tescalationTimer = setTimeout(() => {\n\t\t\tescalationTimer = undefined;\n\t\t\tif (isChildTerminal(child)) {\n\t\t\t\tsettle(\"terminated\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tescalated = true;\n\t\t\tif (!signalTree(pid, \"SIGKILL\")) {\n\t\t\t\tsettle(\"terminated\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tacknowledgementTimer = setTimeout(() => {\n\t\t\t\tchild.unref();\n\t\t\t\tsettle(\"killed\");\n\t\t\t}, KILL_ACKNOWLEDGEMENT_MS);\n\t\t\tacknowledgementTimer.unref();\n\t\t}, graceMs);\n\t\tescalationTimer.unref();\n\t});\n}\n\n/** Immediate tree kill (SIGKILL / synchronous taskkill). */\nexport function killTreeNow(pid: number): void {\n\tif (process.platform === \"win32\") {\n\t\tspawnSync(\"taskkill\", [\"/F\", \"/T\", \"/PID\", String(pid)], {\n\t\t\tstdio: \"ignore\",\n\t\t\ttimeout: 10_000,\n\t\t\twindowsHide: true,\n\t\t});\n\t} else {\n\t\tsignalTree(pid, \"SIGKILL\");\n\t}\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caupulican/pi-agent-core",
3
- "version": "0.81.32",
3
+ "version": "0.81.35",
4
4
  "description": "General-purpose agent with transport abstraction, state management, and attachment support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -33,7 +33,7 @@
33
33
  "prepublishOnly": "npm run clean && npm run build"
34
34
  },
35
35
  "dependencies": {
36
- "@caupulican/pi-ai": "^0.81.32",
36
+ "@caupulican/pi-ai": "^0.81.35",
37
37
  "ignore": "7.0.5",
38
38
  "typebox": "1.1.38",
39
39
  "yaml": "2.9.0"