/**
 * A2ADeliveryTracker — the "communications never just die out" guarantee for
 * agent-to-agent (Threadline / file-relay) messaging.
 *
 * The problem this closes (operator directive, 2026-06-06): every A2A hop was
 * fire-and-forget. A `threadline_send` that returns `delivered:true` only means
 * the TRANSPORT accepted it — it says nothing about whether the peer ever
 * PROCESSED it. And there was no record on the SENDER side of "this message is
 * still waiting for the peer to acknowledge it", so a peer going dark was
 * invisible until a human noticed silence (Dawn's check-in sat 10h unread; my
 * hosting kickoff was accepted by the relay but never seen).
 *
 * This is the durable spine of the fix:
 *   - recordSent()      — every outbound A2A message is written here BEFORE/with
 *                         the transport attempt (lifecycle starts 'awaiting-ack').
 *   - recordAck()       — a peer's PROCESSED-ack (not transport-ack) flips it to
 *                         'acked'. This is the real delivered signal.
 *   - recordInboundFrom — every accepted inbound message bumps the peer's
 *                         liveness clock (last time we heard FROM them).
 *   - findOverdue()     — awaiting-ack rows past a TTL: the redelivery +
 *                         escalation sentinel's work-list (PR2 layers on this).
 *   - peerHealth()      — "is my channel to <peer> alive?" as a lookup, not a
 *                         guess: last sent, last acked, last heard-from, how many
 *                         messages are stuck awaiting ack, and a stale flag.
 *
 * Substrate is SQLite (the proven MessageProcessingLedger / 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).
 * Per-agent-id isolation. Read-only at the HTTP layer (observability) — it never
 * gates a send.
 */
/** Outbound delivery lifecycle. */
export type A2ADeliveryState = 'awaiting-ack' | 'acked' | 'escalated' | 'failed';
export interface A2ADeliveryEntry {
    messageId: string;
    peerFp: string;
    peerName: string | null;
    threadId: string | null;
    subject: string | null;
    transport: string | null;
    state: A2ADeliveryState;
    sentAt: string;
    ackedAt: string | null;
    attempts: number;
    lastAttemptAt: string | null;
    nextRetryAt: string | null;
    escalatedAt: string | null;
}
export interface PeerHealth {
    peerFp: string;
    peerName: string | null;
    /** Last outbound message we sent to this peer (any state). */
    lastSentAt: string | null;
    /** Last time a message we sent was PROCESSED-acked by the peer. */
    lastAckedAt: string | null;
    /** Last time we ACCEPTED an inbound message from this peer. */
    lastInboundAt: string | null;
    /** Outbound messages still awaiting the peer's ack. */
    pendingCount: number;
    /** Age (ms) of the OLDEST awaiting-ack message; null when none pending. */
    oldestPendingAgeMs: number | null;
    /** Messages that exhausted retries and were escalated. */
    escalatedCount: number;
    /**
     * True when the channel looks unhealthy: a message has been awaiting ack
     * longer than `staleAfterMs`. This is the "is my channel to Dawn alive?"
     * signal — silence made visible.
     */
    stale: boolean;
}
export declare function resolveA2ADeliveryPath(stateDir: string, agentId: string): string;
/** Default: a message awaiting ack longer than this marks the channel stale. */
export declare const DEFAULT_STALE_AFTER_MS: number;
export declare class A2ADeliveryTracker {
    private readonly db;
    readonly path: string;
    private unregister;
    private constructor();
    static open(agentId: string, stateDir: string): A2ADeliveryTracker;
    /** Open an in-memory tracker (tests). */
    static openMemory(): A2ADeliveryTracker;
    /**
     * Record an outbound A2A message. Lifecycle starts 'awaiting-ack'. Idempotent
     * on messageId (INSERT OR IGNORE) so a retry of the SAME message never
     * double-inserts — and never resurrects a row already acked/failed.
     */
    recordSent(opts: {
        messageId: string;
        peerFp: string;
        peerName?: string | null;
        threadId?: string | null;
        subject?: string | null;
        transport?: string | null;
        sentAt?: string;
    }): void;
    /**
     * Record a peer's PROCESSED-ack for a specific outbound message — the real
     * "delivered" signal. Flips 'awaiting-ack'/'escalated' → 'acked'. Idempotent;
     * never downgrades an already-acked row. Returns true if a row was flipped.
     */
    recordAck(messageId: string, ackedAt?: string): boolean;
    /**
     * Implicit ack: a reply ON A THREAD is proof the peer processed our prior send
     * on that thread. Acks the OLDEST awaiting message on the thread. Returns the
     * messageId acked, or null.
     *
     * Keyed on threadId ALONE — deliberately NOT on peer fingerprint. The inbound
     * sender identity differs by transport (a cross-machine relay carries the
     * peer's FINGERPRINT in from.agent; a same-machine local delivery carries the
     * peer's NAME), while the threadId is consistent on both sides of a
     * conversation. Keying on the thread makes the ack robust to that asymmetry —
     * the bug a same-model test suite missed but cross-perspective review caught:
     * outbound rows are keyed by fingerprint, so a name-keyed ack never matched and
     * the implicit ack never fired in production.
     *
     * Count-conservative, not per-message truth: if N messages await on the thread
     * and one reply arrives, only the OLDEST flips to acked (the reply may actually
     * be answering the newest, but acking the oldest never OVER-acks — the genuinely
     * unanswered tail stays pending and can still go stale/escalate).
     */
    recordAckByThread(threadId: string, ackedAt?: string): string | null;
    /** Bump a peer's inbound-liveness clock — call when we ACCEPT a message from them. */
    recordInboundFrom(peerFp: string, peerName: string | null, at?: string): void;
    /** Outbound messages still awaiting the peer's ack (optionally for one peer). */
    pending(peerFp?: string): A2ADeliveryEntry[];
    /**
     * awaiting-ack messages whose last attempt is older than ttlMs — the
     * redelivery/escalation work-list. Sorted oldest-first.
     */
    findOverdue(ttlMs: number, nowMs?: number): A2ADeliveryEntry[];
    /** Record a redelivery attempt: bump attempts, stamp lastAttempt + nextRetry. */
    markAttempt(messageId: string, nextRetryAt?: string, at?: string): void;
    /** Mark a message escalated (retries exhausted, peer dark — operator notified). */
    markEscalated(messageId: string, at?: string): void;
    /** Mark a message permanently failed (no further retries/escalation). */
    markFailed(messageId: string): void;
    /** Single entry by messageId. */
    get(messageId: string): A2ADeliveryEntry | null;
    /** "Is my channel to <peer> alive?" — composed from outbound + inbound records. */
    peerHealth(peerFp: string, opts?: {
        nowMs?: number;
        staleAfterMs?: number;
    }): PeerHealth;
    /** Health for every peer we've sent to or heard from. */
    allPeerHealth(opts?: {
        nowMs?: number;
        staleAfterMs?: number;
    }): PeerHealth[];
    close(): void;
}
//# sourceMappingURL=A2ADeliveryTracker.d.ts.map