/**
 * ConversationStore — the single source of truth for a Threadline conversation.
 *
 * Phase 1 of the Threadline re-assessment (THREADLINE-CONVERSATION-KEYSTONE-SPEC.md)
 * introduced this as a server-process in-memory store. Phase 2a
 * (THREADLINE-SINGLE-STORE-SPEC.md / CMT-497) makes it **cross-process safe** so it
 * can be the ONE authoritative store and the legacy `ThreadResumeMap` can become a
 * view over it.
 *
 * Concurrency (Phase 2a): `conversations.json` on disk is the source of truth.
 * Every write — async `mutate()` and sync `mutateSync()` — does reload-per-op +
 * optimistic per-record version CAS + atomic tmp-write+rename, the same
 * cross-process-safe pattern the legacy maps used (reload-per-op) hardened with a
 * `version` token so a same-thread race loses no update (a strict improvement over
 * the legacy last-writer-wins). A write reads the LATEST full file immediately
 * before committing and rewrites it with only its own record changed, so a
 * concurrent write to a DIFFERENT thread from another process is preserved — the
 * only residual is the microsecond window between that final re-read and the
 * atomic rename, which matches the (accepted, never-observed-as-a-problem) legacy
 * full-file-rewrite behavior. Reads use a very-short-TTL snapshot cache so the
 * loop gate's hot path doesn't re-read on every call; staleness only ever causes a
 * redundant reload on the next mutate (which always re-reads), never a lost write.
 *
 * Storage: {stateDir}/threadline/conversations.json. NOT relay-hosted —
 * authoritative state stays local. The append-only SharedStateLedger remains the
 * audit trail; this is the live mutable state it logs transitions from.
 */
import type { NegotiatorLease, LeaseOwner, LeaseResult } from './NegotiatorLease.js';
/**
 * Conversation lifecycle state. Superset of the legacy `ThreadState`
 * (`active|idle|resolved|failed|archived`) plus `open` (created, not yet
 * worked) and `awaiting-reply` (we replied, waiting on the peer). `idle` is
 * also the warrants-a-reply gate's "suppressed, do not spawn" terminal-ish
 * state.
 */
export type ConversationState = 'open' | 'active' | 'idle' | 'awaiting-reply' | 'resolved' | 'failed' | 'archived';
/**
 * The durable conversation record. EXHAUSTIVE against the legacy stores — an
 * incomplete field list silently drops live data on migration (convergence
 * finding). Every field the four legacy stores held is preserved here.
 */
export interface Conversation {
    /** Primary key — the Threadline thread id. */
    threadId: string;
    /** Monotonic version for optimistic CAS in mutate(). */
    version: number;
    /** Conversation participants — self + peer fingerprint(s). */
    participants: {
        /** This agent's name/fingerprint (best-effort). */
        self?: string;
        /** Remote peer fingerprint(s). */
        peers: string[];
    };
    /** Convenience display handle for the primary remote peer. */
    remoteAgent?: string;
    /** Lifecycle state. */
    state: ConversationState;
    /** When state became 'resolved' (grace-period clock). */
    resolvedAt?: string;
    /** The Claude/Codex session UUID the resume primitive depends on. */
    sessionUuid?: string;
    /** The live (or last-known) tmux session bound to this conversation. */
    boundSessionName?: string;
    /** Owning Telegram topic (formerly originTopicId). */
    boundTopicId?: number;
    /** Originating topic-session name at send time (fast-path resume cache). */
    originSessionName?: string;
    /** Spawn mode of the worker handling this conversation. */
    spawnMode?: 'interactive' | 'pipe';
    /** Thread subject. */
    subject?: string;
    /** A2A contextId mapped to this thread, if any. */
    contextId?: string;
    /**
     * The authenticated agent identity that owns the contextId binding. Dropping
     * this reopens the session-smuggling vector ContextThreadMap defends.
     */
    agentIdentity?: string;
    pinned: boolean;
    messageCount: number;
    machineOrigin?: string;
    migratedTo?: string;
    migrateFrom?: string;
    turnCount: number;
    lastInboundHash?: string;
    lastOutboundHash?: string;
    createdAt: string;
    savedAt: string;
    lastActivityAt: string;
    /** Snapshot of the unified-trust level at last contact. */
    trustLevel?: string;
    /** Snapshot of the MoltBridge IQS band at last contact. */
    iqsBand?: string;
    /** The single-negotiator lease — names the one session that owns the voice. */
    negotiatorLease?: NegotiatorLease;
    /** Durable per-epoch holding-notice rate limit (FD-3). */
    lastHoldingNoticeEpoch?: number;
    /** Durable global min-interval floor for holding notices (FD-3). */
    lastHoldingNoticeAt?: string;
    /** Cached count of NON-backfilled canonical-log entries (symmetry `count`). */
    historyCount?: number;
    /** Cached canonical-log chain head hash. */
    historyHeadHash?: string;
    /** Cached order-independent symmetry accumulator (64-hex). */
    historySetAccum?: string;
    /** Saturating count of same-id-different-content collisions (FD-2, anti-amplify). */
    collisionCount?: number;
    canonicalBinding?: {
        peerPrincipal: string;
        workstreamKey: string;
    };
    /** Last symmetry health computed for this thread (closed set; advisory). */
    symmetryState?: string;
    /** Last peer-reported threadSync (verified-participant, monotonic). */
    peerThreadSync?: {
        digestVersion: number;
        count: number;
        setAccum: string;
        at: string;
    };
    /** True once a one-time bounded backfill has run for this thread (memoized). */
    backfilled?: boolean;
}
/** Mutation function: receives a draft clone, returns the next record. */
export type ConversationMutateFn = (draft: Conversation) => Conversation | Promise<Conversation>;
export declare class ConversationStore {
    /** P3 §3.1 — injected coherence-journal emitter (undefined = dark). */
    private journalSeam?;
    /**
     * Robustness Phase 2 (SA5/E1) — injected post-commit canonical-log retention
     * seam (undefined = dark). Fired ONLY when a NON-pinned conversation transitions
     * INTO a genuine terminal close (resolved/failed) — NOT on cold archive/LRU
     * eviction. The server wires this to `ThreadLog.deleteThread` (SafeFsExecutor).
     */
    private logRetentionSeam?;
    private filePath;
    /** Per-threadId FIFO mutate queues — serialize same-process concurrent writers. */
    private mutateQueues;
    private mutateRunning;
    /** Ephemeral verified-only peer-affinity hints (peerFingerprint → hint). */
    private affinity;
    /** Short-TTL read snapshot cache (disk is the source of truth). */
    private cache;
    private cacheAt;
    constructor(stateDir: string);
    /** A cached snapshot of the on-disk file (reloaded after READ_CACHE_TTL_MS). */
    private snapshot;
    private invalidateCache;
    /** Get a conversation by threadId. Returns null if missing or TTL-expired. */
    get(threadId: string): Conversation | null;
    /** True if a (non-expired) conversation exists for the threadId. */
    has(threadId: string): boolean;
    /** Find conversations whose peer set includes the given fingerprint/name. */
    getByParticipant(participant: string): Conversation[];
    /** Find the conversation bound to a Telegram topic, if any. */
    getByTopicId(topicId: number): Conversation | null;
    /** Reverse lookup: conversation owning an A2A contextId (identity-bound). */
    getByContextId(contextId: string, agentIdentity: string): Conversation | null;
    /** List active/idle conversations (not resolved/failed/archived). */
    listActive(): Conversation[];
    /** All non-expired (or pinned) conversations, any lifecycle state. */
    all(): Conversation[];
    /** Total stored conversations (for monitoring). */
    size(): number;
    /**
     * Single-writer mutate surface. Every write path routes through here so
     * concurrent writers (the inbound funnel gate, the router resume/spawn, the
     * relay-send binding stamp) can't clobber each other.
     *
     * Contract (mirrors CommitmentTracker.mutate):
     *  - FIFO queue per threadId, max depth 256.
     *  - Optimistic CAS on `version`: read → fn(clone) → write iff version
     *    unchanged, else retry (max 5). On success version is incremented and
     *    the store is persisted atomically.
     *  - If the conversation does not exist yet, the fn receives a fresh skeleton
     *    (state 'open', version 0) so callers can upsert in one call.
     */
    mutate(threadId: string, fn: ConversationMutateFn): Promise<Conversation>;
    private drainMutateQueue;
    private applyMutationWithCAS;
    /**
     * Synchronous single-record mutate (Phase 2a). Used by the synchronous legacy
     * interfaces (the `ThreadResumeMap` view's `save`/`remove`/etc.). Same file
     * version-CAS as `mutate`, but the fn is synchronous so the re-read→write
     * window has no await and is atomic within this process. Cross-process safety
     * is identical to `mutate` (per-record version CAS + atomic rename).
     *
     * The fn MAY return null to signal "delete this record".
     */
    mutateSync(threadId: string, fn: (draft: Conversation) => Conversation | null): Conversation | null;
    /** Commit a record into the freshly-read file with version+1 (LOAD-BEARING:
     *  both async and sync commit paths bump version so the CAS detects races). */
    private commit;
    /**
     * P3 §3.1 — derive the content-free lifecycle action from a prev/next
     * transition diff on `state` + `boundTopicId` ONLY (a message append /
     * lastActivity bump / unread write changes neither and emits NOTHING).
     * Never throws into the write path (observability never endangers the
     * observed operation).
     */
    private emitLifecycleDiff;
    /** P3 §3.1 — inject the coherence-journal emitter (server wiring). */
    setCoherenceJournalSeam(seam: (data: {
        action: 'started' | 'bound' | 'unbound' | 'closed';
        conversationId: string;
        peerFingerprint: string;
        topicId?: number;
    }) => void): void;
    /**
     * Robustness Phase 2 (SA5/E1) — inject the post-commit canonical-log retention
     * seam (server wires it to `ThreadLog.deleteThread`). Fires ONLY on a non-pinned
     * conversation's transition INTO resolved/failed — NOT on cold archive/LRU.
     */
    setLogRetentionSeam(seam: (threadId: string) => void): void;
    /**
     * Robustness Phase 2 — fire the canonical-log retention seam when a non-pinned
     * conversation transitions INTO a genuine terminal close (resolved/failed). A
     * POST-COMMIT action (the write has already landed), never mid-mutate, so a CAS
     * rollback can't strand a record without its log. Never throws into the write.
     */
    private fireLogRetention;
    /**
     * Direct upsert of a full record. Used by migration / bulk import. Now does a
     * read-merge-write so it is safe even if the file changed since construction.
     */
    importDirect(conversation: Conversation): void;
    /** Holds an in-flight importDirect batch until flush(). */
    private pendingFile;
    /** Persist after a batch of importDirect calls. */
    flush(): void;
    /** Remove a conversation. */
    remove(threadId: string): void;
    /**
     * Move stale non-terminal conversations out of the active set without
     * deleting them. Archived records keep history/resume metadata; a later peer
     * reply can still reactivate the thread through the normal resume path.
     */
    retireInactive(maxInactiveMs?: number, now?: Date): number;
    /**
     * Acquire-or-renew the negotiator lease for a thread (D-A). One synchronous
     * CAS transaction over `mutate()` — no background timers (FD-6). Decision:
     *  - no lease / expired / owner provably dead → acquire (epoch += 1).
     *  - already owned by the caller (and not expired) → renew (epoch unchanged).
     *  - a live, unexpired FOREIGN lease → held (caller does NOT own the voice).
     *
     * `isOwnerLive(sessionName)` lets the caller fence a dead owner: a foreign
     * lease whose owner session is absent from the live session registry is
     * reclaimable even before its TTL expires. Never reads identity from a
     * message body — the owner is always the server-authoritative live session.
     */
    acquireOrRenewLease(threadId: string, owner: LeaseOwner, opts: {
        ttlMs: number;
        now?: number;
        isOwnerLive?: (sessionName: string) => boolean;
    }): Promise<LeaseResult>;
    /** Read the current lease for a thread (no TTL filtering — lease may be stale). */
    readLease(threadId: string): NegotiatorLease | null;
    /**
     * Stamp the durable holding-notice rate-limit fields (FD-3). Called by the
     * gate when it decides to emit a notice, so the per-epoch + min-interval
     * limits survive restarts.
     */
    recordHoldingNotice(threadId: string, epoch: number, now?: number): Promise<void>;
    /**
     * Record a short-lived, verified-only affinity hint (peer → most-recent
     * thread). Deliberately NOT persisted (accepted loss on restart). Callers
     * MUST only call this for VERIFIED peers — an unverified affinity would be a
     * hijack vector.
     */
    recordAffinity(peerFingerprint: string, threadId: string): void;
    /** Look up a fresh affinity hint, honoring the sliding + absolute windows. */
    getAffinity(peerFingerprint: string): string | null;
    /**
     * Stamp the COALESCED canonical-log head cache (count/headHash/setAccum). The
     * JSONL log is the source of truth; this is a best-effort cache the funnel
     * refreshes on a debounced cadence — NEVER a synchronous per-message CAS. A
     * READ never calls this (read-never-writes, SI1).
     */
    stampHistoryHead(threadId: string, head: {
        count: number;
        headHash: string;
        setAccum: string;
    }): Promise<void>;
    /** Record the memoized one-time backfill marker for a thread (D-C). */
    stampBackfilled(threadId: string, value: boolean): Promise<void>;
    /** Stamp the last computed symmetry state + (optionally) the peer threadSync (D-D). */
    stampSymmetry(threadId: string, symmetryState: string, peerThreadSync?: {
        digestVersion: number;
        count: number;
        setAccum: string;
        at: string;
    }): Promise<void>;
    /**
     * Bump the SATURATING collision counter (FD-2). Saturates at a ceiling so an
     * endless same-id-different-content replay cannot write-amplify conversations.json.
     */
    recordCollision(threadId: string): Promise<void>;
    /**
     * Resolve the canonical thread for a verified `(peerPrincipal, workstreamKey)`
     * group, or null if none is bound yet. Returns `{ kind:'found', threadId }`,
     * `{ kind:'none' }`, or `{ kind:'lookup-failed' }` (a transient read error). The
     * caller MUST distinguish lookup-failed from none — a lookup failure observes/
     * retries; it does NOT mint a fresh canonical (avoids an under-load F5
     * regression). `peerPrincipal` MUST be the VERIFIED peer fingerprint.
     */
    resolveCanonicalThread(peerPrincipal: string, workstreamKey: string): {
        kind: 'found';
        threadId: string;
    } | {
        kind: 'none';
    } | {
        kind: 'lookup-failed';
    };
    /**
     * Bind a thread as the canonical thread for a verified `(peerPrincipal,
     * workstreamKey)` group. First-write-wins: if a DIFFERENT thread already holds
     * the binding for that key, this is a no-op (the existing canonical stands).
     */
    bindCanonicalThread(threadId: string, peerPrincipal: string, workstreamKey: string): Promise<void>;
    /** Prune expired + resolved-past-grace + LRU-overflow (non-pinned). */
    prune(): void;
    private skeleton;
    private isExpired;
    /** Prune a conversations map in place (non-pinned expired + LRU overflow). */
    private pruneMapInPlace;
    /** Read + parse the on-disk store fresh (the source of truth). */
    private readFileFresh;
    /** Atomic write (tmp + rename) — no reader ever sees a torn file. */
    private writeFileAtomic;
}
//# sourceMappingURL=ConversationStore.d.ts.map