/**
 * MessageProcessingLedger — the no-loss / no-duplicate-reply guarantee (spec §8 G3a).
 *
 * A durable, SQLite-backed record of each inbound message's lifecycle:
 *   received → processing → reply_committed → cursor_advanced
 * keyed by the adapter's `dedupeKey(rawEvent)` (Telegram update_id, Slack
 * event_id/client_msg_id). Rules:
 *
 *  - An event whose dedupeKey is already reply_committed/cursor_advanced is
 *    NEVER acted on again — a redelivery (Telegram retry, Slack reconnect, or a
 *    transfer-window overlap during a handoff) is recognized and dropped. This
 *    is what makes "no duplicate replies" structural, not merely "only one
 *    consumer".
 *  - The ingress cursor advances ONLY on durable completion (cursor_advanced),
 *    so a crash before completion replays the event (at-least-once) and the
 *    ledger makes the replay a no-op-or-resume (exactly-once effect).
 *  - Each transition is flushed to local SQLite synchronously on commit (WAL +
 *    NORMAL), so a same-machine crash-restart never double-acts.
 *  - A stuck `processing` entry past maxProcessingMs (old holder fenced
 *    mid-turn) is re-runnable by the current holder from its stored input.
 *
 * Substrate is SQLite (the proven PendingRelayStore/CommitmentTracker path) —
 * NOT a new ad-hoc JSON file and NOT a git-synced blob. Schema self-initializes
 * on first access (no PostUpdateMigrator step needed). Per-agent-id isolation.
 */
export type LedgerState = 'received' | 'processing' | 'reply_committed' | 'cursor_advanced' | 'abandoned';
export interface LedgerEntry {
    dedupeKey: string;
    platform: string;
    topic: string | null;
    state: LedgerState;
    receivedAt: string;
    processingStartedAt: string | null;
    replyCommittedAt: string | null;
    cursorAdvancedAt: string | null;
    /** When this entry was terminally abandoned (stuck-recovery exhausted its
     *  re-run budget without a reply). Set iff state === 'abandoned'. */
    abandonedAt: string | null;
    replyIdempotencyKey: string | null;
    replyEpoch: number | null;
    inputSnapshot: string | null;
    attempts: number;
    /**
     * The inbound sender, captured at ingress so a stuck-recovery re-run replays
     * the message AS the real sender — not "from Unknown" (the 2026-06-07
     * identity-loss bug). JSON `{ userId, username?, firstName? }` or null.
     */
    senderEnvelope: SenderEnvelope | null;
}
/** Inbound sender identity, stored so a replay preserves "Know Your Principal". */
export interface SenderEnvelope {
    userId?: string | number;
    username?: string;
    firstName?: string;
}
export declare function resolveMessageLedgerPath(stateDir: string, agentId: string): string;
/**
 * Deterministic outbound idempotency key — any machine re-running the same
 * inbound event reproduces it identically (spec §8 G3a). Used so a failover
 * re-send is recognized as the same reply.
 */
export declare function computeReplyIdempotencyKey(dedupeKey: string, replyIndex: number): string;
export declare class MessageProcessingLedger {
    private readonly db;
    readonly path: string;
    private constructor();
    static open(agentId: string, stateDir: string): MessageProcessingLedger;
    /** Open an in-memory ledger (tests). */
    static openMemory(): MessageProcessingLedger;
    /**
     * Record an inbound event. Idempotent on dedupeKey (INSERT OR IGNORE).
     * Returns whether this is the first time we've seen it + the current state.
     * A caller seeing firstSeen:false with an acted-on state must DROP the event.
     */
    record(dedupeKey: string, opts: {
        platform: string;
        topic?: string | null;
        input?: string;
        sender?: SenderEnvelope | null;
    }): {
        firstSeen: boolean;
        state: LedgerState;
    };
    /** Has this event already been acted on (reply committed, cursor advanced, or
     *  terminally abandoned)? An 'abandoned' entry is terminal — a provider
     *  redelivery of the SAME event is dropped (we gave up on it; a genuine resend
     *  arrives with a fresh dedupeKey). */
    isActedOn(dedupeKey: string): boolean;
    /**
     * Claim an event for processing under the given fencing epoch. Returns false
     * if it has already been acted on (drop). A stuck 'processing' entry can be
     * re-claimed (the old holder was fenced); attempts is incremented.
     */
    beginProcessing(dedupeKey: string, epoch: number): boolean;
    /**
     * Commit that a reply was sent. Records the deterministic idempotency key and
     * the fencing epoch it was committed under. Idempotent — committing twice is
     * a no-op (the marker already exists).
     */
    commitReply(dedupeKey: string, replyIdempotencyKey: string, epoch: number): void;
    /** Advance the ingress cursor — only call after durable reply commit. */
    advanceCursor(dedupeKey: string): void;
    /**
     * Terminally abandon a stuck 'processing' entry whose re-run budget is exhausted
     * (stuck-recovery gave up). Moves it OUT of 'processing' so `reclaimStuck` stops
     * re-selecting it every cycle (the give-up log-loop), WITHOUT setting
     * `reply_committed_at` — so it never masquerades as a real reply in
     * `hasReplyCommittedForTopicSince`. Terminal: `beginProcessing`/`isActedOn` treat
     * it as acted-on, so a provider redelivery is dropped (a genuine resend has a
     * fresh dedupeKey). The caller is expected to surface a "I didn't get to this"
     * loss notice so the abandonment is never silent. No-op unless still 'processing'.
     */
    markAbandoned(dedupeKey: string, epoch: number): void;
    /**
     * Apply a reply_committed marker propagated from another machine (dual-medium
     * marker, spec §8 G3a): if we have the entry and it's not yet acted on, mark
     * it committed so a failover does not re-send. Creates the entry if unknown
     * (the other machine saw it first).
     */
    applyRemoteReplyMarker(dedupeKey: string, opts: {
        platform: string;
        replyIdempotencyKey: string;
        epoch: number;
        topic?: string | null;
    }): void;
    /**
     * Entries stuck in 'processing' past maxProcessingMs — eligible for re-run by
     * the current lease holder from their stored input (spec §8 G3a). The old
     * holder's abandoned output is discarded (the new holder re-executes).
     */
    reclaimStuck(maxProcessingMs: number, nowMs?: number): LedgerEntry[];
    /**
     * Reply-evidence check (the no-DUPLICATE-re-run half of stuck recovery): has
     * ANY inbound on this topic been reply_committed at/after `sinceISO`? If so the
     * agent already answered this topic since the stuck entry arrived, so re-running
     * it would re-deliver an already-handled message (the 2026-06-07 every-~10-min
     * "from Unknown" replay loop). Durable across restarts (reads the ledger, not
     * in-memory state). Cheap: indexed on (topic, reply_committed_at).
     */
    hasReplyCommittedForTopicSince(topic: string, sinceISO: string): boolean;
    get(dedupeKey: string): LedgerEntry | null;
    close(): void;
}
//# sourceMappingURL=MessageProcessingLedger.d.ts.map