akm-cli
Version:
akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cur
65 lines (64 loc) • 3.21 kB
JavaScript
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
/**
* Parent-death watchdog (#956).
*
* The published launcher (`scripts/node-runtime/akm`) now forwards SIGTERM to
* its child, so a `kill <launcher-pid>` no longer orphans it — but a launcher
* that dies WITHOUT delivering a signal (SIGKILL, an out-of-memory kill, a
* supervisor that force-removes the process) still reparents the child to
* init with nothing to catch. Field evidence: 40 orphaned
* `bun …/dist/cli.js` processes over one day, up to 10h old, some created by
* a curate hook killing its own launcher on timeout — every akm invocation
* under the launcher is exposed to this, not only `akm index`, so this
* watchdog runs for every command (wired at the CLI entry point in
* `src/cli.ts`), not just index.
*
* `process.ppid` on POSIX changes the instant the parent exits (the child is
* reparented, usually to pid 1) — polling it is the standard orphan-detection
* trick when no direct death notification exists. This module owns only the
* poll/compare mechanics; the caller's `onOrphaned` decides what "stop"
* means. `src/cli.ts` wires it to `process.kill(process.pid, "SIGTERM")`,
* reusing the same self-signal every command already has rather than a
* second, parallel abort path. Only `akm index`/`improve` register their own
* SIGTERM listener (the index command's AbortController in
* `commands/sources/stash-cli.ts`) and get a graceful, in-process shutdown
* from it. Every other command has no listener of its own, so the runtime's
* default disposition terminates it directly — `exit` handlers (lock
* release included) do NOT run — and any lock it held is cleared later by
* the next acquirer's dead-pid stale-reclaim (`file-lock.ts`), not by
* in-process release. Either way the orphaned process stops, which is this
* watchdog's actual job.
*/
/** Pure comparison seam: true once the observed ppid differs from the one seen at startup. */
export function isReparented(initialPpid, currentPpid) {
return currentPpid !== initialPpid;
}
/**
* Start polling `getPpid()` every `intervalMs` and invoke `onOrphaned` once,
* the first time the observed ppid no longer matches `initialPpid`. The timer
* is unref'ed so it never keeps the process alive on its own — a command that
* finishes normally still exits promptly.
*/
export function startParentDeathWatchdog(options) {
const { initialPpid, onOrphaned } = options;
const intervalMs = options.intervalMs ?? 2000;
const getPpid = options.getPpid ?? (() => process.ppid);
const setIntervalFn = options.setIntervalFn ?? setInterval;
const clearIntervalFn = options.clearIntervalFn ?? clearInterval;
let fired = false;
const timer = setIntervalFn(() => {
if (fired)
return;
if (isReparented(initialPpid, getPpid())) {
fired = true;
onOrphaned();
}
}, intervalMs);
if (typeof timer !== "number")
timer.unref?.();
return {
stop: () => clearIntervalFn(timer),
};
}