/** * Daemon crash forensics — answers "HOW did the daemon die?" after the fact. * * The daemon keeps an alive-marker on disk (pid + lastAliveAt, refreshed on * every heartbeat tick) and records a clean-exit stamp when it shuts down * normally. Whoever runs next — a restarted daemon or the watchdog task — * compares the marker against reality: a marker without a clean exit whose * pid is gone means the previous daemon was killed externally (EDR/AV kill, * OOM, forced reboot). That produces a post-mortem record (with the last * daemon.log lines) which is reported upstream via the heartbeat/beacon, so * a remote fleet machine's death is diagnosable without hands-on access. * * Everything here is best-effort file I/O: forensics must never break the * daemon or the watchdog. Pure logic (`detectUncleanDeath`) is injectable * for offline tests. */ export interface DaemonAliveMarker { pid: number; version?: string; startedAt: string; lastAliveAt: string; /** Present only when the daemon shut down on purpose (SIGINT/SIGTERM/supersede). */ cleanExit?: { at: string; reason: string; }; } export interface DaemonPostmortem { /** Version of the daemon that died. */ version?: string; startedAt?: string; /** Last proof of life — death happened within one heartbeat tick after this. */ lastAliveAt?: string; detectedAt: string; detectedBy: 'daemon' | 'watchdog'; uncleanExit: true; /** Tail of daemon.log at detection time — the daemon's last words. */ lastLogLines: string[]; /** Set once the post-mortem reached the backend (stop re-sending). */ reportedAt?: string; } export declare function aliveMarkerFile(): string; export declare function postmortemFile(): string; export declare function readAliveMarker(): DaemonAliveMarker | undefined; export declare function readPostmortem(): DaemonPostmortem | undefined; /** Last non-empty lines of daemon.log (the dead daemon's final output). */ export declare function tailDaemonLog(maxLines?: number): string[]; /** * Pure detection: did the previously recorded daemon die WITHOUT a clean exit? * Returns the post-mortem (not yet persisted) or undefined when the previous * daemon exited cleanly, is still alive, or nothing was recorded. */ export declare function detectUncleanDeath(input: { marker: DaemonAliveMarker | undefined; isPidAlive: (pid: number) => boolean; detectedBy: 'daemon' | 'watchdog'; lastLogLines?: string[]; ownPid?: number; now?: string; }): DaemonPostmortem | undefined; /** * Called once at daemon startup: detect an unclean previous death (persisting * the post-mortem for reporting), then claim the alive marker for this pid. * Returns the detected post-mortem so the daemon can log it. */ export declare function recordDaemonStart(input: { version?: string; isPidAlive: (pid: number) => boolean; }): DaemonPostmortem | undefined; /** Watchdog-side detection (daemon not running): persist + return the post-mortem. */ export declare function recordUncleanDeathFromWatchdog(isPidAlive: (pid: number) => boolean): DaemonPostmortem | undefined; /** Refresh proof-of-life; called from the daemon's heartbeat tick. */ export declare function touchDaemonAlive(): void; /** Record an intentional shutdown so the next boot knows this was NOT a kill. */ export declare function recordCleanExit(reason: string): void; /** * A pid can lie in two ways: the daemon process exists but its event loop is * WEDGED (never heartbeats, never serves the pipe, never polls the control * plane), or the daemon died long ago and the OS RECYCLED its pid for an * unrelated process. Both look "alive" to a pid check; both mean the machine * has no working control plane. The alive-marker freshness plus a verdict-pipe * probe tells them apart: * * marker fresh → healthy (heartbeats are running) * marker stale + pipe answers → healthy (only the marker write fails) * marker stale + pipe exists, silent → hung (kernel frees a named pipe when * its owner dies, so a silent endpoint * means a live-but-wedged owner) * marker stale + no pipe endpoint → pid recycled (daemon is gone; do NOT * kill the unrelated process) */ export type DaemonHealthClass = 'dead' | 'healthy' | 'hung' | 'pid_reused' | 'indeterminate'; /** Marker older than this is stale — 4 missed 5-minute heartbeats. */ export declare function daemonHungAfterMs(): number; export declare function classifyDaemonHealth(input: { pidAlive: boolean; pid: number | undefined; marker: DaemonAliveMarker | undefined; staleAfterMs: number; pipeProbe: 'responsive' | 'unresponsive' | 'no-endpoint' | 'not-probed'; nowMs?: number; }): DaemonHealthClass; /** Stamp the post-mortem as delivered so heartbeats stop re-sending it. */ export declare function markPostmortemReported(): void;