/**
 * ResumeQueue — the durable, ordered queue of mid-work reaped sessions
 * awaiting revival (reap-notify spec R2.2/R2.3, parts of R2.9/R2.10).
 *
 * One queue per machine, JSON-file backed (`state/resume-queue.json`),
 * in-memory authoritative with SYNCHRONOUS persist on every mutation using
 * the crash-durable discipline: write temp → fsync temp → rename → fsync
 * parent dir. A crash loses at most the latest mutation, which boot
 * reconciliation absorbs (a lost enqueue is re-created from the reap-log; a
 * lost transition replays as a failed attempt).
 *
 * Single-writer is ENFORCED, not assumed: a lockfile
 * (`state/resume-queue.lock`, pid + hostname + heartbeat mtime) is taken at
 * start. Stale-lock recovery is automatic on the SAME host (dead pid or old
 * heartbeat ⇒ reclaim + log); a LIVE other process disables the queue loudly.
 * HARD INVARIANT: a lock whose hostname differs from this host is NEVER
 * liveness-probed or reclaimed — pid checks are meaningless cross-host — it
 * disables the queue loudly instead, and the disable message documents the
 * operational recovery (verify nothing else uses the state dir, delete the
 * lock, restart).
 *
 * Eligibility (R2.2) is stricter than midWork: terminal + autonomous +
 * (≥1 strong signal, OR topic-bound with ≥2 distinct weak signals); jobs
 * must opt in (`resumeOnReap:true`, default false); sessions with neither a
 * topic binding nor a jobSlug are excluded at enqueue; operator kills
 * excluded by default; watchdog stuck-kills and topic-moved closeouts never
 * queue. The resurrection LEDGER (keyed on stable identity
 * `topicId ?? jobSlug ?? tmuxSession`, tombstones surviving dequeue, 24h
 * reset window) caps kill-resume-kill loops at `maxResurrections`.
 *
 * Dry-run posture (the fleet's shipped default): entries ARE durably
 * enqueued (observable in `GET /sessions/resume-queue` — the soak needs real
 * midWork entries), but the drainer never spawns (it audits `would-resume`)
 * and nothing user-facing claims a queued resume. Deliberate interpretation
 * of "observe-only", recorded in the side-effects review.
 */
export type ResumePriorityClass = 'interactive' | 'job' | 'other';
export type ResumeEntryStatus = 'queued' | 'starting' | 'respawned' | 'failed' | 'cancelled' | `invalidated:${string}` | `gave-up:${string}`;
export interface ResumeQueueEntry {
    id: string;
    queuedAt: string;
    /** TTL re-anchor on an operator requeue (R2.10); TTL keys on max(queuedAt, requeuedAt). */
    requeuedAt?: string;
    /** Stable identity: topicId ?? jobSlug ?? tmuxSession (R2.3). */
    stableKey: string;
    sessionName: string;
    tmuxSession: string;
    topicId?: number;
    /** Conversation-resume UUID snapshot at enqueue time (R2.6 revalidates). */
    resumeUuid?: string;
    jobSlug?: string;
    cwd: string;
    worktreePath?: string;
    priorityClass: ResumePriorityClass;
    reason: string;
    workEvidence: string[];
    attempts: number;
    status: ResumeEntryStatus;
    lastAttemptAt?: string;
    /** Backoff hold between attempts (R2.9). */
    nextAttemptAt?: string;
    /** Milliseconds of operator-pause time excluded from this entry's TTL clock. */
    frozenMs?: number;
    /** TTL expiry under sustained pressure carries this marker (R2.9). */
    pressureStarved?: boolean;
}
/** Tombstone ledger row — survives dequeue (R2.9 resurrection cap). */
export interface ResurrectionTombstone {
    stableKey: string;
    resurrections: number;
    windowStartAt: string;
    lastResumeAt?: string;
}
/**
 * FD1 — Is the resume-queue state dir on a HOST-LOCAL filesystem (not a network/
 * shared mount)? FAIL-CLOSED: anything we cannot positively confirm as local
 * returns false, so a genuine shared volume is NEVER auto-healed (the two-hosts-
 * one-volume corruption the host-lock invariant protects against). Portable
 * device-column classification via `df -P` (field 1 of the data row):
 *   - `/dev/...`           → local disk          → true
 *   - `host:/path` (NFS) / `//host/share` (SMB)  → network             → false
 *   - df failure / timeout / unparseable / unknown source → false (fail-closed)
 * Checked ONCE at lock-acquisition. Exported for the unit truth-table + canary.
 */
export declare function isStateDirHostLocalDefault(stateDir: string): boolean;
/**
 * Pure FD1 classifier over the `df -P` device-source column. Exported for the
 * unit truth-table + drift canary. FAIL-CLOSED: only a positively-recognized
 * local block device is local; network signatures and anything unrecognized
 * (map/tmpfs/empty) are NOT local.
 */
export declare function classifyDfSourceLocal(source: string): boolean;
export interface ResumeQueueConfig {
    enabled: boolean;
    dryRun: boolean;
    /**
     * FD5 — auto-heal a stale FOREIGN-host lock when it is provably a single-host
     * RENAME (local FS + dead pid + stale heartbeat), instead of disabling the
     * queue. Fleet code-default FALSE (touches a durable-state-corruption
     * invariant — never "cheap"); the dev-agent gate flips it true at the
     * consumption site, dryRun-first. When false → today's disable-on-mismatch.
     */
    autoHealStaleHostLock: boolean;
    maxAttempts: number;
    maxResurrections: number;
    entryTtlHours: number;
    maxQueueSize: number;
    includeOperatorKills: boolean;
}
export declare const DEFAULT_RESUME_QUEUE_CONFIG: ResumeQueueConfig;
export interface EnqueueDecision {
    enqueued: boolean;
    /** Why not (audit vocabulary). */
    why?: string;
    entry?: ResumeQueueEntry;
}
export interface ResumeCandidateInput {
    sessionName: string;
    tmuxSession: string;
    topicId?: number | null;
    jobSlug?: string;
    jobResumeOptIn?: boolean;
    resumeUuid?: string | null;
    cwd: string;
    worktreePath?: string;
    reason: string;
    disposition: 'terminal' | 'recovery-bounce';
    origin: 'operator' | 'autonomous';
    workEvidence: string[];
}
export interface ResumeQueueDeps {
    /** `.instar` state dir — files live under `<stateDir>/state/`. */
    stateDir: string;
    /** Decision-transition audit sink (logs/resume-queue.jsonl). */
    audit?: (event: Record<string, unknown>) => void;
    /** Aggregated attention surface (ONE rolling item — the caller dedupes). */
    raiseAggregated?: (kind: string, detail: string) => void;
    now?: () => number;
    hostname?: () => string;
    /** pid liveness probe (tests override). */
    pidAlive?: (pid: number) => boolean;
    /** FD1 host-local FS probe (tests override; default `isStateDirHostLocalDefault`). */
    isStateDirHostLocal?: (stateDir: string) => boolean;
}
/** Pure eligibility classifier (R2.2) — exported for tests. */
export declare function classifyEligibility(input: ResumeCandidateInput, cfg: Pick<ResumeQueueConfig, 'includeOperatorKills'>): {
    eligible: boolean;
    why?: string;
};
export declare class ResumeQueue {
    private readonly deps;
    private readonly cfg;
    private readonly now;
    private readonly statePath;
    private readonly lockPath;
    private state;
    private seq;
    /** Non-null ⇒ the queue is disabled (lock conflict / foreign lock). */
    private disabledReason;
    private lockHeld;
    constructor(deps: ResumeQueueDeps, cfg?: Partial<ResumeQueueConfig>);
    config(): ResumeQueueConfig;
    isDisabled(): string | null;
    isPaused(): boolean;
    isDryRun(): boolean;
    /**
     * Acquire the single-writer lock + load the durable state. Returns false
     * (queue disabled, reason in `isDisabled()`) on a live-other-process or
     * foreign-host lock — never throws for those.
     */
    start(): boolean;
    stop(): void;
    /** Refresh the lock heartbeat (drainer calls this every tick). */
    heartbeat(): void;
    private acquireLock;
    /**
     * FD4 — atomic first-writer-wins takeover of a classified-stale lock. Removes
     * the stale lock then creates the new one with O_EXCL ('wx'): exactly one
     * racer's create succeeds; a concurrent boot gets EEXIST → false (it disables
     * and re-evaluates next start, never blind-overwrites). The next-acquire
     * live-pid + heartbeat check backstops the ultra-narrow double-unlink window.
     */
    private takeOverLockAtomic;
    /**
     * D2 — guard-posture self-report. `enabled:false` whenever the queue is
     * disabled (e.g. an un-healable foreign-host lock), so the guard-posture
     * inventory classifies a disabled revival queue as `off-runtime-divergent`
     * (config on, runtime off → the alerting class) instead of it being visible
     * only as a `disabled:` string. ALWAYS reflects live runtime state — NOT
     * dryRun-gated (a disabled guard must be loud even during a dryRun soak).
     */
    guardStatus(): {
        enabled: boolean;
        dryRun: boolean;
        reason?: string;
    };
    private load;
    /** Crash-durable persist: temp → fsync temp → rename → fsync parent dir. */
    private persist;
    private audit;
    /** Boot reconciliation half 1 (R2.4): `starting` found at load = failed attempt. */
    private reconcileStartingEntries;
    /**
     * Boot reconciliation half 2 (R2.4): recent reap-log midWork terminal
     * autonomous entries with no queue entry AND no tombstone are re-enqueued —
     * closing the crash window where an enqueue's persist was lost. The caller
     * supplies reconstruction (session records, resume map) — a candidate that
     * cannot be reconstructed is skipped (the next re-reap recreates it).
     */
    reconcileFromReapLog(candidates: ResumeCandidateInput[]): number;
    private stableKeyFor;
    /**
     * The enqueue decision (R2.2 + R2.3 + R2.9 resurrection cap). Audited on
     * every outcome. In dry-run the entry is still durably enqueued (the soak's
     * observable), but nothing downstream spawns or claims a resume.
     */
    considerEnqueue(input: ResumeCandidateInput, opts?: {
        source?: string;
    }): EnqueueDecision;
    private tombstoneFor;
    /** Record a successful resume (drainer, after spawn verification). */
    recordResumeSuccess(stableKey: string): void;
    /** Ordered open entries: interactive → job → other, FIFO inside each (R2.5). */
    nextCandidates(): ResumeQueueEntry[];
    list(): ResumeQueueEntry[];
    get(id: string): ResumeQueueEntry | undefined;
    /** State-transition helpers (each persists + audits the TRANSITION). */
    transition(id: string, status: ResumeEntryStatus, extra?: Partial<ResumeQueueEntry>): boolean;
    /** Operator/API cancel (an explicit per-topic stop cancels that topic's entries — R2.7). */
    cancel(id: string): boolean;
    cancelByTopic(topicId: number): number;
    /**
     * Requeue clamps (R2.10): eligible from gave-up:* ONLY — never `cancelled`
     * (an operator stop) and refused while paused (callers enforce the 409).
     * Resets attempts and RE-ANCHORS the TTL clock while preserving the original
     * queuedAt for the R2.6 operator-stop / job-ran-since checks. Requeueing a
     * gave-up:resurrection-cap entry grants exactly ONE audited override.
     */
    requeue(id: string): {
        ok: boolean;
        why?: string;
    };
    /**
     * Queue-global pause (R2.7): entries keep their states; TTLs freeze.
     *
     * UPGRADE-ON-DELIBERATE-HALT (spec: resume-queue-stale-emergency-pause.md,
     * review rounds 4–5 — codex/gemini): pause is first-writer-wins EXCEPT that a
     * DELIBERATE, non-auto-resumable reason (e.g. `'autonomous stop-all'`) UPGRADES
     * an existing AUTO-RESUMABLE (emergency/sentinel) pause — so an operator's
     * explicit "halt all automation" issued while a stale-emergency pause is active
     * is honored, not silently no-op'd into something the drainer can later
     * auto-clear. The reverse (an emergency stop while a deliberate halt is active)
     * stays a no-op: a deliberate halt is never downgraded into auto-resumable.
     */
    pause(reason: string): void;
    /** Explicit unpause lever (R2.7) — accumulates the pause into frozenMs. */
    unpause(): void;
    pauseInfo(): {
        paused: boolean;
        pausedAt?: string;
        reason?: string;
    };
    /**
     * TTL sweep (R2.9): INCIDENT-AGE semantics — wall clock since
     * max(queuedAt, requeuedAt), minus operator-pause freeze, NOT frozen by
     * pressure. Expiries under sustained pressure carry the pressure-starved
     * marker so a day-long overload reads differently from ordinary staleness.
     * No sweep while paused (an operator pause must not silently expire the queue).
     */
    expireTtl(pressureCalm: boolean): ResumeQueueEntry[];
    /** Snapshot for the API surface (R2.10). */
    snapshot(): {
        paused: boolean;
        pauseReason?: string;
        disabled: string | null;
        dryRun: boolean;
        entries: ResumeQueueEntry[];
        tombstones: ResurrectionTombstone[];
    };
    /** True when a LIVE (non-dry-run) queued entry exists for this tmux session
     *  — feeds the notifier's "restart is queued" line (R1.2). */
    hasLiveQueuedEntryFor(tmuxSession: string): boolean;
}
//# sourceMappingURL=ResumeQueue.d.ts.map