/**
 * Detects macOS/Linux sleep/wake events via timer drift.
 *
 * When the system sleeps, setInterval timers stop. On wake, the
 * time elapsed between ticks will be much larger than expected.
 * We detect this drift and fire a callback.
 *
 * Ported from Dawn's infrastructure — battle-tested in production.
 *
 * ## CPU-starvation guard (2026-05-28)
 *
 * Timer drift has TWO causes, not one:
 *   1. Real sleep — the OS suspends the process; the wall clock jumps forward.
 *   2. CPU starvation — the machine is so oversubscribed (load >> cores) that the
 *      event loop can't service the `checkIntervalMs` timer on time. The wall
 *      clock advances normally, but the callback fires seconds late.
 *
 * The original detector could not tell these apart, so on a heavily-loaded box
 * (e.g. many concurrent agent sessions) it fired hundreds of false "wake" events
 * — each triggering expensive wake-recovery (tunnel restart, re-registration,
 * failure-counter resets) that piled MORE load on, a self-reinforcing storm.
 *
 * The guard distinguishes the two:
 *   - A **long** drift (>= `longSleepFloorSeconds`) is unambiguously real sleep —
 *     a live machine never starves its own event loop for minutes (a watchdog
 *     would declare it dead first). Always emitted, regardless of load.
 *   - A **short** drift under high system load (`loadavg[0] / cpuCount >
 *     maxLoadRatio`) is treated as CPU starvation and SUPPRESSED — no `wake`.
 *   - A short drift under normal load is a brief real sleep — emitted.
 * On top of classification, a `minWakeIntervalMs` cooldown caps the emit rate so
 * even a misclassified burst can't trigger a recovery storm.
 *
 * ## Recurring-drift guard for the MODERATE-load band (2026-06-15)
 *
 * The load guard (`maxLoadRatio`, default 1.5) and the consecutive-drift burst floor
 * together miss one real cascade: a host oversubscribed only MODERATELY — load just
 * above 1.0/core but below `maxLoadRatio` — stalls its event loop for tens of seconds
 * *intermittently*, ~every couple of minutes. Because on-time ticks fall between those
 * drifts, the consecutive counter resets (so the burst floor never trips), and because
 * the 1-minute `loadavg[0]` sits below 1.5, the load guard never trips either. Each
 * isolated drift then emits a FALSE wake, and the ~2-minute cadence outlasts the
 * `minWakeIntervalMs` cooldown — the 2026-06-15 multi-machine cascade (loadRatio ~1.1/core,
 * `Wake detected after ~33s sleep` roughly every 2 minutes while the host was in use).
 *
 * The recurring-drift guard closes that band: a SHORT drift within `recentDriftWindowMs`
 * of a PRIOR short drift, while `loadRatio > recentDriftLoadFloor`, is recurring
 * starvation and is suppressed — generalizing the burst floor from *consecutive* ticks
 * to *recent* ticks. A genuine isolated sleep (no recent prior drift) and any drift on a
 * lightly-loaded host (ratio <= the floor) are unaffected and still emit; long sleeps are
 * always exempt. Set `recentDriftWindowMs: 0` to disable.
 *
 * Suppressed events are NOT added to `wakeHistory`, so `getCumulativeSleepMsBetween`
 * (the wake-reaper's sleep-credit source) only ever counts genuine sleep.
 */
import { EventEmitter } from 'node:events';
export interface SleepWakeDetectorConfig {
    /** How often to check for drift (ms). Default: 2000 */
    checkIntervalMs?: number;
    /** How much drift (ms) indicates a sleep event. Default: 10000 */
    driftThresholdMs?: number;
    /**
     * Above this `loadavg[0] / cpuCount` ratio, a SHORT drift is classified as CPU
     * starvation and suppressed rather than emitted as a wake. Default: 1.5.
     * Set to `Infinity` to disable the load guard (always trust the drift).
     */
    maxLoadRatio?: number;
    /**
     * A drift at least this long (seconds) is always treated as real sleep,
     * regardless of load — a live event loop never starves for this long.
     * Default: 300 (5 minutes).
     */
    longSleepFloorSeconds?: number;
    /**
     * Minimum gap (ms) between EMITTED wake events. A short drift that would emit
     * within this cooldown of the previous emitted wake is suppressed, bounding
     * recovery storms. Long sleeps bypass the cooldown. Default: 60000 (1 min).
     */
    minWakeIntervalMs?: number;
    /**
     * Number of BACK-TO-BACK drift ticks at/above which a drift is treated as a CPU-
     * starvation burst and suppressed — regardless of duration or the (lagging) load
     * ratio. A genuine sleep is a single isolated drift (the next tick is on-time, which
     * resets the counter); sustained starvation produces consecutive drifts. Default: 2
     * (the 2nd consecutive drift is already a storm). Set 0 to disable.
     */
    driftBurstSuppressFloor?: number;
    /**
     * A SHORT drift within this window (ms) of a PRIOR short drift, while
     * `loadRatio > recentDriftLoadFloor`, is treated as recurring CPU starvation and
     * suppressed — closing the moderate-load band (above the floor, below `maxLoadRatio`)
     * where intermittent drifts dodge BOTH the load guard and the consecutive burst floor.
     * Default: 300000 (5 min). Set 0 to disable.
     */
    recentDriftWindowMs?: number;
    /**
     * `loadavg[0] / cpuCount` above which the recurring-drift guard applies. At or below
     * this the host is not oversubscribed, so a recurring short drift is trusted as a real
     * brief sleep and still emits. Default: 1.0.
     */
    recentDriftLoadFloor?: number;
    /** Injectable system-load source (testing). Default: os.loadavg. */
    loadAvgProvider?: () => number[];
    /** Injectable CPU-count source (testing). Default: os.cpus().length. */
    cpuCountProvider?: () => number;
    /** Injectable wall-clock source (testing). Default: Date.now. */
    nowProvider?: () => number;
}
export interface WakeEvent {
    sleepDurationSeconds: number;
    timestamp: string;
}
export type WakeSuppressionReason = 'cpu-starvation' | 'cooldown';
export interface SuppressedWakeEvent {
    reason: WakeSuppressionReason;
    driftSeconds: number;
    loadRatio: number;
    timestamp: string;
}
export interface SleepWakeStats {
    wakeCount: number;
    totalSleepSeconds: number;
    longestSleepSeconds: number;
    /** Drifts classified as CPU starvation / rate-limited and NOT emitted. */
    suppressedCount: number;
    suppressedByReason: Record<WakeSuppressionReason, number>;
    /** ISO timestamp of the most recent suppression, or null. */
    lastSuppressedAt: string | null;
}
export declare class SleepWakeDetector extends EventEmitter {
    private interval;
    private lastTick;
    private lastEmittedWakeAtMs;
    private checkIntervalMs;
    private driftThresholdMs;
    private maxLoadRatio;
    private longSleepFloorSeconds;
    private minWakeIntervalMs;
    private driftBurstSuppressFloor;
    private recentDriftWindowMs;
    private recentDriftLoadFloor;
    /** Wall-clock (ms) of the most recent SHORT drift, emitted or suppressed. Drives the
     *  recurring-drift guard: a fresh short drift within `recentDriftWindowMs` of this,
     *  under elevated load, is recurring starvation. Null until the first short drift. */
    private lastShortDriftAtMs;
    /** Count of BACK-TO-BACK drift ticks (reset by any on-time tick). A real sleep is
     *  ONE isolated drift; sustained CPU starvation produces consecutive drifts. The
     *  Nth+ consecutive drift is a storm, not a sleep, and is suppressed regardless of
     *  the (lagging, fluctuating) 1-min load ratio — the gap maxLoadRatio alone missed
     *  (2026-06-07: tunnel-restart storm from 10-42s drifts firing whenever loadRatio
     *  momentarily dipped below maxLoadRatio). */
    private consecutiveDrifts;
    private loadAvgProvider;
    private cpuCountProvider;
    private now;
    private wakeHistory;
    private suppressionHistory;
    constructor(config?: SleepWakeDetectorConfig);
    start(): void;
    stop(): void;
    /** Current `loadavg[0] / cpuCount` ratio. Returns 0 when load is unavailable
     *  (e.g. Windows reports [0,0,0]), which disables the starvation guard there. */
    private currentLoadRatio;
    private recordSuppression;
    /**
     * Cumulative wall-time-asleep during the half-open window [startMs, endMs).
     * Used by the wake-reaper (UNIFIED-SESSION-LIFECYCLE §P0 #9 / SE-8) to subtract
     * sleep that overlapped a job run rather than relying on the single last
     * `sleepDurationSeconds` event — a run that started before multiple sleeps was
     * previously credited only the last sleep's duration and reaped early.
     *
     * Each wake event's sleep window is approximated as
     *   [wakeTimestamp − sleepDurationSeconds, wakeTimestamp].
     * Returns the sum of overlap with the query window in milliseconds. Returns 0
     * when the history is empty or no event overlaps.
     *
     * Only EMITTED wakes are in `wakeHistory`; suppressed CPU-starvation drifts are
     * deliberately excluded so starvation is never credited as real sleep.
     */
    getCumulativeSleepMsBetween(startMs: number, endMs: number): number;
    /** Get wake event stats for telemetry reporting. */
    getStats(sinceMs?: number): SleepWakeStats;
}
//# sourceMappingURL=SleepWakeDetector.d.ts.map