/**
 * recordThreadMessage — the ONE append funnel every Threadline message-persisting
 * path calls (Robustness Phase 2, D-B; the structural F3 fix) + the
 * conversation-discipline resolver (D-E) + the identity-free digest (FD-5).
 *
 * Mirrors Phase 1's `recordInboundAck` funnel: outbound send AND inbound receive,
 * EVERY path, append to the canonical per-thread log through this one helper. A
 * wiring-integrity test enumerates the message-persisting routes and asserts each
 * goes through `recordThreadMessage` — so a future bypassing path fails the test
 * (Structure > Willpower). This is the structural guarantee that no leg is
 * silently dropped again (the root of F3).
 *
 * Discipline:
 *  - The append NEVER blocks delivery and is off the send critical path (Signal
 *    vs. Authority — observability must not gate the message).
 *  - But a failure is LOUD, matching Phase 1's fail-open bar: N consecutive append
 *    failures on a thread raise ONE deduped Attention item (Bounded Notification
 *    Surface) — a persistently-incomplete history is the literal F3 symptom and
 *    must be operator-visible, not log-file-visible.
 *  - The content digest is ALWAYS recomputed locally (identity-free); a
 *    wire-supplied digest is a cross-check only and NEVER enters the chain.
 *  - The head-cache is refreshed on a COALESCED cadence (debounced, single-flight),
 *    never inside a synchronous per-message CAS (FD-2).
 */
import type { ThreadLog, ThreadLogAuthor, ThreadDirection } from './ThreadLog.js';
import type { ConversationStore } from './ConversationStore.js';
/** The bounded Attention surface (a subset of TelegramAdapter.createAttentionItem). */
export interface AttentionRaiser {
    createAttentionItem?: (item: {
        id: string;
        title: string;
        summary: string;
        category: string;
        priority: 'URGENT' | 'HIGH' | 'NORMAL' | 'LOW';
        sourceContext?: string;
    }) => unknown;
}
export interface ThreadMessageRecorderDeps {
    threadLog: ThreadLog;
    conversationStore: ConversationStore;
    /** Bounded notification surface for the append-failure Attention item (FD-1). */
    attention?: AttentionRaiser | null;
    /** Directory for the dry-run resolver decision log (default `{stateDir}/logs`). */
    logDir?: string;
    now?: () => number;
    /** Coalesce window for the head-cache refresh (FD-2). Default 500ms. */
    headCacheCoalesceMs?: number;
    /** Consecutive-append-failure threshold before the ONE Attention item (FD-1). Default 3. */
    appendFailureAlertThreshold?: number;
    /** Inline body cap before a `store` reference (FD-4). Default 8 KB. */
    inlineMaxBytes?: number;
}
export interface RecordThreadMessageInput {
    threadId: string;
    messageId: string;
    direction: ThreadDirection;
    /** The EXACT body text (used for the identity-free digest + inline storage). */
    body: string;
    /** The verbatim wire `createdAt` (hashed AS RECEIVED). */
    createdAt: string;
    /** Optional wire-supplied digest — a CROSS-CHECK only, never trusted into the chain. */
    wireDigest?: string;
    wireDigestVersion?: number;
    author?: ThreadLogAuthor;
    peerFingerprint?: string;
    subject?: string;
    backfilled?: true;
    /** The message-store id to reference when the body exceeds inlineMaxBytes. */
    messageStoreId?: string;
}
export interface RecordThreadMessageResult {
    status: 'appended' | 'duplicate' | 'collision' | 'append-failed';
    contentDigest: string;
    /** True when a present wire digest disagreed with the locally-computed one (flagged, never overwritten). */
    wireDigestMismatch?: boolean;
}
export type WorkstreamKeyMode = 'subject-slug' | 'peer-only' | 'off';
export interface ResolveOutboundInput {
    /** Caller-supplied explicit threadId (a reply / continuation). */
    explicitThreadId?: string;
    /** The threadId this send would mint today (the as-today UUID). */
    mintedThreadId: string;
    /** The VERIFIED peer fingerprint, when locally resolvable (never a name/subject). */
    peerPrincipal?: string;
    subject?: string;
    /** Caller asked for a brand-new thread (explicit fork intent). */
    fork?: boolean;
    /** Resolver master switch (dev-gated; off → mint as today). */
    enabled: boolean;
    /** Dry-run: log the would-join/would-fork decision but DO NOT reroute. */
    dryRun: boolean;
    workstreamKeyMode: WorkstreamKeyMode;
    /** Holder-only (E5): the JOIN runs only on the conversation's holder machine. */
    isHolder?: boolean;
}
export type ResolverDecision = 'resolver-off' | 'explicit-threadid' | 'minted:fork-requested' | 'minted:no-binding' | 'minted:lookup-failed' | 'joined:existing-binding' | 'would-join:existing-binding';
export interface ResolveOutboundResult {
    /** The threadId the send should use (== minted/explicit unless ENFORCE-join). */
    threadId: string;
    decision: ResolverDecision;
    /** The canonical thread a dry-run would have joined (telemetry). */
    wouldJoin?: string;
    workstreamKey?: string;
}
/** Normalize a subject to a workstream slug (lower, collapse ws/punct, length-cap). */
export declare function deriveWorkstreamKey(subject: string | undefined, mode: WorkstreamKeyMode): string;
export declare class ThreadMessageRecorder {
    private readonly d;
    private readonly coalesceMs;
    private readonly failThreshold;
    private readonly inlineMaxBytes;
    private readonly now;
    /** Consecutive append-failure counts per thread (reset on a successful append). */
    private failCounts;
    /** Pending coalesced head refresh: threadId → debounce timer. */
    private headTimers;
    constructor(deps: ThreadMessageRecorderDeps);
    /**
     * Append one message to its canonical log. Returns synchronously (off the send
     * critical path); never throws into the caller.
     */
    record(input: RecordThreadMessageInput): RecordThreadMessageResult;
    /**
     * Resolve which threadId an OUTBOUND send should use (D-E). Recoverable routing,
     * never an authority: returns the minted/explicit threadId unless the resolver is
     * ENABLED + ENFORCE (not dry-run) + a binding exists. Always logs the decision to
     * the dry-run JSONL so join/fork rates are measurable before enforce.
     */
    resolveOutboundThread(input: ResolveOutboundInput): Promise<ResolveOutboundResult>;
    /** Force any pending coalesced head refresh immediately (tests + shutdown). */
    flushPending(): Promise<void>;
    private scheduleHeadRefresh;
    private refreshHead;
    private onAppendFailure;
    private logDecision;
}
/**
 * The funnel entry point every message-persisting route calls (the greppable name
 * the wiring-integrity test enumerates). Thin wrapper over the injected recorder
 * so funnel state (failure streaks, coalesced head timers) lives in one instance.
 */
export declare function recordThreadMessage(recorder: ThreadMessageRecorder, input: RecordThreadMessageInput): RecordThreadMessageResult;
//# sourceMappingURL=recordThreadMessage.d.ts.map