teapot-coding-agent 0.37.101 → 0.37.103

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,32 @@
1
+ import { spawnSync } from "node:child_process";
1
2
  export function canKillProcessGroup(platform = process.platform) {
2
3
  return platform !== "win32";
3
4
  }
4
- export function killTree(child, platform = process.platform) {
5
+ const runTaskkill = (file, args) => {
6
+ try {
7
+ const r = spawnSync(file, args, { stdio: "ignore", windowsHide: true });
8
+ return { status: r.status };
9
+ }
10
+ catch {
11
+ return { status: null };
12
+ }
13
+ };
14
+ export function killTree(child, platform = process.platform, run = runTaskkill) {
5
15
  if (!child?.pid)
6
16
  return { killed: false, wholeTree: false, reason: "no-pid" };
7
17
  if (!canKillProcessGroup(platform)) {
18
+ /**
19
+ * Windows has no process groups, and `process.kill(pid)` terminates ONLY the process you name — the
20
+ * shell — so everything it started keeps running. That is not just a leak: the caller waits for the
21
+ * child's stdio to close, and a surviving grandchild holds those pipes open, so the tool call never
22
+ * returns even though the timeout fired (#190).
23
+ *
24
+ * `taskkill /T` walks the parent-child tree and is the only way to get all of it. If it is missing or
25
+ * refuses, fall back to the direct kill and SAY the tree is uncovered.
26
+ */
27
+ const r = run("taskkill", ["/pid", String(child.pid), "/T", "/F"]);
28
+ if (r.status === 0)
29
+ return { killed: true, wholeTree: true };
8
30
  try {
9
31
  process.kill(child.pid, "SIGKILL");
10
32
  return { killed: true, wholeTree: false };
@@ -39,11 +61,13 @@ export function killTree(child, platform = process.platform) {
39
61
  }
40
62
  }
41
63
  export function describeKill(outcome) {
64
+ // "tree", not "process group": on Windows the mechanism is `taskkill /T`, and the report is read by an
65
+ // operator who cannot see which platform it came from
42
66
  if (outcome.killed && outcome.wholeTree)
43
- return "killed (whole process group)";
67
+ return "killed (whole process tree)";
44
68
  if (outcome.killed) {
45
- return ("killed the process, but this platform has no process groups — any " +
46
- "background children it started may still be running");
69
+ return ("killed the process, but its children may still be running — " +
70
+ "a background command it started could survive this");
47
71
  }
48
72
  switch (outcome.reason) {
49
73
  case "no-pid":
@@ -466,7 +466,7 @@ function runShell(cmd, ctx, timeoutMs, explicitTimeout = false) {
466
466
  resolve({ ok: false, result: `spawn error: ${err.message}${elapsed(t0)}` });
467
467
  }
468
468
  });
469
- child.on("close", (code, signal) => {
469
+ const finish = (code, signal) => {
470
470
  clearTimeout(timer);
471
471
  ctx.signal?.removeEventListener("abort", onAbort);
472
472
  if (done)
@@ -485,7 +485,23 @@ function runShell(cmd, ctx, timeoutMs, explicitTimeout = false) {
485
485
  clip(out.trim() || "(no output)", ctx.maxOutputBytes) +
486
486
  elapsed(t0),
487
487
  });
488
+ };
489
+ /**
490
+ * `close` is the normal end: it fires only once the child AND everything holding its stdio has gone, so
491
+ * the output is complete.
492
+ *
493
+ * `exit` is the escape hatch for the kill paths, and without it a timeout can hang forever. `close`
494
+ * waits for the pipes to close, and a surviving grandchild holds those pipes open — measured on Linux
495
+ * with a child that leaves the process group: `exit` at 501ms, `close` still not fired 2.5s later. On
496
+ * Windows that is the NORMAL case, because `process.kill` cannot end anything but the shell (#190,
497
+ * "timeout時間超過って / stopおしても継続しちゃった"). Once a kill has been requested there is no more
498
+ * waiting to do, so settle on `exit`.
499
+ */
500
+ child.on("exit", (code, signal) => {
501
+ if (killReason)
502
+ finish(code, signal);
488
503
  });
504
+ child.on("close", (code, signal) => finish(code, signal));
489
505
  });
490
506
  }
491
507
  function matchLines(text, needle) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teapot-coding-agent",
3
- "version": "0.37.101",
3
+ "version": "0.37.103",
4
4
  "description": "A lightweight, always-on multi-agent harness for AI coding agents",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-or-later",