/**
 * StuckInputSentinel — persistent, periodic recovery for stuck tmux prompts.
 *
 * Why this exists, separate from SessionManager.verifyInjection:
 *
 * verifyInjection (PR #159, v0.28.92) runs as four in-process setTimeout
 * callbacks at 0.5s / 1.5s / 3.5s / 6.5s after an injection. If the server
 * process exits anywhere in that 6.5s window — crash, restart, OOM — all
 * pending timers die. Any message injected just before the crash sits at the
 * `❯` prompt forever. This was exactly the failure mode reproduced on echo
 * 2026-05-11: a 30+ minute server crash loop (better-sqlite3 ABI rebuild
 * loop) left three of echo's sessions with messages from the user that
 * never submitted.
 *
 * This sentinel is the durable backstop. It runs as a long-lived setInterval
 * on the server, scans every running session every TICK_MS, and fires the
 * same escalating recovery actions verifyInjection uses — but it doesn't
 * depend on having armed a timer at injection time. A server restart re-
 * starts the sentinel and it picks up any session that's still stuck.
 *
 * Detection (per session per tick):
 *   1. Capture the bottom of the pane.
 *   2. Find the `❯` prompt line and read whatever's after it.
 *   3. Skip if the pane shows Claude Code is actively working (spinner row,
 *      "esc to interrupt", churn indicator). The sentinel must never fire
 *      Enter while a tool/turn is in flight — that would interrupt work.
 *   4. Skip if the prompt text just appeared (give verifyInjection its 6.5s
 *      window to handle the common race first).
 *   5. If the same prompt text persists across MIN_TICKS_BEFORE_FIRE ticks
 *      without changing AND no activity indicator, fire one recovery action,
 *      then escalate next tick if still stuck.
 *
 * Bounded escalation matches SessionManager.fireStuckInputRecovery:
 *   attempt 0,1 → Enter        attempt 2 → C-m        attempt 3 → Enter+sleep+Enter
 * After MAX_ATTEMPTS the per-session record is marked exhausted; it will not
 * fire again until the prompt text changes (i.e., new content arrives).
 *
 * Persistence: every fire writes one line to
 * `<stateDir>/stuck-input-events.jsonl` for observability. The sentinel does
 * NOT rely on the file to function — in-memory state is fine. The file is
 * for operators auditing what fired and when, and feeds the
 * DegradationReporter.
 *
 * Signal vs authority: this is a recovery detector with the smallest possible
 * action surface — a single Enter keypress per attempt. It does not gate
 * messages, does not block any flow. A false positive (Enter fired against a
 * session that wasn't actually stuck) is a no-op against an empty prompt and
 * therefore safe. The cost is bounded; the benefit is "messages don't
 * silently disappear after a server crash."
 */
import type { SessionManager } from './SessionManager.js';
import { SessionRecoveryChannel, type RecoveryTier } from './SessionRecoveryChannel.js';
export interface StuckInputSentinelOptions {
    /** Poll interval. Default 10s. */
    tickMs?: number;
    /** Minimum ticks the same prompt text must persist before firing. Default 2
     *  (i.e. ≥20s with the default tickMs, which sits comfortably past
     *  verifyInjection's 6.5s window). */
    minTicksBeforeFire?: number;
    /** Max recovery attempts per stuck event. After this, the per-session
     *  record is exhausted until the prompt text changes. Default 4. */
    maxAttempts?: number;
    /** State directory for the events log. */
    stateDir: string;
    /** Disable persistence to disk (used by unit tests). */
    noPersist?: boolean;
    /** Cross-process recovery channel. When provided AND escalationEnabled, the
     *  sentinel escalates PAST the keypress ladder (tier C: it requests a server
     *  restart + replay from the lifeline, which owns ServerSupervisor). When
     *  absent the sentinel keeps its legacy behavior (exhaust → stop). */
    recoveryChannel?: SessionRecoveryChannel;
    /** Master gate for the deeper-tier escalation. Default false (dark): the
     *  sentinel marks the record exhausted after the keypress ladder, exactly as
     *  before. Set true (via config) to enable tier-C recovery requests. */
    escalationEnabled?: boolean;
    /** How many ticks to wait for the lifeline to ack a tier-C request before
     *  giving up (bounded — no restart loop). Default 6. */
    escalationTimeoutTicks?: number;
}
/** In-memory record of what the sentinel has seen for a given session. */
interface SessionStuckRecord {
    /** The prompt text observed most recently (the bit after `❯`). */
    lastPromptText: string;
    /** When we first saw this exact text at the prompt. */
    firstSeenAt: number;
    /** How many consecutive ticks have observed this same text. */
    consecutiveTicks: number;
    /** How many recovery actions have been fired so far for this text. */
    attempts: number;
    /** Whether this record is exhausted (max attempts reached, won't fire
     *  again until the prompt text changes). */
    exhausted: boolean;
    /** Deeper-tier escalation phase (after the keypress ladder exhausts).
     *   - 'none': not escalating (keypress ladder still running or escalation off).
     *   - 'requested': a tier-C recovery request is in flight to the lifeline;
     *     subsequent ticks read the ack.
     *   - 'recovered'/'gave-up': terminal. */
    recoveryPhase: 'none' | 'requested' | 'recovered' | 'gave-up';
    /** attemptId of the in-flight recovery request (matches the channel ack). */
    recoveryAttemptId?: string;
    /** tickCounter value when the request was emitted (for the bounded wait). */
    recoveryRequestedTick?: number;
}
/** Event row written to stuck-input-events.jsonl on each fire. */
export interface StuckInputEvent {
    ts: string;
    session: string;
    promptText: string;
    attempt: number;
    action: 'Enter' | 'C-m' | 'Enter-sleep-Enter' | 'escalate-request' | 'escalate-recovered' | 'escalate-failed' | 'escalate-timeout';
    outcome: 'fired' | 'fire-error' | 'escalated' | 'recovered' | 'gave-up';
    /** Recovery tier for escalation events (absent for keypress events). */
    tier?: RecoveryTier;
    error?: string;
}
export declare class StuckInputSentinel {
    private readonly sessionManager;
    private readonly tickMs;
    private readonly minTicksBeforeFire;
    private readonly maxAttempts;
    private readonly stateDir;
    private readonly noPersist;
    private readonly eventsLogPath;
    private readonly recoveryChannel;
    private readonly escalationEnabled;
    private readonly escalationTimeoutTicks;
    /** Per-session sticky state. Keyed by tmuxSession. */
    private readonly records;
    private intervalHandle;
    private tickInProgress;
    private degradationReportedOnce;
    /** Monotonic tick counter — drives the bounded escalation wait. */
    private tickCounter;
    constructor(sessionManager: SessionManager, opts: StuckInputSentinelOptions);
    start(): void;
    stop(): void;
    /**
     * One tick. Public for unit tests; production code uses start()/stop().
     *
     * Iterates running sessions, captures each pane, and decides whether the
     * session looks stuck. Fires one recovery action per stuck session per
     * tick (escalating across ticks).
     */
    tick(): void;
    /** Evaluate a single tmux session for stuck-input recovery. */
    private evaluateSession;
    /** Pull the text after `❯` from the captured pane.
     *  Returns null if no ❯ line is found or the prompt is empty.
     *
     *  Public for unit tests; the live tick goes through evaluateSession. */
    extractPromptText(pane: string): string | null;
    /** True if the pane currently shows a Claude Code "working" indicator.
     *
     *  We deliberately use ONLY the footer activity hints (`esc to interrupt`,
     *  `ctrl+t to hide tasks`, `tokens · esc`). Claude Code shows these on the
     *  bottom status line ONLY when a turn is in progress, so they're a
     *  precise tell. We do NOT key on spinner glyphs at line-start (✻, ✶, etc.)
     *  because those persist as part of completed-turn markers like
     *  `✻ Brewed for 14m 11s` and `✻ Churned for 1m 16s` — past-tense renders
     *  that stay in the pane long after the turn finished and the agent went
     *  idle. Live reproduction on echo's 2026-05-11 stuck sessions
     *  (echo-qalatra, echo-exploring-slack-integration) showed both held a
     *  stale `✻ Brewed`/`Churned` line while genuinely idle. Including the
     *  glyph in the working-state check would silently exclude those sessions
     *  from recovery — the most user-visible cases of the bug we're closing.
     *
     *  A false-positive "working" (sentinel skips a session that's actually
     *  idle) leaves the user stuck. A false-negative "idle" (sentinel fires
     *  Enter while the agent is mid-turn) interrupts work. Choosing the
     *  precise tell on the right side: the footer hint is structurally only
     *  present mid-turn, so this can't double-fire against an active session.
     *
     *  Public for unit tests. */
    isPaneActivelyWorking(pane: string): boolean;
    /** Heuristic: lines that look like Claude Code status/separator content
     *  rather than wrapped input. Used to avoid mistaking a separator for
     *  wrapped prompt text in extractPromptText. */
    private isStatusLine;
    /** Map attempt index → human-readable action name (matches
     *  SessionManager.fireStuckInputRecovery's escalation). */
    static actionForAttempt(attempt: number): StuckInputEvent['action'];
    /**
     * Clear a session's in-flight tier-C recovery request when the session has
     * recovered out-of-band — it's now actively working (the wedge drained) or it
     * is gone. The sentinel is the sole writer of the request file, so it (not the
     * lifeline) does this cleanup. No-op unless a request is actually in flight.
     * This closes the "request lingers after a successful drain" path that the
     * stuck-detection branches would otherwise skip (they early-return before
     * handleEscalation ever reads the ack).
     */
    private finalizePendingRecovery;
    /**
     * Deeper-tier escalation, invoked once the keypress ladder (tier A) is
     * exhausted but the prompt is still stuck. This is the codex session-wedge
     * SELF-recovery path: the sentinel (server process) cannot restart the server
     * itself — ServerSupervisor lives in the lifeline process — so it REQUESTS a
     * tier-C recovery (server restart + queue replay) through SessionRecoveryChannel
     * and the lifeline executes + acks. The sentinel here only signals, polls the
     * ack, verifies, and bounds the wait (no restart loop). Spec:
     * docs/specs/CODEX-SESSION-WEDGE-SELF-RECOVERY.md.
     *
     * Dark by default: with escalation disabled or no channel, this reproduces the
     * legacy behavior exactly (mark the record exhausted and stop).
     *
     * NOTE: a gentler server-side tier B (re-inject the full pending message before
     * requesting a restart) is a planned refinement; this first cut goes A→C.
     */
    private handleEscalation;
    /** Append one event row to the JSONL log. Best-effort. */
    private recordEvent;
    /** Test helper: read the in-memory record for a session. */
    getRecordForTest(tmuxSession: string): SessionStuckRecord | undefined;
    /** Test helper: clear all in-memory state. */
    resetForTest(): void;
}
export {};
//# sourceMappingURL=StuckInputSentinel.d.ts.map