/**
 * ActiveWorkSilenceSentinel — detects sessions in the registry that were
 * actively producing output and then went silent for an extended period,
 * independent of topic binding.
 *
 * Closes the watchdog gap surfaced 2026-05-22: a gsd-style sub-spawned
 * worktree session went silent for 1h16m and three existing watchdogs all
 * missed it:
 *   - SessionWatchdog requires a long-running child (this had none).
 *   - SessionMonitor only inspects topic-bound sessions (this wasn't).
 *   - PresenceProxy wakes on inbound user messages (none arrived).
 *
 * This sentinel sits one layer below those: it walks the SessionRegistry
 * directly, looking for "had output recently, now hasn't for N minutes."
 * On match it tries one gentle nudge; if that doesn't unstick the session
 * within the verify window, it escalates via the tone-gated /attention
 * path.
 *
 * Spec: docs/specs/silently-stopped-trio.md
 *
 * Signal-vs-authority: the threshold check is a detector. The nudge is a
 * bounded recovery primitive. The escalation goes through MessagingToneGate
 * via the notify path. No new blocking authority.
 */
import { EventEmitter } from 'node:events';
export type SilenceStatus = 'detected' | 'nudged' | 'recovered' | 'recovering' | 'recovery-failed' | 'suppressed-active' | 'escalated';
/** Observability funnel events (HONEST-PROGRESS-MESSAGING E). */
export type SilenceFunnelEvent = 'detected' | 'suppressed_active_indicator' | 'suppressed_subagent_live' | 'suppressed_corroborate_error' | 'escalated_indeterminate' | 'escalated_frozen_indicator' | 'escalated_legacy';
export interface SilenceState {
    sessionName: string;
    detectedAt: number;
    lastOutputAtAtDetection: number;
    nudgedAt: number;
    status: SilenceStatus;
    /** Auto-recovery (respawn) attempts made for this stall episode. Bounded by
     *  maxAutoRecoveries to prevent a respawn-loop on a session that stays stuck. */
    recoveryAttempts: number;
    /** A1/A5: when escalation was suppressed because the live frame still showed an
     *  active-work indicator, the wall-clock the suppression began AND the frozen
     *  frame's hash. If the SAME frame stays byte-identical past
     *  activeWorkMaxFrozenIndicatorMs, A5 escalates (a frozen-indicator hang).
     *  Reset whenever the frame changes (genuine progress). */
    activeFrozenSince?: number;
    activeFrozenHash?: string;
}
export interface SessionRegistryEntry {
    sessionName: string;
    /** Wall-clock (ms) of the most recent tmux output observed for this session. */
    lastOutputAt: number;
    /** Optional flag — if true, sentinel skips this session (e.g. operator paused). */
    paused?: boolean;
    /** Optional flag — true if another sentinel/restart is in flight; skip. */
    recoveryInFlight?: boolean;
}
export interface ActiveWorkSilenceSentinelDeps {
    /** List every session the registry knows about (topic-bound or not). */
    listSessions: () => SessionRegistryEntry[];
    /** Send an empty send-keys to wake the pane. Returns whether it was accepted. */
    nudgeFn: (sessionName: string) => Promise<boolean>;
    /** Route a user-facing message; server.ts owns topic routing (→ the stalled
     *  session's OWN topic when auto-recover is on). */
    notifyFn: (sessionName: string, text: string) => Promise<void>;
    /** Auto-recovery primitive: fresh-respawn a confirmed-stuck session
     *  (conversation preserved via resume/bootstrap). Returns whether it succeeded.
     *  Optional — when absent (or autoRecover off) the sentinel falls back to the
     *  ask-the-user escalation. DESTRUCTIVE (discards in-context work), so it only
     *  runs after the nudge fails AND is bounded by maxAutoRecoveries. */
    recoverFn?: (sessionName: string) => Promise<boolean>;
    /** Capture the session's CURRENT live tmux frame (HONEST-PROGRESS-MESSAGING
     *  A1/A2). Used to corroborate a wedge before escalating. When absent, the
     *  sentinel keeps its legacy behavior (escalate on threshold+nudge, no
     *  corroboration) — back-compat for callers that don't wire it. */
    captureFrame?: (sessionName: string) => string | null;
    /** Does this live frame still show an active-work indicator (spinner / "esc to
     *  interrupt" / "(running)")? Wired to looksActivelyWorking(). A1: if true, the
     *  session is in an active turn — suppress, never escalate. */
    looksActivelyWorking?: (frame: string, sessionName: string) => boolean;
    /** Is this live frame a clean idle prompt (turn finished, waiting for input)?
     *  A2(b): a clean idle prompt is NOT a wedge — only an indeterminate non-prompt
     *  frame is. When absent, the indeterminate check degrades to "no active-work
     *  indicator" alone. */
    isCleanIdlePrompt?: (frame: string, sessionName: string) => boolean;
    /** Does the session have a live sub-agent (SubagentTracker, no stoppedAt)?
     *  A2(c): a session whose sub-agent is mid-work is not wedged — suppress. */
    hasActiveSubagents?: (sessionName: string) => boolean;
    /** Observability hook (HONEST-PROGRESS-MESSAGING E). Records a funnel event so
     *  the suppressed-vs-escalated rates are auditable. Never throws into the
     *  sentinel; best-effort. */
    recordEvent?: (event: SilenceFunnelEvent, sessionName: string, detail?: string) => void;
    /** Override Date.now for tests. */
    now?: () => number;
    /** Override timer setters for tests. */
    setTimer?: (fn: () => void, ms: number) => ReturnType<typeof setTimeout>;
    clearTimer?: (handle: ReturnType<typeof setTimeout>) => void;
}
export interface ActiveWorkSilenceSentinelConfig {
    enabled?: boolean;
    /** Tick interval — how often we walk the registry (ms). Default 60s. */
    tickIntervalMs?: number;
    /** Silence threshold — output gap that triggers detection (ms). Default 30m
     *  (HONEST-PROGRESS-MESSAGING A4 — raised from 15m; with A1/A2 corroboration
     *  this collapses the false-positive rate without missing a genuine wedge). */
    silenceThresholdMs?: number;
    /** Verify window — how long after nudge before declaring escalate (ms). Default 30s. */
    verifyWindowMs?: number;
    /** Frozen-indicator hard timeout (HONEST-PROGRESS-MESSAGING A5). A1 suppresses
     *  escalation whenever the live frame still shows an active-work indicator —
     *  which would permanently hide a genuine hang that froze mid-tool with the
     *  indicator still on screen. Backstop: if a frame WITH an active-work
     *  indicator stays byte-identical this long, escalate once with an extra-hedged
     *  message. Default 90m. */
    activeWorkMaxFrozenIndicatorMs?: number;
    /** Auto-recover (respawn) a stalled session after the nudge fails, instead of
     *  only asking the user. DARK by default — destructive (discards in-context
     *  work), so opt-in. When off, behaviour is unchanged (nudge → ask). */
    autoRecover?: boolean;
    /** Hard cap on auto-recovery (respawn) attempts per stall episode. Prevents a
     *  respawn-loop on a session that stays stuck after a respawn. Default 1 —
     *  one auto-respawn, then fall back to asking the user. */
    maxAutoRecoveries?: number;
}
export declare class ActiveWorkSilenceSentinel extends EventEmitter {
    private readonly deps;
    private readonly cfg;
    private readonly states;
    private readonly verifyTimers;
    private tickHandle;
    /** Liveness of the tick loop (GUARD-POSTURE-ENDPOINT-SPEC §2.2): 0 = never ticked. */
    private lastTickAt;
    constructor(deps: ActiveWorkSilenceSentinelDeps, cfg?: ActiveWorkSilenceSentinelConfig);
    start(): void;
    stop(): void;
    /** Public for tests. Walk the registry and act on silence findings. */
    tick(): void;
    /** Public entry: report a silence finding. Idempotent. */
    report(sessionName: string, lastOutputAt: number): void;
    isRecoveryActive(sessionName: string): boolean;
    listActive(): SilenceState[];
    clear(sessionName: string): void;
    private runNudge;
    private verifyNudge;
    /**
     * Corroboration gate (HONEST-PROGRESS-MESSAGING A1/A2/A5). Before claiming a
     * session is stuck, re-capture its LIVE frame and prove it really is wedged —
     * a static scrollback on a session that is mid-tool, mid-sub-agent, or just
     * finished is NOT a freeze. The whole path FAILS CLOSED (FD-6): any error
     * suppresses the escalation rather than risk a false "it's stuck" claim.
     */
    private escalate;
    /** Mark a session as suppressed-active and (re)stamp the frozen-frame timer.
     *  A changed frame hash resets the A5 timer (genuine progress within the turn). */
    private markSuppressedActive;
    /** The autoRecover-or-ask escalation, reached only after corroboration (or in
     *  the legacy no-capture path). */
    private proceedEscalation;
    /** A3 honest wording — evidence + uncertainty, never an asserted conclusion. */
    private honestAskMessage;
    /**
     * Auto-recovery ladder (dark by default): notify in the stalled session's own
     * topic, respawn it, then notify the outcome. Bounded by maxAutoRecoveries
     * (the recoveryAttempts increment + the escalate() guard) so a session that
     * stays stuck after a respawn falls back to asking the user — never a loop.
     */
    private runRecovery;
    private notify;
    private setTimer;
    private clearTimer;
    /** Sync in-memory runtime read for the GuardRegistry (GET /guards).
     *  MUST stay a cheap property read — no I/O, no session listing. */
    guardStatus(): {
        enabled: boolean;
        lastTickAt: number;
    };
}
//# sourceMappingURL=ActiveWorkSilenceSentinel.d.ts.map