@martinloop/mcp 0.8.4 → 0.8.5

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 +1 @@
1
- export declare const MARTIN_MCP_PACKAGE_VERSION = "0.8.4";
1
+ export declare const MARTIN_MCP_PACKAGE_VERSION = "0.8.5";
@@ -1,3 +1,3 @@
1
1
  // Auto-generated by build — do not edit by hand.
2
2
  // Source of truth: packages/mcp/package.json#version
3
- export const MARTIN_MCP_PACKAGE_VERSION = "0.8.4";
3
+ export const MARTIN_MCP_PACKAGE_VERSION = "0.8.5";
@@ -57,4 +57,11 @@ export declare function spawnSupervisedProcess(command: string, args: readonly s
57
57
  export declare function spawnSupervisedProcessForTest(command: string, args: readonly string[], options: SpawnSupervisedProcessOptions & {
58
58
  spawnImpl: ProcessSpawnLike;
59
59
  }, seams: ProcessSupervisorTestSeams): Promise<SupervisedProcessResult>;
60
+ /**
61
+ * Classify an OS process-table snapshot with PID, PGID and STAT columns.
62
+ * POSIX Z means terminated but not yet reaped; other states remain live.
63
+ * Malformed or empty output must never assert cleanup success.
64
+ * @internal - deterministic pure classification for process-supervisor tests
65
+ */
66
+ export declare function classifyPosixProcessGroupSnapshot(output: string, ownedGroupPid: number): "closed" | "alive" | "absent" | "invalid";
60
67
  export {};
@@ -327,14 +327,21 @@ async function terminatePosixGroupAfterRootExit(pid, killGroup, graceMs, waitImp
327
327
  const initial = probePosixProcessGroup(target, killGroup);
328
328
  if (initial === "closed")
329
329
  return { state: "closed" };
330
- if (initial instanceof Error)
330
+ if (initial instanceof Error) {
331
+ if (isErrno(initial, "EPERM") && await confirmInactivePosixGroup(pid, killGroup)) {
332
+ return { state: "closed" };
333
+ }
331
334
  return cleanupFailure("termination_failed", initial);
335
+ }
332
336
  try {
333
337
  killGroup(target, "SIGTERM");
334
338
  }
335
339
  catch (error) {
336
340
  if (isErrno(error, "ESRCH"))
337
341
  return { state: "closed" };
342
+ if (isErrno(error, "EPERM") && await confirmInactivePosixGroup(pid, killGroup)) {
343
+ return { state: "closed" };
344
+ }
338
345
  return cleanupFailure("termination_failed", error);
339
346
  }
340
347
  if (await waitForPosixGroupExit(target, killGroup, graceMs, waitImpl))
@@ -345,14 +352,30 @@ async function terminatePosixGroupAfterRootExit(pid, killGroup, graceMs, waitImp
345
352
  catch (error) {
346
353
  if (isErrno(error, "ESRCH"))
347
354
  return { state: "closed" };
355
+ if (isErrno(error, "EPERM") && await confirmInactivePosixGroup(pid, killGroup)) {
356
+ return { state: "closed" };
357
+ }
348
358
  return cleanupFailure("termination_failed", error);
349
359
  }
350
360
  if (await waitForPosixGroupExit(target, killGroup, graceMs, waitImpl))
351
361
  return { state: "closed" };
362
+ // A reparented, already-terminated descendant can remain as a zombie in the
363
+ // process table. On POSIX, kill(-pgid, 0) still sees that process group even
364
+ // though its remaining members cannot execute code. Prove the absence of
365
+ // *runnable* group members rather than treating an unreapable zombie as live.
366
+ // Do not turn an unreadable/ambiguous process snapshot into success.
367
+ const snapshot = await inspectPosixProcessGroupMembers(pid);
368
+ if (snapshot === "closed" || (snapshot === "absent" && probePosixProcessGroup(target, killGroup) === "closed")) {
369
+ return { state: "closed" };
370
+ }
371
+ if (snapshot instanceof Error)
372
+ return cleanupFailure("termination_failed", snapshot);
352
373
  return {
353
374
  state: "failed",
354
375
  code: "tree_close_timeout",
355
- message: "Owned POSIX process group remained live after SIGTERM and SIGKILL."
376
+ message: snapshot === "invalid"
377
+ ? "Could not prove owned POSIX group closure from the process table."
378
+ : "Owned POSIX process group still has live members after SIGTERM and SIGKILL."
356
379
  };
357
380
  }
358
381
  async function waitForPosixGroupExit(target, killGroup, durationMs, waitImpl) {
@@ -367,6 +390,14 @@ async function waitForPosixGroupExit(target, killGroup, durationMs, waitImpl) {
367
390
  }
368
391
  return probePosixProcessGroup(target, killGroup) === "closed";
369
392
  }
393
+ async function confirmInactivePosixGroup(ownedGroupPid, killGroup) {
394
+ const snapshot = await inspectPosixProcessGroupMembers(ownedGroupPid);
395
+ if (snapshot === "closed")
396
+ return true; // Observed owned members are all defunct zombies.
397
+ // An absent group is not proof on its own after EPERM. Require an ESRCH re-probe.
398
+ return snapshot === "absent"
399
+ && probePosixProcessGroup(-ownedGroupPid, killGroup) === "closed";
400
+ }
370
401
  function probePosixProcessGroup(target, killGroup) {
371
402
  try {
372
403
  killGroup(target, 0);
@@ -378,6 +409,49 @@ function probePosixProcessGroup(target, killGroup) {
378
409
  return error instanceof Error ? error : new Error(String(error));
379
410
  }
380
411
  }
412
+ /**
413
+ * Classify an OS process-table snapshot with PID, PGID and STAT columns.
414
+ * POSIX Z means terminated but not yet reaped; other states remain live.
415
+ * Malformed or empty output must never assert cleanup success.
416
+ * @internal - deterministic pure classification for process-supervisor tests
417
+ */
418
+ export function classifyPosixProcessGroupSnapshot(output, ownedGroupPid) {
419
+ if (!isOwnedPid(ownedGroupPid))
420
+ return "invalid";
421
+ let observed = 0;
422
+ let matchingGroupMembers = 0;
423
+ for (const line of output.split(/\r?\n/u)) {
424
+ if (!line.trim())
425
+ continue;
426
+ const match = /^\s*(\d+)\s+(\d+)\s+(\S+)\s*$/u.exec(line);
427
+ if (!match)
428
+ return "invalid";
429
+ observed += 1;
430
+ if (Number(match[2]) === ownedGroupPid) {
431
+ matchingGroupMembers += 1;
432
+ if (!match[3].startsWith("Z"))
433
+ return "alive";
434
+ }
435
+ }
436
+ if (observed === 0)
437
+ return "invalid";
438
+ return matchingGroupMembers > 0 ? "closed" : "absent";
439
+ }
440
+ async function inspectPosixProcessGroupMembers(ownedGroupPid) {
441
+ return new Promise((resolveResult) => {
442
+ execFile("ps", ["-axo", "pid=,pgid=,stat="], {
443
+ encoding: "utf8",
444
+ timeout: 2_000,
445
+ maxBuffer: 8 * 1024 * 1024
446
+ }, (error, stdout) => {
447
+ if (error) {
448
+ resolveResult(error);
449
+ return;
450
+ }
451
+ resolveResult(classifyPosixProcessGroupSnapshot(stdout, ownedGroupPid));
452
+ });
453
+ });
454
+ }
381
455
  function isErrno(error, code) {
382
456
  return typeof error === "object" && error !== null && "code" in error
383
457
  && String(error.code) === code;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@martinloop/mcp",
3
- "version": "0.8.4",
3
+ "version": "0.8.5",
4
4
  "mcpName": "io.github.Keesan12/martin-loop",
5
5
  "private": false,
6
6
  "type": "module",
package/server.json CHANGED
@@ -7,12 +7,12 @@
7
7
  "url": "https://github.com/Keesan12/martin-loop",
8
8
  "source": "github"
9
9
  },
10
- "version": "0.8.4",
10
+ "version": "0.8.5",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "@martinloop/mcp",
15
- "version": "0.8.4",
15
+ "version": "0.8.5",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  }