/** * supervisor.ts, the restart policy for a wake-word detector process. * * The detector holds a microphone open for as long as the user has it on, so * the failure that matters is not one crash, it is a crash LOOP. A process * that dies on startup and is restarted immediately becomes a storm that pins a * core, spams the log, and repeatedly grabs and releases the capture device. * * The policy is the one already used for MCP clients, expressed as pure state * so it can be tested without spawning anything: * * - restart after `restartBackoffMs * attempt` (linear, so 2 s, 4 s, 6 s), * - allow at most `maxRestarts` restarts within a rolling * `crashWindowSeconds` window, * - exceeding that LATCHES the supervisor off with a stated reason, rather * than continuing to try, so the user sees a detector that stopped and why * instead of one that thrashes silently, * - a crash older than the window is forgotten, so a process that runs for an * hour and then dies gets its full restart budget again. * * Time is injected. Nothing here reads a clock, spawns a process, or sleeps. */ /** Supervisor tuning, mirroring the `voice.wake.*` rows that drive it. */ export interface WakeSupervisorPolicy { /** Restarts allowed inside the crash window. 0 means any crash is terminal. */ readonly maxRestarts: number; /** Base delay before a restart, multiplied by the attempt number. */ readonly restartBackoffMs: number; /** Rolling window, in seconds, over which crashes are counted. */ readonly crashWindowSeconds: number; } /** Defaults matching the shipped `voice.wake.*` rows. */ export declare const WAKE_SUPERVISOR_DEFAULTS: WakeSupervisorPolicy; /** What the supervisor decided to do about a crash. */ export type WakeRestartDecision = /** Restart after `delayMs`; this is attempt `attempt` inside the window. */ { readonly kind: 'restart'; readonly delayMs: number; readonly attempt: number; } /** Give up. `reason` is written for a user to read, not only a log. */ | { readonly kind: 'latched'; readonly reason: string; readonly crashes: number; }; /** Current supervisor state, for a status surface. */ export interface WakeSupervisorState { readonly running: boolean; readonly latched: boolean; readonly latchReason: string | null; /** Crashes still inside the rolling window. */ readonly recentCrashes: number; /** Total crashes since the supervisor was created or cleared. */ readonly totalCrashes: number; /** Total restarts issued since the supervisor was created or cleared. */ readonly totalRestarts: number; } /** * Crash accounting for one supervised detector. Holds no process handle: the * caller owns spawning and killing, and asks this object what to do. */ export declare class WakeSupervisor { #private; constructor(policy?: Partial); /** The policy in force, after defaults were merged in. */ get policy(): WakeSupervisorPolicy; /** True once the supervisor has given up and will not restart again. */ get latched(): boolean; /** Why the supervisor gave up, or null while it has not. */ get latchReason(): string | null; /** Snapshot for a status surface. `now` prunes the rolling window first. */ state(now: number): WakeSupervisorState; /** * Record that the process started successfully. Does NOT clear the crash * window: a process that starts and immediately dies would otherwise reset * its own budget on every attempt and loop forever. */ noteStarted(): void; /** * Record a crash and decide what to do. Returns `latched` once the budget is * spent, and keeps returning it, the decision is sticky until * {@link clearLatch}. */ noteCrashed(now: number): WakeRestartDecision; /** * Record a deliberate stop, the user turned the feature off, or the process * was replaced. A clean stop is not a crash and consumes no budget. */ noteStopped(): void; /** * Clear the latch and the crash window, so the detector may run again. Called * when the user explicitly re-enables the feature, which is the deliberate * act the latch was waiting for. */ clearLatch(): void; } //# sourceMappingURL=supervisor.d.ts.map