/**
 * ResumeQueueDrainer — the gated recovery loop that revives mid-work reaped
 * sessions, one at a time, once the machine has recovered
 * (reap-notify spec R2.4–R2.11; Tier 1 supervision per P7, observe-only
 * during the soak).
 *
 * Discipline (all deterministic gates must pass; NEVER bypassable — the
 * manual drain lever may skip the calm-ticks requirement ONLY):
 *  - pressure tier `normal` for `requiredCalmTicks` consecutive ticks (the
 *    SAME shared gauge as the SessionReaper — one definition of "calm");
 *  - the quota gate (`canSpawnSession`);
 *  - session count below cap;
 *  - no quota migration in flight.
 *  AT MOST ONE entry resumes per tick — the operator's explicit "ordered
 *  queue so they don't all resume at once". A full queue (50) drains in
 *  ≥50 calm minutes BY DESIGN (recovery-time envelope, stated).
 *
 * Drain-time reality validation (R2.6) re-checks the world immediately
 * before any spawn; any failure → `invalidated:<why>`, audited, folded into
 * the aggregated surface — never a spawn. Dequeue-side HARD INVARIANTS
 * (UUID format, enum, charset, length caps — the Signal-vs-Authority
 * brittle-blocker exemption) protect `claude --resume` argv and the
 * scheduler from corrupted state.
 *
 * Failure ladder + brakes (R2.9 / P19): spawn verified alive after a grace
 * period; failure → attempts++ with backoff; maxAttempts → gave-up;
 * `breakerThreshold` consecutive failures ACROSS entries opens the circuit
 * for `breakerCooldownMin` with ONE aggregated degradation notice. ALL
 * give-up classes fold into ONE rolling deduped attention item (P17).
 *
 * Tier 1 supervision (observe-only during the dev soak): each
 * about-to-resume decision gets a fast-tier LLM sanity check whose verdict
 * is AUDITED but never defers; shed/timeout (5s deadline) → deterministic
 * gates proceed, audited `supervision:'shed'`. Its own lever:
 * `resumeQueue.tier1Check`.
 *
 * Dry-run (the fleet's shipped code-default): every would-spawn is audited
 * `would-resume` (once per entry) and nothing spawns; TTL expiries do not
 * raise attention (a fleet of observe-only queues must not page anyone).
 */
import type { ResumeQueue, ResumeQueueEntry } from './ResumeQueue.js';
export interface ResumeQueueDrainerDeps {
    queue: ResumeQueue;
    /** Shared pressure gauge (HostPressureSampler) — one definition of calm. */
    pressureTier: () => 'normal' | 'moderate' | 'critical';
    /** QuotaManager.canSpawnSession — consulted, never bypassed. */
    canSpawnSession: () => boolean;
    /** Session count below cap. */
    sessionCountOk: () => boolean;
    /** A quota migration is mid-flight. */
    migrationInFlight: () => boolean;
    liveSessionForTopic: (topicId: number) => boolean;
    currentResumeUuid: (topicId: number) => string | null;
    topicOwnerElsewhere: (topicId: number) => boolean;
    /** The topic's CURRENT project binding still matches the entry's cwd. */
    topicBindingMatches: (topicId: number, cwd: string) => boolean;
    /** An operator stop instruction recorded for the topic since the entry queued. */
    operatorStopSince: (topicId: number, sinceIso: string) => boolean;
    /**
     * Resume-idle-autonomous fix (spec: resume-idle-autonomous-on-reap.md):
     * OPTIONAL drain-time liveness re-check for an entry admitted because its topic
     * had an active autonomous run at age-limit-reap time. Returns `false` when the
     * run is NO LONGER active (completed OR its window elapsed between enqueue and
     * drain) → the entry invalidates `autonomous-run-finished`, never a spawn.
     * Absent (undefined) ⇒ today's behavior (no extra check) — back-compat.
     */
    autonomousRunFinished?: (topicId: number, reason: string) => boolean;
    /**
     * GAP-B D9 (spec: autonomous-registration-guarantee.md) — OPTIONAL drain-time
     * re-validation for an entry admitted via the COMMITMENT_ACTIVE_RUN_REASON
     * backstop (an UNregistered run kept alive by a fresh open commitment). Returns
     * `true` when the qualifying commitment + recent-user-activity STILL hold at
     * drain time → spawn proceeds; `false` when the commitment was
     * delivered/expired/violated or the user-activity window lapsed between enqueue
     * and drain → the entry invalidates `commitment-no-longer-active`, never a
     * spawn (so a done-but-not-marked commitment can't revive finished work). The
     * state-file `autonomousRunFinished` re-check is useless here (no state file by
     * construction), so this is its parallel. Absent ⇒ today's behavior. A
     * throwing/absent dep resolves to the SAFE side (still-active ⇒ allow), matching
     * autonomousRunFinished's contract: it never wrongly drops a legitimate revival.
     */
    commitmentStillActiveForTopic?: (topicId: number) => boolean;
    /** Jobs: exists, not disabled, not CrashLoopPauser-paused, not run since queuedAt. */
    jobCheck: (slug: string, queuedAtIso: string) => {
        ok: boolean;
        why?: string;
    };
    pathExists: (p: string) => boolean;
    /** Respawn the topic's session (continuation prompt + entry cwd via the new
     *  spawn-path parameter). Returns the spawned tmux session name. */
    respawnTopic: (entry: ResumeQueueEntry, continuationPrompt: string) => Promise<string>;
    triggerJob: (slug: string) => Promise<'triggered' | 'queued' | 'skipped'>;
    /** Spawn verification after a grace period (R2.9). */
    spawnAliveAfterGrace: (tmuxSession: string) => Promise<boolean>;
    /** R2.11 honest resume notice to the topic ("restarted", never "resumed"). */
    notifyResumed?: (entry: ResumeQueueEntry) => void;
    /** Build-Session Yield Safety (ACT-839) R2.2: fired after a successful respawn
     *  when the revived entry carried `uncommitted-worktree-work`. The wiring
     *  (server.ts) registers a durable, beacon-enabled CommitmentTracker obligation
     *  so a STALLED revived session is re-surfaced. Present ONLY when the dev-gated
     *  yieldSafety feature is live — its presence is the gate. */
    onWorktreeRevival?: (entry: ResumeQueueEntry) => void;
    /** ONE rolling aggregated attention surface (caller dedupes on kind). */
    raiseAggregated: (kind: string, detail: string) => void;
    /** Decision-transition audit sink (logs/resume-queue.jsonl). */
    audit: (event: Record<string, unknown>) => void;
    /** Observe-only Tier 1 check (LlmQueue-backed). Resolves a verdict;
     *  the drainer imposes the 5s deadline and NEVER defers on it. */
    tier1Check?: (entry: ResumeQueueEntry) => Promise<{
        sensible: boolean;
        reasoning?: string;
    }>;
    now?: () => number;
}
export interface ResumeQueueDrainerConfig {
    drainIntervalSec: number;
    requiredCalmTicks: number;
    maxAttempts: number;
    breakerThreshold: number;
    breakerCooldownMin: number;
    tier1Check: boolean;
    /** Spawn-failure backoff base (doubles per attempt). */
    attemptBackoffMs: number;
    /** Tier-1 verdict deadline (prevents tick serialization). */
    tier1DeadlineMs: number;
    /**
     * Stale-emergency-pause auto-recovery (spec:
     * resume-queue-stale-emergency-pause.md). Layer 2 auto-resumes a stale
     * emergency/sentinel pause only when an active-autonomous-run entry was queued
     * STRICTLY MORE than this many minutes AFTER the pause began — long enough that
     * a fresh "kill all" + a coincidental age-reap minutes later never auto-undoes
     * the stop. CODE-defaulted (never frozen into ConfigDefaults — preserves the
     * fleet flip), like the other resumeQueue keys.
     */
    staleEmergencyPauseAutoResumeMin: number;
    /** Master off-switch for Layer 2 (the bounded behavior change). Layer 1 (the
     *  paused-with-waiting-work alert) is unaffected and always on. */
    autoResumeStalePause: boolean;
}
export declare const DEFAULT_RESUME_DRAINER_CONFIG: ResumeQueueDrainerConfig;
export declare class ResumeQueueDrainer {
    private readonly deps;
    private readonly cfg;
    private readonly now;
    private timer;
    private ticking;
    private calmTicks;
    private consecutiveFailures;
    private breakerOpenUntil;
    private lastGateBlock;
    /** would-resume audited once per entry in dry-run. */
    private dryRunAudited;
    /**
     * Layer-1 (paused-with-waiting-work) dedupe marker — IN MEMORY by design (a
     * server restart mid-pause may re-alert once, which folds harmlessly into the
     * single rolling aggregate item). Keyed on `pausedAt|waitingCount` so a NEW
     * pause OR a GROWING backlog under the same pause re-alerts (closes the
     * "alert once then go silent as more entries accumulate" gap — codex r3 #3).
     */
    private lastPausedWaitingAlertKey;
    constructor(deps: ResumeQueueDrainerDeps, cfg?: Partial<ResumeQueueDrainerConfig>);
    start(): void;
    stop(): void;
    status(): {
        calmTicks: number;
        breakerOpen: boolean;
        breakerOpenUntil: string | null;
        consecutiveFailures: number;
        lastTickAt: string | null;
    };
    private lastTickAtIso;
    /**
     * One drainer tick. `skipCalmTicks` is the manual-drain lever (R2.10) — it
     * skips the calm-ticks requirement ONLY; every other gate still applies.
     */
    tick(opts?: {
        skipCalmTicks?: boolean;
    }): Promise<{
        resumed: boolean;
        blocked?: string;
        invalidated?: number;
    }>;
    /**
     * Paused-queue handling at the tick chokepoint (LIVE queues only — the caller
     * guards `!isDryRun()`). Returns `true` IFF Layer 2 auto-resumed a stale
     * emergency pause (the caller then falls through to normal draining); `false`
     * keeps the pause (the caller early-returns `blocked:'paused'`).
     *
     * Layer 1 (signal-only): raise ONE deduped `paused-waiting` notice when the
     * queue is paused with ≥1 waiting entry — at most once per (pause episode ×
     * waiting-count) so a NEW pause or a GROWING backlog re-alerts, but a steady
     * pause does not drip every tick.
     *
     * Layer 2 (bounded behavior change): auto-resume a STALE emergency/sentinel
     * pause (see staleness predicate) so a blunt stop on one topic never strands
     * unrelated, later-queued active-run revivals forever.
     */
    private handlePausedQueue;
    /**
     * Staleness predicate (spec §"Layer 2"). ALL must hold:
     *  - the pause reason is a blunt emergency/sentinel stop (closed-world
     *    predicate — never the deliberate `autonomous stop-all` pause);
     *  - a waiting entry's reason === AGE_LIMIT_ACTIVE_RUN_REASON (the operator
     *    has a live autonomous run they want continued);
     *  - that entry's queuedAt is STRICTLY MORE than the threshold AFTER pausedAt
     *    (the work the pause now blocks was queued long after the stop).
     * A missing/unparseable timestamp resolves to the SAFE side (NOT stale — the
     * pause stays), so a malformed clock can only keep the pause, never clear it.
     */
    private isStaleEmergencyPause;
    private safeTier;
    private gateBlock;
    /** Hard invariants (argv/scheduler protection). Returns the violation or null. */
    private corruptEntryReason;
    /** R2.6 — the seven reality validations. Returns invalidation reason or null. */
    private validateReality;
    /** A validation dep that throws resolves to the SAFE side (no spawn). */
    private safeBool;
    private safeVal;
    private safeVerify;
    /**
     * R2.8: the continuation prompt treats entry fields as DATA — `reason`
     * length-capped and delimited as literal text; `workEvidence` enum names only.
     */
    continuationPrompt(entry: ResumeQueueEntry): string;
    private runTier1;
    private foldInvalidated;
    private foldGaveUp;
}
//# sourceMappingURL=ResumeQueueDrainer.d.ts.map