Intelligence

Artifacts

Browse the repository, read documents, and manage the governance folders. Source, runtime, and infrastructure are read-only.

Repository
README.md
CONSTITUTION_COMPLIANCE_AUDIT_V1.mdREADME.md
repositories/aaf-holdings/hq01/lib/process/signal.ts
2.6 KB
/**
 * Process signalling primitives for the HQ01 Session Manager.
 *
 * These helpers wrap the POSIX semantics of `process.kill` so the rest of the
 * code can reason about a child Claude CLI process in plain terms: is it alive,
 * and please stop it. They are intentionally tiny and have no knowledge of
 * sessions, the filesystem, or HTTP — they only know about PIDs and signals.
 */

/**
 * Returns true if a process with the given PID is currently running.
 *
 * `process.kill(pid, 0)` sends no signal but performs the same permission and
 * existence checks the kernel would for a real signal:
 *   - success      → the process exists and we may signal it.
 *   - EPERM        → the process exists but is owned by another user.
 *   - ESRCH/ENOENT → no such process.
 */
export function isAlive(pid: number | null | undefined): boolean {
  if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) {
    return false;
  }
  try {
    process.kill(pid, 0);
    return true;
  } catch (err) {
    const code = (err as NodeJS.ErrnoException)?.code;
    // EPERM means the process is alive but not ours to signal.
    return code === "EPERM";
  }
}

/** The outcome of a {@link terminate} call. */
export type TerminateResult = "gone" | "stopped" | "killed";

/**
 * Gracefully terminate a process.
 *
 * Sends SIGTERM and waits up to `graceMs` for the process to exit on its own.
 * If it is still alive after the grace period, escalates to SIGKILL. Polling is
 * used rather than waiting on the child handle because the session manager runs
 * in a different process than the one that spawned the child (the child is
 * detached and survives request boundaries).
 *
 *   - "gone"    → the process was already dead.
 *   - "stopped" → exited cleanly after SIGTERM.
 *   - "killed"  → required SIGKILL.
 */
export async function terminate(
  pid: number | null | undefined,
  { graceMs = 5000, pollMs = 200 }: { graceMs?: number; pollMs?: number } = {},
): Promise<TerminateResult> {
  if (!isAlive(pid)) return "gone";
  const target = pid as number;

  try {
    process.kill(target, "SIGTERM");
  } catch {
    // Raced with the process exiting — treat as gone.
    if (!isAlive(target)) return "gone";
  }

  const deadline = Date.now() + graceMs;
  while (Date.now() < deadline) {
    if (!isAlive(target)) return "stopped";
    await sleep(pollMs);
  }

  if (!isAlive(target)) return "stopped";

  // Still alive after the grace period — force it.
  try {
    process.kill(target, "SIGKILL");
  } catch {
    /* already gone */
  }
  return "killed";
}

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

root · /srv/aaf