/**
 * CompactionSentinel — Owns the full lifecycle of recovering a Claude session
 * after context compaction.
 *
 * Prior behavior (before this class): three independent triggers —
 * PreCompact hook event, SessionWatchdog 'compaction-idle' poll, and the
 * compaction-recovery.sh endpoint — each called `recoverCompactedSession`
 * fire-and-forget. If the injection didn't wake the session up (session
 * genuinely hung, injection silently dropped, Claude process dead), nobody
 * noticed. The idle-prompt zombie-killer then raced the recovery window
 * and killed the session 15 minutes later, wiping the conversation.
 *
 * What this class adds:
 *   1. Dedupe across triggers — once a session is in recovery, additional
 *      reports within the guard window are ignored.
 *   2. Verification — after each inject attempt, watch the session's JSONL
 *      file for size/mtime growth, which is the cheapest reliable signal
 *      that Claude actually processed the prompt and produced output.
 *   3. Retry with backoff — if verification fails within the window, try
 *      injecting again, up to `maxInjectAttempts`.
 *   4. Zombie-kill veto — while a recovery is in flight, SessionManager's
 *      idle-prompt cleanup is told to leave the session alone via the
 *      activeRecoveryChecker hook.
 *   5. Observable outcomes — emits `compaction:detected`,
 *      `compaction:inject-attempted`, `compaction:recovered`,
 *      `compaction:failed` events with a single `[Sentinel]` log prefix
 *      so the full lifecycle is greppable.
 *
 * Decoupling: the class takes a `recoverFn` in its deps rather than pulling
 * in server.ts. The existing `recoverCompactedSession` helper is passed
 * straight through, which keeps message-bus / topic-routing concerns out
 * of this file.
 */
import { EventEmitter } from 'node:events';
export type CompactionTrigger = 'PreCompact' | 'watchdog-poll' | 'recovery-hook' | string;
export type CompactionStatus = 'pending-inject' | 'verifying' | 'deferring' | 'retrying' | 'recovered' | 'failed';
export interface RecoveryState {
    sessionName: string;
    trigger: CompactionTrigger;
    detectedAt: number;
    attempts: number;
    /** Times we deferred an inject because the session was actively working. */
    workingDefers: number;
    lastInjectAt: number;
    baselineJsonlPath: string | null;
    baselineJsonlSize: number | null;
    baselineJsonlMtime: number | null;
    status: CompactionStatus;
}
export interface CompactionSentinelDeps {
    /**
     * Actually perform a recovery injection on a session. The sentinel uses this
     * rather than talking to SessionManager directly so topic/channel routing
     * stays in server.ts. Returns whether the injection was accepted.
     */
    recoverFn: (sessionName: string, triggerLabel: string) => Promise<boolean>;
    /**
     * Project directory, used to locate Claude Code's JSONL files for the
     * verification check. Usually `config.projectDir`.
     */
    projectDir: string;
    /**
     * Resolve a session's Claude Code session UUID (from hook events). When
     * present, the sentinel reads the jsonl file named exactly `<uuid>.jsonl`
     * instead of falling back to most-recently-modified — which prevents
     * false positives where a sibling session's output makes this one look
     * recovered. Return undefined for sessions whose uuid isn't known yet.
     */
    getClaudeSessionId?: (sessionName: string) => string | undefined;
    /**
     * Resolve a session's framework ('codex-cli' | 'claude-code' | undefined). When it
     * returns 'codex-cli', recovery-verification reads the newest codex ROLLOUT jsonl
     * (account-wide growth signal) instead of the Claude transcript. Absent/non-codex →
     * the unchanged Claude path is used (Claude behavior byte-for-byte preserved).
     */
    getSessionFramework?: (sessionName: string) => string | undefined;
    /** Override $CODEX_HOME (tests). Defaults to ~/.codex. Only used for codex sessions. */
    codexHome?: string;
    /** Override ~/.gemini (tests). Defaults to ~/.gemini. Only used for gemini-cli sessions. */
    geminiHome?: string;
    /**
     * Override for the JSONL lookup root. Primarily for tests. Defaults to
     * `$HOME/.claude/projects/<project-hash>`.
     */
    jsonlRoot?: string;
    /**
     * Defer (skip starting) a compaction recovery when this returns true — e.g.
     * a rate-limit recovery already owns this session. Prevents two sentinels
     * injecting into one pane concurrently. Default: never defer.
     */
    deferIf?: (sessionName: string) => boolean;
    /**
     * Is the session actively working RIGHT NOW (mid-turn)? When this returns
     * true the sentinel will NOT inject/re-inject a recovery prompt — it waits
     * another verify window instead, up to `maxWorkingDefers`. This is the fix
     * for the false "session is restarting" loop: a long extended-think on a
     * large context writes nothing to the JSONL until the turn lands, so the
     * no-growth check used to read it as "stuck" and re-inject, burying the
     * user's real message under stacked recovery bootstraps. Wired in server.ts
     * to SessionManager.isSessionActivelyWorking. When undefined, behavior is
     * unchanged (never defers) — so this is purely additive.
     */
    isActivelyWorking?: (sessionName: string) => boolean;
    /** Override for 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 CompactionSentinelConfig {
    /** Ignore repeat reports for the same session within this window. */
    dedupeWindowMs?: number;
    /** How long to wait after injection before verifying output. */
    verifyWindowMs?: number;
    /** Max inject attempts before declaring failure. */
    maxInjectAttempts?: number;
    /** Max total time a recovery can block the zombie-killer. */
    recoveryGuardMs?: number;
    /**
     * Max consecutive times a recovery may defer an inject because the session
     * is actively working (mid-turn). Each defer waits one `verifyWindowMs`
     * WITHOUT re-injecting. Bounds the wait for a session whose "working" footer
     * is genuinely hung — after the cap, normal inject/retry resumes. Set to 0
     * to disable deferral entirely (restores pre-fix behavior).
     */
    maxWorkingDefers?: number;
}
export interface CompactionSentinelEvents {
    'compaction:detected': [RecoveryState];
    'compaction:inject-attempted': [RecoveryState & {
        accepted: boolean;
    }];
    'compaction:deferred': [RecoveryState];
    'compaction:recovered': [RecoveryState & {
        jsonlDelta: number;
    }];
    'compaction:failed': [RecoveryState & {
        reason: string;
    }];
}
export declare class CompactionSentinel extends EventEmitter {
    private readonly deps;
    private readonly cfg;
    private readonly active;
    private readonly timers;
    private readonly recentReports;
    private deferIf?;
    constructor(deps: CompactionSentinelDeps, config?: CompactionSentinelConfig);
    /** Late-bind the deferral predicate (server.ts wires the two sentinels at each other). */
    setDeferIf(fn: (sessionName: string) => boolean): void;
    private now;
    private setTimer;
    private clearTimer;
    /**
     * Report a compaction event for a session. Called by all three trigger paths
     * (PreCompact hook, watchdog poll, recovery-hook endpoint). Deduped — if the
     * same session is already in active recovery or was reported within the
     * dedupe window, this is a no-op.
     */
    report(sessionName: string, trigger: CompactionTrigger): void;
    /**
     * Predicate for SessionManager's zombie-killer: is this session currently
     * being recovered? If yes, the zombie-killer should skip it.
     */
    isRecoveryActive(sessionName: string): boolean;
    /**
     * Manually clear recovery state. Used when the session completes or is
     * explicitly reset. Exposed mainly for tests and edge cases.
     */
    clear(sessionName: string): void;
    /** Stop all pending timers. Safe to call multiple times. */
    stop(): void;
    /** Get current active recovery state (test introspection). */
    getState(sessionName: string): RecoveryState | undefined;
    private attemptInjection;
    /**
     * If the session is actively working (mid-turn) and we haven't exhausted the
     * defer budget, record a defer, schedule another verify window WITHOUT
     * injecting, and return true (the caller must return). Otherwise return false
     * and let the caller proceed with its normal inject/retry/fail path.
     *
     * This is the heart of the busy-session guard: a long extended-think on a
     * large context emits nothing to the JSONL until the turn lands, so the
     * no-growth check would otherwise read it as "stuck" and re-inject — burying
     * the user's real message. Deferring waits it out instead, and the cap means
     * a genuinely-hung "working" footer still gets a forced inject eventually.
     */
    private deferForActiveWork;
    /** Schedule one verify pass after the verify window. */
    private scheduleVerify;
    private verifyRecovery;
    private finalize;
    /**
     * Look up the JSONL file for this session. Prefers the exact file by
     * Claude Code session UUID when known (prevents a sibling session's
     * activity from looking like this session recovered). Falls back to the
     * most recently-modified file in the project's jsonl root.
     */
    private readJsonlBaseline;
}
//# sourceMappingURL=CompactionSentinel.d.ts.map