/**
 * ThreadLog — the canonical, append-only, hash-chained log, ONE file per thread
 * (Threadline Robustness Phase 2, D-A). The structural fix for F3 (an agent
 * reading "0 messages" on a thread it had just sent 4 messages on).
 *
 * Reuses the `MandateAudit`/`TrustAuditLog` pattern VERBATIM (no new crypto, no
 * new format): each entry embeds the hash of the previous one
 * (`hash = sha256(prevHash + canonical(entry-without-hash))`), append-only JSONL,
 * `verify() → {ok, brokenAt}`. One log file per thread at
 * `{stateDir}/threadline/threads/{threadId}.log.jsonl`.
 *
 * Design points (all from the converged spec):
 *  - Append is IDEMPOTENT on `(threadId, messageId, direction)` via a persisted
 *    per-thread seen-set whose authority is the LIVE LOG itself (rebuilt from the
 *    log on a cold/evicted thread) — NOT a best-effort tail scan (FD-2). A second
 *    append on an existing key whose `contentDigest` DIFFERS is a `collision`
 *    (recorded, never overwritten — a same-id-different-content replay is a
 *    poisoning signal).
 *  - The JSONL append (O(1) `appendFileSync`) is the SOURCE OF TRUTH. `head()`
 *    returns `{count, headHash, setAccum}` from an in-memory running cache that is
 *    REBUILT FROM THE LOG whenever cold — the log always wins.
 *  - `count`/`setAccum` are RETENTION-INDEPENDENT running totals (SI2): rotating
 *    old entries to `archive/` folds them into a base sidecar so the observable
 *    total never changes — asymmetric local rotation between two ends can never
 *    manufacture a false `diverged`.
 *  - `verify()` walks the LIVE segment, anchored at the first live entry's
 *    `prevHash` as the documented chain root (the archived prefix is not walked).
 *    It detects a torn/edited line but not a wholesale self-consistent re-chain —
 *    that residual is caught (partially) by the ConversationStore head stamp and
 *    is named honestly in the spec (the local-FS attacker is already out of scope).
 *
 * Machine-local BY DESIGN under the single-holder model; the per-entry `author`
 * field is the Phase-3 cross-machine-merge seam (recorded, not yet trusted).
 */
export type ThreadDirection = 'outbound' | 'inbound';
/** Who authored THIS leg — the Phase-3 cross-machine-merge seam (recorded, not trusted). */
export interface ThreadLogAuthor {
    agentFingerprint?: string;
    sessionName?: string;
    machineId?: string;
}
export type ThreadTextRef = {
    kind: 'inline';
    text: string;
} | {
    kind: 'store';
    messageStoreId: string;
};
/** A persisted log entry (the full hash-chained shape). */
export interface ThreadLogEntry {
    seq: number;
    threadId: string;
    messageId: string;
    direction: ThreadDirection;
    digestVersion: number;
    contentDigest: string;
    /** Reconstructed from outbox/aggregate; EXCLUDED from the symmetry head (FD-5). */
    backfilled?: true;
    author: ThreadLogAuthor;
    /** The verified peer on the other end of this leg (participant key). */
    peerFingerprint?: string;
    /** Snapshot at append (display only). */
    subject?: string;
    textRef: ThreadTextRef;
    /** The sender-stamped message `createdAt` (the digest input — distinct from `at`). */
    createdAt: string;
    /** ISO APPEND time (when this end logged it) — distinct from the message createdAt. */
    at: string;
    prevHash: string;
    hash: string;
}
/** Caller-supplied fields for an append (seq/at/prevHash/hash are computed here). */
export interface ThreadLogAppendInput {
    threadId: string;
    messageId: string;
    direction: ThreadDirection;
    contentDigest: string;
    digestVersion?: number;
    backfilled?: true;
    author?: ThreadLogAuthor;
    peerFingerprint?: string;
    subject?: string;
    textRef: ThreadTextRef;
    /** The sender-stamped message `createdAt` (the digest input). */
    createdAt: string;
    /** Test/seam injection for the ISO append time. */
    at?: string;
}
export type ThreadLogAppendStatus = 'appended' | 'duplicate' | 'collision';
export interface ThreadLogAppendResult {
    status: ThreadLogAppendStatus;
    entry: ThreadLogEntry | null;
}
export interface ThreadLogHead {
    count: number;
    headHash: string;
    setAccum: string;
}
export interface ThreadLogOptions {
    /** Live-segment size before oldest entries rotate to archive/. */
    maxEntriesPerThread?: number;
    /** Per-thread in-memory seen-set ceiling (memory bound; live log is the authority). */
    seenSetMaxPerThread?: number;
    /** Global LRU ceiling on how many threads' state is held in memory. */
    seenSetMaxThreads?: number;
    now?: () => number;
}
/** The anchored allowlist for a thread id — the real minted shapes only (FD-7). */
export declare const THREAD_ID_RE: RegExp;
export declare class ThreadLog {
    private readonly dir;
    private readonly archiveDir;
    private readonly maxEntriesPerThread;
    private readonly seenSetMaxPerThread;
    private readonly seenSetMaxThreads;
    private readonly now;
    /** Bounded LRU of per-thread in-memory state. The log on disk is the authority. */
    private readonly states;
    constructor(stateDir: string, opts?: ThreadLogOptions);
    private logPath;
    private metaPath;
    private archivePath;
    /** Confirm a resolved log path is inside the threads dir (traversal defense, FD-7). */
    isPathConfined(threadId: string): boolean;
    /**
     * Append one message to the thread's canonical log. Idempotent on
     * `(threadId, messageId, direction)`; first-write-wins. A differing
     * `contentDigest` on an existing key returns `collision` (NOT overwritten). The
     * append is SYNCHRONOUS so two appends in this process cannot interleave the
     * read-head→write window (Node is single-threaded; `appendFileSync` of one
     * sub-PIPE_BUF line is atomic).
     */
    append(input: ThreadLogAppendInput): ThreadLogAppendResult;
    /** True if `(messageId, direction)` is already logged (live-log authority). */
    has(threadId: string, messageId: string, direction: ThreadDirection): boolean;
    /**
     * The authoritative dedup lookup: the in-memory seen cache first, falling back
     * to a LIVE-LOG scan when the cache is incomplete (trimmed). Returns the stored
     * `contentDigest` for the key, or undefined when genuinely unseen. This is what
     * makes idempotency the full-live-log authority rather than a tail window — a
     * duplicate is caught however many entries intervened.
     */
    private lookupSeen;
    private findInLiveLog;
    /**
     * Read the LIVE segment paginated by `seq` cursor. `afterSeq` returns entries
     * with `seq > afterSeq`; O(limit) per page over the bounded live segment.
     */
    read(threadId: string, opts?: {
        limit?: number;
        afterSeq?: number;
    }): {
        entries: ThreadLogEntry[];
        hasMore: boolean;
    };
    /** Head `{count, headHash, setAccum}` — from the in-memory cache, log-rebuilt if cold. */
    head(threadId: string): ThreadLogHead;
    /** The verified peer fingerprints recorded across this thread's live legs. */
    participants(threadId: string): Set<string>;
    /** True if any live leg is `backfilled` (drives the `unverified-backfill` state). */
    hasBackfilledLegs(threadId: string): boolean;
    /**
     * Verify the LIVE segment of the chain, anchored at the first live entry's
     * `prevHash` as the documented chain root (FD-10). Returns `{ok:false, brokenAt}`
     * at the first entry whose recomputed hash or prevHash linkage breaks.
     */
    verify(threadId: string): {
        ok: true;
    } | {
        ok: false;
        brokenAt: number;
    };
    /** All live thread ids that have a log file on disk (for the orphan sweep). */
    listThreadIds(): string[];
    /**
     * Delete a thread's log + archive segments + meta sidecar (close-only
     * retention, driven by the ConversationStore `closed` lifecycle — NEVER on cold
     * LRU eviction; SA5). All deletes route through SafeFsExecutor.
     */
    deleteThread(threadId: string): void;
    /** Read every entry in the live segment (tolerant of a torn trailing line). */
    private readLiveEntries;
    private readBase;
    private writeBase;
    /** Load (or rebuild from the log) the in-memory running state for a thread. */
    private loadState;
    /** Cap the in-memory seen cache; once it evicts a key it is no longer complete. */
    private trimSeen;
    private evictStatesIfNeeded;
    /**
     * Rotate the oldest entries to archive/ when the live segment exceeds the cap.
     * Rotation NEVER changes the observable total `count`/`setAccum` (SI2): the
     * rotated entries are folded into the base sidecar so base + live is invariant.
     */
    private rotateIfNeeded;
    private nextArchiveSegment;
    /** Keep the archive file-count bounded (oldest rotated segment reclaimed). */
    private reclaimOldArchive;
}
//# sourceMappingURL=ThreadLog.d.ts.map