SPB Git

spb/khaelor Public

KHAELOR — a terminal-native autonomous engineering agent powered by Anthropic.

TypeScript 82.9% HTML 14.9% CSS 1.1% JavaScript 0.7%
3.0 KB · 88 lines typescript
Raw Blame History
1/**2 * KHAELOR3 * File: src/cli/lifecycle.ts4 * Description: Startup/shutdown discipline — crash logging, signal handling, quit-with-running-processes confirmation.5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import { defaultLogDir, writeCrashFile } from "./logger.js";11import type { FileLogger } from "./logger.js";1213/**14 * Install crash handlers that persist the error to ~/.khaelor/logs/ before15 * the terminal guard (src/tui/renderer/terminal.ts) restores the shell and16 * exits. Registered FIRST so the crash file exists by the time the guard's17 * own uncaughtException handler runs. Never writes to the live screen.18 */19export function installCrashHandlers(logger: FileLogger): void {20  const record = (origin: string) => {21    return (error: unknown): void => {22      const file = writeCrashFile(defaultLogDir(), error);23      logger.error(`${origin}`, {24        error: error instanceof Error ? error.message : String(error),25        ...(file !== null ? { crashFile: file } : {}),26      });27    };28  };29  process.on("uncaughtException", record("uncaughtException"));30  process.on("unhandledRejection", record("unhandledRejection"));31}3233/**34 * Quit confirmation policy: quitting with running background processes35 * requires a second quit within the window (they are killed on exit —36 * ProcessExited{cause:"khaelor-shutdown"} is recorded for each).37 */38export class QuitGuard {39  readonly #windowMs: number;40  readonly #now: () => number;41  #armedAt = 0;4243  constructor(options: { windowMs?: number; now?: () => number } = {}) {44    this.#windowMs = options.windowMs ?? 5_000;45    this.#now = options.now ?? Date.now;46  }4748  /**49   * Returns "quit" when quitting may proceed, or "confirm" when the caller50   * must warn the user (running processes) and wait for a second quit.51   */52  request(runningProcesses: number): "quit" | "confirm" {53    if (runningProcesses === 0) return "quit";54    const now = this.#now();55    if (now - this.#armedAt <= this.#windowMs) return "quit";56    this.#armedAt = now;57    return "confirm";58  }59}6061/** Message shown when quit needs confirmation. */62export function quitConfirmationLines(runningProcesses: number): string[] {63  const plural = runningProcesses === 1 ? "process is" : "processes are";64  return [65    "",66    ` ${runningProcesses} background ${plural} still running`,67    "   quitting stops them (recorded as khaelor-shutdown) — quit again to confirm",68  ];69}7071/**72 * Wire SIGINT/SIGTERM to an async, exactly-once shutdown path. The terminal73 * guard has its own SIGTERM restore; this handler runs first (registration74 * order) and owns the orderly engine shutdown.75 */76export function installSignalShutdown(shutdown: (signal: string) => Promise<void>): void {77  let invoked = false;78  const handler = (signal: string): void => {79    if (invoked) return;80    invoked = true;81    void shutdown(signal).finally(() => {82      process.exit(signal === "SIGINT" ? 130 : 143);83    });84  };85  process.on("SIGINT", () => handler("SIGINT"));86  process.on("SIGTERM", () => handler("SIGTERM"));87}88