/**
 * CorrectionCaptureLoop — the hot-path capture → distill → ledger hop (spec §3.1/§3.3).
 *
 * VOID fire-and-forget: `classify()` (Layer 0) is SYNC and runs on the delivery
 * seam; this module's `captureAndDistill()` is invoked with `void` so the async
 * distill NEVER blocks message delivery and a thrown distill error NEVER
 * propagates back (the HumanAsDetectorLog fail-open contract must hold).
 *
 * Privacy (spec §3.3 — both-sided deterministic scrub):
 *   - per-topic look-back ring, hard-capped at captureContextTurns, held in a
 *     topic-map that is LRU/TTL-evicted. NEVER serialized into /health.
 *   - PRE-SCRUB: every captured turn is scrubSecrets()'d BEFORE it enters the
 *     distill prompt (the egress boundary).
 *   - POST-SCRUB: the LLM's learning + scrubbed_summary are scrubSecrets()'d
 *     AGAIN before they touch the CorrectionLedger.
 *
 * Prompt-injection hardening (spec §3.3): captured turns are delimited as
 * untrusted data; the model is told never to follow instructions inside; each
 * turn is marked fromUser; the learning must be derived from a USER turn only.
 * `kind` is enum-validated (the LLM cannot widen it); llm_confidence is advisory.
 *
 * LlmQueue discipline (spec §3.1): every enqueue() is wrapped in try/catch for
 * ALL FOUR capacity throw paths (daily-cap, reserve-breach, LlmAbortedError,
 * breaker-open). Previously these DROPPED the capture silently. Now, when a
 * bounded durable backlog is wired (default-on when the feature is enabled), a
 * capacity throw PERSISTS the already-pre-scrubbed capture to the backlog so a
 * later headroom window can distill it ('distill-backlogged'). A non-capacity
 * throw (e.g. a malformed provider error) still drops ('distill-dropped'), and
 * if the backlog enqueue itself fails the capture falls back to the old drop —
 * the fire-and-forget seam never throws regardless.
 */
import type { CorrectionLedger, CorrectionKind } from './CorrectionLedger.js';
import type { CorrectionCaptureBacklog } from './CorrectionCaptureBacklog.js';
export interface CaptureTurn {
    text: string;
    fromUser: boolean;
    /** epoch ms — for TTL eviction + ordering. */
    at: number;
}
export interface CaptureRingOptions {
    /** Per-topic ring depth (drop-oldest on push). */
    captureContextTurns: number;
    /** Max distinct topics held before LRU eviction. */
    captureTopicMapMax: number;
    /** Idle TTL (ms) before a topic's ring is evicted. */
    topicTtlMs: number;
    now?: () => number;
}
/**
 * A per-topic look-back ring. Bounded two ways: each topic's ring is capped at
 * `captureContextTurns`; the topic map is LRU/TTL-evicted at `captureTopicMapMax`
 * / `topicTtlMs`. NEVER serialized — there is no toJSON and `server.ts` never
 * puts this instance anywhere /health can reach (an integration test pins that
 * /health's response shape contains no captured text).
 */
export declare class CaptureRing {
    private readonly rings;
    private readonly opts;
    constructor(options: CaptureRingOptions);
    /** Push a turn onto the topic's ring and return the current window (oldest-first). */
    push(topicId: number, turn: CaptureTurn): CaptureTurn[];
    /** Current window for a topic (oldest-first); empty if none / evicted. */
    window(topicId: number): CaptureTurn[];
    /** Number of topics currently held (observability/test). */
    topicCount(): number;
    private evictExpired;
}
/** The strict JSON envelope the distiller must return. */
export interface DistillEnvelope {
    learning: string;
    kind: CorrectionKind;
    llm_confidence: number;
    scrubbed_summary: string;
}
/**
 * Build the distillation prompt. Turns are PRE-SCRUBBED here (the egress
 * boundary), delimited as untrusted data, and the model is instructed never to
 * follow instructions inside the block + to derive the learning from a USER
 * turn only. Returns the prompt string.
 */
export declare function buildDistillPrompt(turns: CaptureTurn[]): string;
/**
 * Parse + validate the distiller's response into a DistillEnvelope. Returns null
 * on any malformed/invalid output. `kind` is validated against the allow-list
 * (anything else → coerced to 'noise'); llm_confidence clamped to [0,1]. The
 * learning + scrubbed_summary are POST-SCRUBBED here (the deterministic guarantee
 * over the LLM's best-effort scrub).
 */
export declare function parseDistillEnvelope(raw: string): DistillEnvelope | null;
/**
 * Is this distill rejection a rate-limit / capacity / backpressure condition
 * (i.e. "the LLM is unavailable RIGHT NOW, try later") rather than a genuine
 * fault (malformed output, programmer error)? Only capacity throws are worth
 * backlogging — a later headroom window can succeed where they failed. The four
 * shapes the per-sentinel distill path can throw:
 *   - LlmQueue daily-cap:       Error('LLM daily spend cap exceeded')
 *   - LlmQueue reserve breach:  Error('...breach interactive reserve')
 *   - LlmQueue abort:           LlmAbortedError (name === 'LlmAbortedError')
 *   - circuit breaker open:     LlmCircuitOpenError (`circuitOpen === true`)
 * Plus a defensive net for provider rate-limit strings (429 / quota / "rate
 * limit"). Conservative: an unrecognized error is NOT treated as capacity (it
 * drops, preserving the old behavior for genuine faults).
 */
export declare function isCapacityThrow(err: unknown): boolean;
export type DistillFn = (prompt: string) => Promise<string>;
export interface CaptureAndDistillDeps {
    ring: CaptureRing;
    ledger: CorrectionLedger;
    /** Routes the distill prompt through the sentinel's OWN LlmQueue. Throws on
     *  cap / reserve / abort — the caller catches ALL THREE and drops silently. */
    distill: DistillFn;
    /** Skip capture under sustained quota pressure (load-shedding). */
    shouldShed?: () => boolean;
    /** Per-topic distill rate ceiling. */
    rateCeiling?: {
        maxPerWindow: number;
        windowMs: number;
    };
    /** Durable retry backlog. When wired, a CAPACITY distill throw persists the
     *  pre-scrubbed capture here ('distill-backlogged') instead of dropping it, so
     *  a later headroom window can distill it. null/undefined → old drop behavior. */
    backlog?: CorrectionCaptureBacklog | null;
    /** Audit sink — one structured line per capture decision. Never throws. */
    audit?: (event: {
        decision: string;
        topicId: number | null;
        detail?: string;
    }) => void;
    now?: () => number;
}
export type CaptureDecision = 'recorded' | 'noise' | 'no-signal' | 'no-topic' | 'shed' | 'rate-limited' | 'distill-dropped' | 'distill-backlogged' | 'distill-malformed' | 'error';
/** Per-topic rate-limit state, owned by the loop wiring (persists across turns). */
export interface CaptureRateState {
    attempts: Map<number, number[]>;
}
export declare function makeCaptureRateState(): CaptureRateState;
/**
 * Capture one inbound turn and (if it carries a learning signal) distill it
 * off the delivery path. ALWAYS returns a CaptureDecision; NEVER throws. The
 * three LlmQueue throw paths are caught here and become a silent 'distill-dropped'.
 *
 * @param signalDeterministicWeight  Layer-0 total weight for THIS message (the
 *   code-determined provenance recorded with the occurrence). 0 when the message
 *   was not itself a learning signal but is being captured as context.
 * @param isLearningSignal  Whether THIS message was a preference/frustration
 *   signal (only signal-bearing messages trigger a distill).
 */
export declare function captureAndDistill(deps: CaptureAndDistillDeps, input: {
    topicId: number | null;
    text: string;
    fromUser: boolean;
    sessionId?: string | null;
    deterministicWeight: number;
    isLearningSignal: boolean;
}, rateState?: CaptureRateState): Promise<CaptureDecision>;
export interface DrainBacklogDeps {
    backlog: CorrectionCaptureBacklog;
    ledger: CorrectionLedger;
    distill: DistillFn;
    /** Pure, NON-mutating "is the LLM available right now?" gate (breaker closed +
     *  not load-shedding). The drainer NEVER runs while this is false — draining
     *  into an open breaker would just re-fail every entry. Wire to
     *  llmCircuitAvailable() (which does not consume a half-open probe slot). */
    llmAvailable: () => boolean;
    /** TTL (ms) — entries older than this are pruned before draining. */
    ttlMs?: number;
    /** Audit sink — one structured line per drain decision. Never throws. */
    audit?: (event: {
        decision: string;
        topicId: number | null;
        detail?: string;
    }) => void;
}
export interface DrainResult {
    /** Why the drain stopped without processing (when applicable). */
    skipped?: 'breaker-open' | 'empty';
    claimed: number;
    distilled: number;
    recorded: number;
    failed: number;
    dropped: number;
    pruned: number;
}
/**
 * Opportunistically distill up to `batchSize` backlogged captures into the
 * CorrectionLedger. OFF-HOT-PATH (call via void/async, NEVER on the message
 * seam). Bounded (batchSize), fail-open (a per-entry fault bumps that entry, a
 * top-level fault returns a safe result), and SKIPPED when the LLM is
 * unavailable (breaker open / shedding) — draining into an open breaker would
 * only re-trip it and waste the entries' retry budget.
 *
 * Per entry: build the SAME distill prompt (turns are already scrubbed on
 * persist; buildDistillPrompt re-scrubs harmlessly), parse the envelope, and —
 * on a real, non-noise learning — record it to the ledger then DELETE the
 * backlog row (markDistilled). A capacity throw OR a malformed/failed distill
 * bumps the entry's attempt count (dropped once it exceeds maxRetries). A 'noise'
 * verdict is terminal: the row is deleted (markDistilled) — it carried no lesson.
 */
export declare function drainBacklog(deps: DrainBacklogDeps, batchSize: number): Promise<DrainResult>;
//# sourceMappingURL=CorrectionCaptureLoop.d.ts.map