/**
 * StaleSessionBackstop — the unkillability backstop (UNIFIED-SESSION-LIFECYCLE §P5).
 *
 * Rules 1–2 of the robustness bar are deliberately conservative ("can't tell"
 * never means "dead"). That creates a dual risk the round-1 review surfaced:
 *   (a) a session that FAKES work (a tight CPU loop, or a transcript that appends
 *       a heartbeat byte every few minutes) is KEPT by every killer forever;
 *   (b) a session stuck `indeterminate` forever leaks a slot.
 * Both are resolved by a single staleness escalation — NEVER an auto-kill. After
 * M minutes of no-forward-progress, or N consecutive `indeterminate` probes, ONE
 * deduped Attention-queue item is raised for an operator decision
 * (investigate / force-kill?). Dedupe is per EPISODE (a recovered session that
 * later goes stale again raises a fresh item).
 *
 * Forward progress is NOT raw byte growth — a wedged session that appends a
 * heartbeat byte would defeat a naive "any growth" gate forever (exactly the
 * absence-of-signal inference rule 2 forbids). "Progress" = ANY of:
 *   (i)   a MEANINGFUL transcript advance: delta ≥ progressFloorBytes AND the new
 *         tail differs from the prior tail (guards the heartbeat/loop case),
 *   (ii)  main-process CPU above an idle floor,
 *   (iii) a change in the positive-idle / prompt state.
 *
 * A server-unreachable `indeterminate` raises ONE GLOBAL "tmux control-plane
 * unreachable" item (not one per session) — anti-flood. Long-`indeterminate`
 * sessions are flagged so the spawn-path can exclude them from the ABSOLUTE
 * session cap, so a fleet of unverifiable panes can never lock a human out of
 * spawning (the death-spiral cannot relocate here).
 *
 * Signal-only: this backstop never ends a session. It only observes and asks.
 */
import type { Session } from '../core/types.js';
import type { AttentionPoster } from './sentinelWiring.js';
export type Liveness = 'alive' | 'dead' | 'indeterminate';
/** A per-tick forward-progress probe for one session. */
export interface ProgressSnapshot {
    /** Whether the transcript path resolved + statted (false ⇒ ambiguous). */
    transcriptResolved: boolean;
    /** Transcript size in bytes (0 when unresolved). */
    transcriptSize: number;
    /** Hash of the last `progressFloorBytes` of the transcript (null when unresolved/short). */
    transcriptTailHash: string | null;
    /** Main-process CPU/IO above an idle floor. `undefined` ⇒ uninspectable. */
    mainProcessActive: boolean | undefined;
    /** Opaque token for the positive-idle / prompt state — ANY change ⇒ progress. */
    idleStateToken: string;
    /** Accumulated CPU-seconds of non-baseline descendant processes. Compared as a
     *  DELTA across snapshots — real CPU used in the interval. For JOB sessions this
     *  replaces the existence-based `mainProcessActive` progress test, so a
     *  wedged-but-alive job (process up, 0% CPU) reads as no-progress. Default 0. */
    descendantCpuSeconds: number;
    /** True when this session was spawned by a job (`Session.jobSlug` set). A job
     *  has no legitimate-idle state (it runs to completion), so it is held to the
     *  stricter cpu-seconds-delta progress test; conversational sessions keep the
     *  existence-based test (which correctly exempts idle-with-bg-process). */
    isJobSession: boolean;
}
export interface LivenessBatch {
    /** False ⇒ the tmux control plane is unreachable (no authoritative snapshot). */
    reachable: boolean;
    /** Per-session tri-state liveness keyed by tmux session name. */
    liveness: Map<string, Liveness>;
}
export interface StaleBackstopDeps {
    listRunningSessions: () => Session[];
    /** ONE batch probe per tick (mirrors the boot-purge single-snapshot path). */
    probeLiveness: (tmuxSessions: string[]) => Promise<LivenessBatch> | LivenessBatch;
    snapshot: (session: Session) => ProgressSnapshot;
    raiseAttention: AttentionPoster;
    /** Flag/unflag a session as long-`indeterminate` for the spawn absolute-cap exclusion. */
    setLongIndeterminate?: (sessionId: string, isLong: boolean) => void;
    /**
     * Resolve a session to its HUMAN Telegram topic name (e.g. "EXO 3.0") so the
     * heads-up reads with the topic name, never a bare `topic-<n>`. Returns null
     * when no friendly name is known (the notice falls back to the session name).
     */
    resolveTopicName?: (session: Session) => string | null;
    /**
     * True when a session is operator-protected (the reaper's `protectedSessions`).
     * A protected session is one the operator deliberately keeps running (e.g. a
     * long autonomous job), so the backstop never escalates it as "stale" — that
     * would be crying wolf on work the user explicitly wants alive. Absent ⇒ no
     * session is treated as protected (prior behavior).
     */
    isProtectedSession?: (session: Session) => boolean;
    now?: () => number;
}
export interface StaleBackstopOptions {
    enabled: boolean;
    tickIntervalSec: number;
    /** No-progress escalation window for JOB sessions (run-to-completion). */
    unverifiableEscalateMinutes: number;
    /**
     * No-progress escalation window for CONVERSATIONAL / autonomous sessions.
     * These legitimately idle between turns and while waiting on multi-minute tool
     * calls, so a tight window false-positives a healthy long-runner as "stale"
     * (the 2026-06-04 flood). Defaults well above the job window. Never below it.
     */
    conversationalEscalateMinutes: number;
    indeterminateEscalateCount: number;
    progressFloorBytes: number;
    /** Minimum CPU-seconds a JOB session's descendants must accumulate between two
     *  snapshots to count as forward progress. Below this, a job with a live but
     *  idle (0% CPU) process is treated as making no progress. Small to catch a
     *  genuinely-wedged job while ignoring sampling jitter. */
    cpuFloorSeconds: number;
}
export declare const DEFAULT_STALE_BACKSTOP_OPTIONS: StaleBackstopOptions;
export declare class StaleSessionBackstop {
    private readonly deps;
    private readonly opts;
    private readonly now;
    private timer;
    private obs;
    private globalUnreachable;
    private globalUnreachableSeq;
    constructor(deps: StaleBackstopDeps, opts?: Partial<StaleBackstopOptions>);
    start(): void;
    stop(): void;
    private obsFor;
    /** Run one observation pass. Public so the lifecycle (and tests) can drive it. */
    tick(): Promise<void>;
    private hasForwardProgress;
    private escalateSession;
    private clear;
}
//# sourceMappingURL=StaleSessionBackstop.d.ts.map