@code-yeongyu/senpi-agent-core 2026.10.10-10 → 2026.10.10-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.
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Context } from "../context.ts";
|
|
2
2
|
import { type ExecutionEnv, ExecutionError, FileError, type FileInfo, type Result, type ShellExecOptions, type ShellExecResult, type TextLineReader } from "../types.ts";
|
|
3
|
+
import { type WindowsProcessRow } from "./windows-process-tree.ts";
|
|
3
4
|
/**
|
|
4
5
|
* Ordered `taskkill` launchers to try, most reliable first.
|
|
5
6
|
*
|
|
@@ -11,7 +12,10 @@ import { type ExecutionEnv, ExecutionError, FileError, type FileInfo, type Resul
|
|
|
11
12
|
*/
|
|
12
13
|
export declare function windowsTaskkillCandidates(env?: NodeJS.ProcessEnv): string[];
|
|
13
14
|
/**
|
|
14
|
-
* Kill a process and
|
|
15
|
+
* Kill a process and its descendants on Windows: one process listing (bounded by `TASKKILL_TIMEOUT_MS`)
|
|
16
|
+
* decides the tree, a process counting as a child only when it started at or after the parent it names,
|
|
17
|
+
* and `taskkill /F` ends each pid by name. `/T` would also adopt an unrelated older process through a
|
|
18
|
+
* recycled parent pid (senpi#2999); it is used only when no listing can be read.
|
|
15
19
|
*
|
|
16
20
|
* Synchronous on purpose. A caller that tears down and exits in the same tick would never
|
|
17
21
|
* observe an asynchronous killer's `error` event, leaving the target alive. `spawnSync`
|
|
@@ -24,7 +28,7 @@ export declare function windowsTaskkillCandidates(env?: NodeJS.ProcessEnv): stri
|
|
|
24
28
|
* touch descendants; nothing in-process can walk a Windows process tree without an
|
|
25
29
|
* external tool, so this still beats leaving the whole tree running.
|
|
26
30
|
*/
|
|
27
|
-
export declare function killWindowsProcessTree(pid: number, taskkillPaths?: string[]): void;
|
|
31
|
+
export declare function killWindowsProcessTree(pid: number, taskkillPaths?: string[], listProcesses?: () => readonly WindowsProcessRow[] | undefined): void;
|
|
28
32
|
export declare class NodeExecutionEnv implements ExecutionEnv {
|
|
29
33
|
cwd: string;
|
|
30
34
|
private shellPath?;
|
|
@@ -7,6 +7,7 @@ import { basename, isAbsolute, join, resolve } from "node:path";
|
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { ExecutionError, err, FileError, ok, toError, } from "../types.js";
|
|
9
9
|
import { OutputCapture } from "../utils/output-capture.js";
|
|
10
|
+
import { listWindowsProcessRowsSync, windowsTreeKillArgs } from "./windows-process-tree.js";
|
|
10
11
|
const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
11
12
|
const MAX_TIMEOUT_SECONDS = MAX_TIMEOUT_MS / 1000;
|
|
12
13
|
const EXIT_STDIO_GRACE_MS = 100;
|
|
@@ -237,9 +238,14 @@ function killProcessDirectly(pid) {
|
|
|
237
238
|
}
|
|
238
239
|
/** Upper bound on how long a teardown may block waiting for `taskkill` to finish. */
|
|
239
240
|
const TASKKILL_TIMEOUT_MS = 5_000;
|
|
240
|
-
function
|
|
241
|
+
function listWindowsProcesses() {
|
|
242
|
+
return listWindowsProcessRowsSync(TASKKILL_TIMEOUT_MS);
|
|
243
|
+
}
|
|
244
|
+
function taskkillHandledTree(taskkillPath, killArgs) {
|
|
245
|
+
if (killArgs.length === 0)
|
|
246
|
+
return true;
|
|
241
247
|
try {
|
|
242
|
-
const result = spawnSync(taskkillPath, [
|
|
248
|
+
const result = spawnSync(taskkillPath, [...killArgs], {
|
|
243
249
|
stdio: "ignore",
|
|
244
250
|
windowsHide: true,
|
|
245
251
|
timeout: TASKKILL_TIMEOUT_MS,
|
|
@@ -253,7 +259,10 @@ function taskkillHandledTree(pid, taskkillPath) {
|
|
|
253
259
|
}
|
|
254
260
|
}
|
|
255
261
|
/**
|
|
256
|
-
* Kill a process and
|
|
262
|
+
* Kill a process and its descendants on Windows: one process listing (bounded by `TASKKILL_TIMEOUT_MS`)
|
|
263
|
+
* decides the tree, a process counting as a child only when it started at or after the parent it names,
|
|
264
|
+
* and `taskkill /F` ends each pid by name. `/T` would also adopt an unrelated older process through a
|
|
265
|
+
* recycled parent pid (senpi#2999); it is used only when no listing can be read.
|
|
257
266
|
*
|
|
258
267
|
* Synchronous on purpose. A caller that tears down and exits in the same tick would never
|
|
259
268
|
* observe an asynchronous killer's `error` event, leaving the target alive. `spawnSync`
|
|
@@ -266,9 +275,10 @@ function taskkillHandledTree(pid, taskkillPath) {
|
|
|
266
275
|
* touch descendants; nothing in-process can walk a Windows process tree without an
|
|
267
276
|
* external tool, so this still beats leaving the whole tree running.
|
|
268
277
|
*/
|
|
269
|
-
export function killWindowsProcessTree(pid, taskkillPaths = windowsTaskkillCandidates()) {
|
|
278
|
+
export function killWindowsProcessTree(pid, taskkillPaths = windowsTaskkillCandidates(), listProcesses = listWindowsProcesses) {
|
|
279
|
+
const killArgs = windowsTreeKillArgs(pid, listProcesses());
|
|
270
280
|
for (const taskkillPath of taskkillPaths) {
|
|
271
|
-
if (taskkillHandledTree(
|
|
281
|
+
if (taskkillHandledTree(taskkillPath, killArgs))
|
|
272
282
|
return;
|
|
273
283
|
}
|
|
274
284
|
killProcessDirectly(pid);
|
|
@@ -959,8 +969,16 @@ export class NodeExecutionEnv {
|
|
|
959
969
|
}
|
|
960
970
|
}
|
|
961
971
|
async cleanup(_context) {
|
|
962
|
-
|
|
963
|
-
|
|
972
|
+
if (process.platform === "win32" && this.activeChildPids.size > 0) {
|
|
973
|
+
// One listing for the whole batch: each tree kill would otherwise list every process again.
|
|
974
|
+
const rows = listWindowsProcesses();
|
|
975
|
+
for (const pid of this.activeChildPids)
|
|
976
|
+
killWindowsProcessTree(pid, undefined, () => rows);
|
|
977
|
+
}
|
|
978
|
+
else {
|
|
979
|
+
for (const pid of this.activeChildPids)
|
|
980
|
+
killProcessTree(pid);
|
|
981
|
+
}
|
|
964
982
|
this.activeChildPids.clear();
|
|
965
983
|
}
|
|
966
984
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface WindowsProcessRow {
|
|
2
|
+
readonly pid: number;
|
|
3
|
+
readonly parentPid: number;
|
|
4
|
+
readonly createdAt: bigint;
|
|
5
|
+
readonly name: string;
|
|
6
|
+
}
|
|
7
|
+
/** One `pid parentPid createdFileTime name` line per process; the listing's own PowerShell is left out. */
|
|
8
|
+
export declare const WINDOWS_PROCESS_TREE_ROWS = "Get-CimInstance Win32_Process | Where-Object { $_.ProcessId -ne $PID } | ForEach-Object {\n$created = if ($_.CreationDate) { $_.CreationDate.ToFileTimeUtc() } else { 0 }\n\"{0} {1} {2} {3}\" -f $_.ProcessId, $_.ParentProcessId, $created, $_.Name }";
|
|
9
|
+
export declare function parseWindowsProcessTreeRows(stdout: string): WindowsProcessRow[];
|
|
10
|
+
export interface WindowsTreeKillPlan {
|
|
11
|
+
/** Pids to end with one `taskkill /F /PID a /PID b ...` (never /T). */
|
|
12
|
+
readonly kill: readonly number[];
|
|
13
|
+
/** Processes the walk adopted that must never be killed; any entry here means only the root is killed. */
|
|
14
|
+
readonly refused: readonly {
|
|
15
|
+
readonly pid: number;
|
|
16
|
+
readonly name: string;
|
|
17
|
+
}[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The root and every process that genuinely descends from it. A protected OS image or an ancestor of
|
|
21
|
+
* `selfPid` in that set means the walk went wrong, so only the root itself is killed (and nothing when
|
|
22
|
+
* the root is one of them).
|
|
23
|
+
*/
|
|
24
|
+
export declare function windowsTreeKillPlan(rows: readonly WindowsProcessRow[], rootPid: number, selfPid: number): WindowsTreeKillPlan;
|
|
25
|
+
/** Bounded synchronous CIM listing; undefined when it cannot run or does not finish in time. */
|
|
26
|
+
export declare function listWindowsProcessRowsSync(timeoutMs: number): WindowsProcessRow[] | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* `taskkill` arguments for one tree kill. With a process listing, the checked tree is named pid by pid
|
|
29
|
+
* (never `/T`). Without one, `/T` is kept: leaving every descendant running is the worse failure, and the
|
|
30
|
+
* recycled-pid hazard needs a pid wraparound that a missing listing does not make more likely.
|
|
31
|
+
*/
|
|
32
|
+
export declare function windowsTreeKillArgs(rootPid: number, rows: readonly WindowsProcessRow[] | undefined, selfPid?: number): string[];
|
|
33
|
+
//# sourceMappingURL=windows-process-tree.d.ts.map
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
/** One `pid parentPid createdFileTime name` line per process; the listing's own PowerShell is left out. */
|
|
3
|
+
export const WINDOWS_PROCESS_TREE_ROWS = `Get-CimInstance Win32_Process | Where-Object { $_.ProcessId -ne $PID } | ForEach-Object {
|
|
4
|
+
$created = if ($_.CreationDate) { $_.CreationDate.ToFileTimeUtc() } else { 0 }
|
|
5
|
+
"{0} {1} {2} {3}" -f $_.ProcessId, $_.ParentProcessId, $created, $_.Name }`;
|
|
6
|
+
export function parseWindowsProcessTreeRows(stdout) {
|
|
7
|
+
const rows = [];
|
|
8
|
+
for (const line of stdout.split(/\r?\n/u)) {
|
|
9
|
+
const [pidText, parentText, createdText, ...nameParts] = line.trim().split(/\s+/u);
|
|
10
|
+
const pid = Number(pidText);
|
|
11
|
+
const parentPid = Number(parentText);
|
|
12
|
+
if (!Number.isInteger(pid) || pid <= 0 || !Number.isInteger(parentPid) || !/^\d+$/u.test(createdText ?? ""))
|
|
13
|
+
continue;
|
|
14
|
+
rows.push({ pid, parentPid, createdAt: BigInt(createdText ?? "0"), name: nameParts.join(" ") });
|
|
15
|
+
}
|
|
16
|
+
return rows;
|
|
17
|
+
}
|
|
18
|
+
const PROTECTED_IMAGES = new Set([
|
|
19
|
+
"system",
|
|
20
|
+
"smss.exe",
|
|
21
|
+
"csrss.exe",
|
|
22
|
+
"wininit.exe",
|
|
23
|
+
"winlogon.exe",
|
|
24
|
+
"services.exe",
|
|
25
|
+
"lsass.exe",
|
|
26
|
+
"lsaiso.exe",
|
|
27
|
+
"svchost.exe",
|
|
28
|
+
]);
|
|
29
|
+
/**
|
|
30
|
+
* The root and every process that genuinely descends from it. A protected OS image or an ancestor of
|
|
31
|
+
* `selfPid` in that set means the walk went wrong, so only the root itself is killed (and nothing when
|
|
32
|
+
* the root is one of them).
|
|
33
|
+
*/
|
|
34
|
+
export function windowsTreeKillPlan(rows, rootPid, selfPid) {
|
|
35
|
+
const byPid = new Map(rows.map((row) => [row.pid, row]));
|
|
36
|
+
const root = byPid.get(rootPid);
|
|
37
|
+
if (root === undefined)
|
|
38
|
+
return { kill: [], refused: [] };
|
|
39
|
+
const tree = new Set([rootPid]);
|
|
40
|
+
let grew = true;
|
|
41
|
+
while (grew) {
|
|
42
|
+
grew = false;
|
|
43
|
+
for (const row of rows) {
|
|
44
|
+
if (tree.has(row.pid) || !tree.has(row.parentPid))
|
|
45
|
+
continue;
|
|
46
|
+
const parent = byPid.get(row.parentPid);
|
|
47
|
+
if (parent === undefined || row.createdAt < parent.createdAt)
|
|
48
|
+
continue;
|
|
49
|
+
tree.add(row.pid);
|
|
50
|
+
grew = true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const ancestors = new Set([selfPid]);
|
|
54
|
+
for (let current = byPid.get(selfPid); current !== undefined;) {
|
|
55
|
+
const parent = byPid.get(current.parentPid);
|
|
56
|
+
if (parent === undefined || ancestors.has(parent.pid) || parent.createdAt > current.createdAt)
|
|
57
|
+
break;
|
|
58
|
+
ancestors.add(parent.pid);
|
|
59
|
+
current = parent;
|
|
60
|
+
}
|
|
61
|
+
const refused = [...tree]
|
|
62
|
+
.map((pid) => ({ pid, name: byPid.get(pid)?.name ?? "" }))
|
|
63
|
+
.filter((entry) => PROTECTED_IMAGES.has(entry.name.toLowerCase()) || ancestors.has(entry.pid));
|
|
64
|
+
if (refused.length === 0)
|
|
65
|
+
return { kill: [...tree], refused };
|
|
66
|
+
const rootRefused = refused.some((entry) => entry.pid === rootPid);
|
|
67
|
+
return { kill: rootRefused ? [] : [rootPid], refused };
|
|
68
|
+
}
|
|
69
|
+
/** Bounded synchronous CIM listing; undefined when it cannot run or does not finish in time. */
|
|
70
|
+
export function listWindowsProcessRowsSync(timeoutMs) {
|
|
71
|
+
const result = spawnSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", WINDOWS_PROCESS_TREE_ROWS], {
|
|
72
|
+
encoding: "utf8",
|
|
73
|
+
windowsHide: true,
|
|
74
|
+
timeout: timeoutMs,
|
|
75
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
76
|
+
});
|
|
77
|
+
if (result.error !== undefined || result.status !== 0 || typeof result.stdout !== "string")
|
|
78
|
+
return undefined;
|
|
79
|
+
const rows = parseWindowsProcessTreeRows(result.stdout);
|
|
80
|
+
return rows.length > 0 ? rows : undefined;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* `taskkill` arguments for one tree kill. With a process listing, the checked tree is named pid by pid
|
|
84
|
+
* (never `/T`). Without one, `/T` is kept: leaving every descendant running is the worse failure, and the
|
|
85
|
+
* recycled-pid hazard needs a pid wraparound that a missing listing does not make more likely.
|
|
86
|
+
*/
|
|
87
|
+
export function windowsTreeKillArgs(rootPid, rows, selfPid = process.pid) {
|
|
88
|
+
if (rows === undefined)
|
|
89
|
+
return ["/F", "/T", "/PID", String(rootPid)];
|
|
90
|
+
const pids = windowsTreeKillPlan(rows, rootPid, selfPid).kill;
|
|
91
|
+
if (pids.length === 0)
|
|
92
|
+
return [];
|
|
93
|
+
return ["/F", ...pids.flatMap((pid) => ["/PID", String(pid)])];
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=windows-process-tree.js.map
|
package/dist/node.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { NodeExecutionEnv } from "./harness/env/nodejs.ts";
|
|
2
|
+
export { listWindowsProcessRowsSync, parseWindowsProcessTreeRows, type WindowsProcessRow, type WindowsTreeKillPlan, windowsTreeKillArgs, windowsTreeKillPlan, } from "./harness/env/windows-process-tree.ts";
|
|
2
3
|
export * from "./index.ts";
|
|
3
4
|
//# sourceMappingURL=node.d.ts.map
|
package/dist/node.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { NodeExecutionEnv } from "./harness/env/nodejs.js";
|
|
2
|
+
export { listWindowsProcessRowsSync, parseWindowsProcessTreeRows, windowsTreeKillArgs, windowsTreeKillPlan, } from "./harness/env/windows-process-tree.js";
|
|
2
3
|
export * from "./index.js";
|
|
3
4
|
//# sourceMappingURL=node.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/senpi-agent-core",
|
|
3
|
-
"version": "2026.10.10-
|
|
3
|
+
"version": "2026.10.10-12",
|
|
4
4
|
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -62,8 +62,8 @@
|
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
64
|
"@earendil-works/chord": "0.99.1",
|
|
65
|
-
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.10-
|
|
66
|
-
"@earendil-works/pi-telemetry": "npm:@code-yeongyu/senpi-telemetry@2026.10.10-
|
|
65
|
+
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.10-12",
|
|
66
|
+
"@earendil-works/pi-telemetry": "npm:@code-yeongyu/senpi-telemetry@2026.10.10-12",
|
|
67
67
|
"diff": "9.0.0",
|
|
68
68
|
"ignore": "7.0.9",
|
|
69
69
|
"typebox": "1.3.34",
|