@sema-agent/core 1.432.0 → 1.434.0

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,4 +1,4 @@
1
- import { spawn } from "node:child_process";
1
+ import { spawn, spawnSync } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  const DEFAULT_GRACE_MS = 3000;
@@ -16,7 +16,7 @@ export function killProcessTree(pid, opts) {
16
16
  return;
17
17
  }
18
18
  const graceMs = normalizeGraceMs(opts?.graceMs);
19
- killProcessTreeWindows(pid, graceMs, opts?.stillRunning);
19
+ killProcessTreeWindows(pid, graceMs, undefined);
20
20
  return;
21
21
  }
22
22
  const useGroupKill = opts?.detached !== false;
@@ -25,8 +25,18 @@ export function killProcessTree(pid, opts) {
25
25
  return;
26
26
  }
27
27
  const graceMs = normalizeGraceMs(opts?.graceMs);
28
- signalProcessTreeUnix(pid, "SIGTERM", useGroupKill);
28
+ const termDescendants = signalProcessTreeUnix(pid, "SIGTERM", useGroupKill);
29
29
  setTimeout(() => {
30
+ const rediscovered = enumerateDescendantsSync(pid);
31
+ const verifiedCarriedForward = new Map();
32
+ for (const [p, originalEtime] of termDescendants) {
33
+ const currentEtime = rediscovered.etimeOf.get(p);
34
+ if (currentEtime !== undefined && verifyStillSameProcess(originalEtime, currentEtime)) {
35
+ verifiedCarriedForward.set(p, currentEtime);
36
+ }
37
+ }
38
+ const knownDescendants = new Map([...verifiedCarriedForward, ...rediscovered.descendants]);
39
+ killEach(knownDescendants.keys(), "SIGKILL");
30
40
  if (escalationCancelled(opts?.stillRunning)) {
31
41
  if (useGroupKill && isProcessAlive(-pid)) {
32
42
  try {
@@ -43,7 +53,19 @@ export function killProcessTree(pid, opts) {
43
53
  if (!stillAlive) {
44
54
  return;
45
55
  }
46
- signalProcessTreeUnix(pid, "SIGKILL", useGroupKill);
56
+ if (useGroupKill) {
57
+ try {
58
+ process.kill(-pid, "SIGKILL");
59
+ return;
60
+ }
61
+ catch {
62
+ }
63
+ }
64
+ try {
65
+ process.kill(pid, "SIGKILL");
66
+ }
67
+ catch {
68
+ }
47
69
  }, graceMs).unref();
48
70
  }
49
71
  function escalationCancelled(stillRunning) {
@@ -82,19 +104,107 @@ function isProcessAlive(pid) {
82
104
  }
83
105
  }
84
106
  function signalProcessTreeUnix(pid, signal, useGroupKill) {
107
+ const descendants = enumerateDescendantsSync(pid).descendants;
85
108
  if (useGroupKill) {
86
109
  try {
87
110
  process.kill(-pid, signal);
88
- return;
89
111
  }
90
112
  catch {
113
+ try {
114
+ process.kill(pid, signal);
115
+ }
116
+ catch {
117
+ }
91
118
  }
119
+ killEach(descendants.keys(), signal);
120
+ return descendants;
92
121
  }
93
122
  try {
94
123
  process.kill(pid, signal);
95
124
  }
96
125
  catch {
97
126
  }
127
+ killEach(descendants.keys(), signal);
128
+ return descendants;
129
+ }
130
+ function killEach(pids, signal) {
131
+ for (const pid of pids) {
132
+ try {
133
+ process.kill(pid, signal);
134
+ }
135
+ catch {
136
+ }
137
+ }
138
+ }
139
+ const PS_ENUMERATION_TIMEOUT_MS = 500;
140
+ const EMPTY_SNAPSHOT = { descendants: new Map(), etimeOf: new Map() };
141
+ function enumerateDescendantsSync(rootPid) {
142
+ if (process.platform === "win32")
143
+ return EMPTY_SNAPSHOT;
144
+ let out;
145
+ try {
146
+ const result = spawnSync("ps", ["-A", "-o", "pid=", "-o", "ppid=", "-o", "etime="], {
147
+ timeout: PS_ENUMERATION_TIMEOUT_MS,
148
+ encoding: "utf8",
149
+ windowsHide: true,
150
+ });
151
+ if (result.error || typeof result.stdout !== "string")
152
+ return EMPTY_SNAPSHOT;
153
+ out = result.stdout;
154
+ }
155
+ catch {
156
+ return EMPTY_SNAPSHOT;
157
+ }
158
+ const childrenOf = new Map();
159
+ const etimeOf = new Map();
160
+ for (const line of out.split("\n")) {
161
+ const m = /^\s*(\d+)\s+(\d+)\s+(\S+)\s*$/.exec(line);
162
+ if (!m)
163
+ continue;
164
+ const p = Number(m[1]);
165
+ const pp = Number(m[2]);
166
+ etimeOf.set(p, m[3]);
167
+ const list = childrenOf.get(pp);
168
+ if (list)
169
+ list.push(p);
170
+ else
171
+ childrenOf.set(pp, [p]);
172
+ }
173
+ const seen = new Set();
174
+ const queue = [rootPid];
175
+ while (queue.length > 0) {
176
+ const cur = queue.shift();
177
+ for (const kid of childrenOf.get(cur) ?? []) {
178
+ if (kid <= 1 || kid === rootPid || seen.has(kid))
179
+ continue;
180
+ seen.add(kid);
181
+ queue.push(kid);
182
+ }
183
+ }
184
+ const descendants = new Map();
185
+ for (const p of seen) {
186
+ const et = etimeOf.get(p);
187
+ if (et !== undefined)
188
+ descendants.set(p, et);
189
+ }
190
+ return { descendants, etimeOf };
191
+ }
192
+ function verifyStillSameProcess(originalEtime, currentEtime) {
193
+ const orig = parseEtimeSeconds(originalEtime);
194
+ const cur = parseEtimeSeconds(currentEtime);
195
+ if (orig === undefined || cur === undefined)
196
+ return false;
197
+ return cur >= orig;
198
+ }
199
+ function parseEtimeSeconds(etime) {
200
+ const m = /^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/.exec(etime.trim());
201
+ if (!m)
202
+ return undefined;
203
+ const days = m[1] !== undefined ? Number(m[1]) : 0;
204
+ const hours = m[2] !== undefined ? Number(m[2]) : 0;
205
+ const minutes = Number(m[3]);
206
+ const seconds = Number(m[4]);
207
+ return days * 86400 + hours * 3600 + minutes * 60 + seconds;
98
208
  }
99
209
  function runTaskkill(args) {
100
210
  try {
@@ -331,7 +331,8 @@ export class NodeExecutionEnv {
331
331
  const onAbort = () => {
332
332
  abortKillInFlight = true;
333
333
  if (child?.pid) {
334
- killProcessTree(child.pid, { force: true });
334
+ const abortTarget = child;
335
+ killProcessTree(child.pid, { stillRunning: () => abortTarget.exitCode === null && abortTarget.signalCode === null });
335
336
  }
336
337
  if (callbackError) {
337
338
  const p = partialOutput();
@@ -440,7 +441,8 @@ export class NodeExecutionEnv {
440
441
  }
441
442
  timedOut = true;
442
443
  if (child?.pid) {
443
- killProcessTree(child.pid, { force: true });
444
+ const timeoutTarget = child;
445
+ killProcessTree(child.pid, { stillRunning: () => timeoutTarget.exitCode === null && timeoutTarget.signalCode === null });
444
446
  }
445
447
  armForceSettle(err(new ExecutionError("timeout", `timeout:${options?.timeout}`, undefined, partialOutput())));
446
448
  }, timeoutMs);
@@ -578,8 +580,10 @@ export class NodeExecutionEnv {
578
580
  if (onDetach("timeout"))
579
581
  return;
580
582
  timedOut = true;
581
- if (child?.pid)
582
- killProcessTree(child.pid, { force: true });
583
+ if (child?.pid) {
584
+ const adoptFallbackTarget = child;
585
+ killProcessTree(child.pid, { stillRunning: () => adoptFallbackTarget.exitCode === null && adoptFallbackTarget.signalCode === null });
586
+ }
583
587
  armForceSettle(err(new ExecutionError("timeout", `timeout:${options?.timeout}`, undefined, partialOutput())));
584
588
  };
585
589
  if (options?.detachSignal) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "1.432.0",
3
+ "version": "1.434.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",