/**
 * QueueDrainLoop — the policy engine over PendingInboundStore (Durable Inbound
 * Message Queue spec §2–§4, §6). Owns:
 *
 *  - the live ENQUEUE side (the router's `queueMessage` dep target): lease-
 *    gated, dry-run-aware, negative-cached, mirror-maintained custody taking;
 *  - the DRAIN side: head-only selection, hold verdicts, claims, dispatch via
 *    the injected `dispatchInbound` seam with the §3.4 receipt handover
 *    contract, disposition mapping, per-entry error isolation, backoff,
 *    maxAttempts forced re-place;
 *  - the backstop tick (Eternal Sentinel, declared §3.2) + event triggers;
 *  - operator halt (§3.6): stop transitions custody + PIS cleanup; pause
 *    freezes queued rows only (cumulative cap);
 *  - clock discipline (§6): sleep-shift on wake, nap clamp.
 *
 * Everything is injected — no timers or I/O of its own beyond the store — so
 * the policy is deterministic and unit-testable. The server wires the real
 * dispatch tail, breaker, registry, and reporting.
 *
 * P19 declaration — Eternal Sentinel (spec §3.2): the backstop tick never
 * gives up, by design. Conditions: (1) declared here; (2) critical-healer role
 * — it is the only thing that drains custody; (3) rate floor = drainTickMs
 * with constant per-tick cost (bounded scans over ≤hardMaxTotal rows);
 * (4) observable — tick failures are episode-latched: one log per episode,
 * one degradation signal after 10 min sustained, recovery logged once.
 */
import type { PendingInboundStore, SenderEnvelope, EnqueueOutcome } from './PendingInboundStore.js';
import type { InboundQueueConfig, HoldForStabilityConfig } from './inboundQueueConfig.js';
export interface DrainMessage {
    sessionKey: string;
    messageId: string;
    payload: string;
    senderEnvelope: SenderEnvelope | null;
    topicMetadata: unknown;
    enqueueSeq: number;
}
/**
 * The ownership-handover contract handed to the dispatch tail. The tail MUST
 * call `commitReceipt()` at the handover point (before the PIS record on the
 * spawn path / before a direct inject) and honor a false return by ABORTING
 * the inject (the §3.6 transactional stop fence fired). After the receipt, it
 * MUST consult `stopRecheck()` immediately before the inject (the round-7
 * receipt→inject gap close) — true means stopped: skip the inject.
 */
export interface DrainHandover {
    commitReceipt(): boolean;
    stopRecheck(): boolean;
}
export type DrainDispatchResult = 
/** forwarded | duplicate | remote spawned | remote owner-dead-replaced. */
{
    kind: 'remote-delivered';
}
/** Local tail completed through the receipt; `injectError` carries a caught
 *  inject failure AFTER the receipt (→ delivered_unconfirmed, §3.4). */
 | {
    kind: 'local-delivered';
    injectError?: string;
}
/** commitReceipt() returned false — a stop transitioned the row mid-flight. */
 | {
    kind: 'handover-refused';
}
/** stopRecheck() fired after the receipt — inject skipped (orphaned receipt
 *  is the named-safe shape; the row already reads operator-stop). */
 | {
    kind: 'stopped-before-inject';
}
/** queued | placement-blocked | spawn-in-progress skip → release+backoff. */
 | {
    kind: 'un-routable';
    reason: string;
}
/** Typed NACK from a peer that re-validated the sender (§3.4 remote). */
 | {
    kind: 'sender-rejected';
} | {
    kind: 'failed';
    error: unknown;
};
export type HoldVerdict = 'hold' | 'failover' | 'deliver';
export interface LossItem {
    sessionKey: string;
    messageId: string;
    enqueuedAt: string;
    reason: string;
    senderDisplay?: string | null;
}
export interface QueueDrainLoopDeps {
    store: PendingInboundStore;
    qcfg: InboundQueueConfig;
    hcfg: HoldForStabilityConfig;
    selfMachineId: string;
    holdsLease(): boolean;
    /** Handoff-in-progress flips the enqueue gate to refused (§3.5 ordered handoff). */
    handoffInProgress?(): boolean;
    /** Per-topic operator-stop state (§3.6) — stop-scoped, never pause. */
    isStopped(sessionKey: string): boolean;
    /** The §3.1 dispatch seam (via:'drain'). Bounded by dispatchDeadlineMs here. */
    dispatchInbound(msg: DrainMessage, handover: DrainHandover): Promise<DrainDispatchResult>;
    /** maxAttempts escape hatch — ONE forced re-place bypassing hold/deliver
     *  verdicts (§3.3). Resolves true when the re-place succeeded. */
    forceReplace(msg: DrainMessage): Promise<boolean>;
    /** §4 hold verdict — required; the server injects always-'failover' when the
     *  policy is off (§4.2). Pure in-memory (breaker + capacity registry). */
    holdVerdict(sessionKey: string): HoldVerdict;
    /** Stop cleanup (§3.6/§3.4): delete the PIS record for a stopped session. */
    clearPisRecord(sessionKey: string): void;
    /** Loss reporting — the dep aggregates into ONE attention item per episode
     *  (§1 copy rules). Never receives payload bytes. */
    reportLoss(items: LossItem[], reason: string): void;
    /** "Possibly not injected — resend if unanswered" (§3.4/§5 windows 4+6). */
    reportPossiblyNotInjected(items: LossItem[]): void;
    log(line: string): void;
    reportDegradation(reason: string): void;
    now(): number;
    /** Monotonic ms (within-boot deadlines, §6). */
    mono(): number;
    bootSessionId: string;
}
export interface DrainPassSummary {
    trigger: string;
    dispatched: number;
    delivered: number;
    unroutable: number;
    failed: number;
    skippedHeld: number;
    skippedStopped: number;
    staleCustodyExpired: number;
    deadlineHit: boolean;
}
export declare class QueueDrainLoop {
    private readonly d;
    /** In-memory session-count mirror (§2.3) — same-code-path updates at every
     *  transition site, boot-rebuilt, read-through on zero. */
    private mirror;
    /** Held-set cache (§3.2) — seqs currently excluded from selection. The
     *  first_held_at COLUMN is authoritative; this is a boot-rebuilt cache. */
    private heldSeqs;
    /** Refusal negative cache keyed on the canonical id (§1). */
    private refusalCache;
    /** Single-flight + rerun (§3.2). */
    private passInFlight;
    private rerunRequested;
    private lastPassEndedMono;
    /** Eternal-Sentinel episode latch (§3.2). */
    private tickFailureSinceMono;
    private tickFailureLogged;
    private tickDegradationSent;
    private holdRecheckLastMono;
    private tickCount;
    /** Current tenure id — refreshed by observeLeaseClaim at lease acquisition. */
    private tenure;
    constructor(deps: QueueDrainLoopDeps);
    private rebuildCaches;
    /** Call at every lease ACQUISITION (not renewal) with the ref-tip holder
     *  observed at claim time. */
    onLeaseAcquired(tipHolderAtClaim: string | null): void;
    currentTenure(): string | null;
    /**
     * Take custody. Returns the tri-state outcome; the router maps
     * `queued`/`already-queued` → acked:true, `refused` → today's fall-through.
     * NEVER throws — a storage failure maps to `refused` (fail-safe direction).
     * This is the caller's LAST fallible step before its outcome return
     * (no-throw-after-commit invariant).
     */
    enqueueLive(msg: {
        sessionKey: string;
        messageId: string;
        payload: string;
        senderEnvelope?: SenderEnvelope | null;
        topicMetadata?: unknown;
    }, reason: string): EnqueueOutcome;
    private pruneRefusalCache;
    /** Mirror with read-through-on-zero (the round-2 honest-consistency contract). */
    hasQueued(sessionKey: string): boolean;
    /** §2.2 route-throw custody check — point read, never the mirror. Throws
     *  propagate so the caller can fail OPEN to fall-through. */
    hasCommittedRow(sessionKey: string, messageId: string): boolean;
    private mirrorAdd;
    private mirrorRemove;
    /** One tick — the server schedules this every drainTickMs. Never throws. */
    tick(): Promise<void>;
    private reconcileMirror;
    /** TTL + stale-custody/nap clamps (§3.5/§6). */
    private expireWithClamps;
    private recheckHolds;
    /** Ownership transition for a session with entries (emitPlacement seam).
     *  Returns the pass promise so callers MAY await (tests do; the server
     *  fire-and-forgets). */
    onOwnershipTransition(sessionKey: string): Promise<DrainPassSummary | null>;
    /** Breaker close — held rows for the recovered owner deliver instantly. */
    onBreakerClose(): Promise<DrainPassSummary | null>;
    onMachineOnline(): Promise<DrainPassSummary | null>;
    runDrainPass(trigger: string): Promise<DrainPassSummary | null>;
    private lossItem;
    private dispatchOne;
    private toMsg;
    private releaseWithBackoff;
    /**
     * Emergency stop for a topic: transition that session's non-terminal rows
     * terminal `expired` reason `operator-stop`, delete the PIS record, ONE
     * loss report ("dropped on your stop command — resend anything still
     * wanted"). The claimed→expired transition is the transactional fence that
     * makes any in-flight conditional receipt write fail atomically.
     */
    onOperatorStop(sessionKey: string): void;
    /** Pause: freeze QUEUED rows only (round-9 pin) — in-flight dispatches
     *  complete normally; the pass/batch consult stops new dispatches. Durable. */
    onPause(): void;
    /** Resume: fold frozen spans, shift deadlines, expire past-cumulative-cap
     *  rows as pause-expired (loss-reported), re-enter via the trigger seam. */
    onResume(): Promise<DrainPassSummary | null>;
    /**
     * Sleep/wake (§6): shift backoff deadlines by the nap span (monotonic
     * deadlines are only reconstructable within one boot session — the shift
     * applies the sleep-shift pattern), then clamp: entries older than
     * `maxNapDeliveryAgeMs` of wall age expire (`nap-stale`, loss-reported) —
     * a 10-minute-old instruction should not fire into a conversation the user
     * has moved past. Low wake-confidence → the conservative branch: clamp
     * WITHOUT deadline-shift (deadlines fire sooner, the clamp still bounds
     * staleness — the fail-safe direction).
     */
    onWake(napMs: number, confidence: 'high' | 'low'): Promise<DrainPassSummary | null>;
    private shiftDeadline;
    /** Bounded top-K per-session depth list (§5.1 heartbeat field; K byte-small
     *  by construction — sessionKey is a topic id string). */
    topKSessionDepths(k: number): Array<{
        sessionKey: string;
        depth: number;
    }>;
    /** Receive-side durable receipt, canonical-id keyed (loss window 6 arm). */
    recordRemoteReceipt(sessionKey: string, messageId: string): boolean;
    /** Flip the injected marker after the local inject completed. */
    markRemoteInjected(sessionKey: string, messageId: string): void;
    /** Peer-side CAUGHT inject failure after receipt-commit (round-9): report at
     *  error time + counter; marker stays unflipped (prune backstop re-covers). */
    reportPeerInjectError(sessionKey: string, messageId: string, error: string): void;
    snapshot(): {
        counts: ReturnType<PendingInboundStore['counts']>;
        counters: Record<string, number>;
        heldSeqs: number;
        paused: boolean;
        tenure: string | null;
    };
}
//# sourceMappingURL=QueueDrainLoop.d.ts.map