/**
 * ThreadlineRouter — Wires ThreadResumeMap into the existing message receive pipeline.
 *
 * When a cross-agent message arrives for this agent:
 * 1. (Phase 2) Check the AutonomyGate for visibility/approval gating
 * 2. Check if a ThreadResumeMap entry exists for this threadId
 * 3. If yes → resume that Claude session (--resume UUID)
 * 4. If no → spawn a new session, save the mapping
 * 5. Inject thread history into the session context
 * 6. On session end → persist the UUID back to ThreadResumeMap
 *
 * The ThreadlineRouter hooks into the existing message pipeline — it does NOT
 * replace the MessageRouter. It handles the spawn/resume decision for threaded
 * cross-agent conversations specifically.
 */
import type { MessageRouter } from '../messaging/MessageRouter.js';
import type { SpawnRequestManager } from '../messaging/SpawnRequestManager.js';
import type { MessageStore } from '../messaging/MessageStore.js';
import type { IMessageDelivery } from '../messaging/types.js';
import type { MessageEnvelope } from '../messaging/types.js';
import type { ThreadResumeMap } from './ThreadResumeMap.js';
import type { AutonomyGate } from './AutonomyGate.js';
import type { AgentTrustLevel } from './AgentTrustManager.js';
import type { WarmSessionPool } from './WarmSessionPool.js';
/** Configuration for the ThreadlineRouter */
export interface ThreadlineRouterConfig {
    /** Name of this agent */
    localAgent: string;
    /** Machine ID */
    localMachine: string;
    /** Max number of thread history messages to inject into context */
    maxHistoryMessages: number;
}
/**
 * Ledger event fired by the router on thread lifecycle transitions.
 * Consumed by the Integrated-Being SharedStateLedger via registerLedgerEmitters().
 *
 * SIGNAL-ONLY by design — the router never blocks on ledger write failures.
 */
export interface ThreadlineLedgerEvent {
    kind: 'thread-opened' | 'thread-closed' | 'thread-abandoned';
    threadId: string;
    remoteAgent: string;
    subject: string;
    /** Trust-tier source — autonomy level snapshot for trust mapping. */
    autonomyLevel?: string;
    /** ISO timestamp of the event. */
    timestamp: string;
}
/**
 * Transport-level authentication kind for a relay message.
 *
 * Distinguishes how the sender's identity was established on the wire:
 * - `verified`: end-to-end cryptographic authentication of this specific message
 * - `plaintext-tofu`: trust-on-first-use over plaintext transport (not hijack-safe)
 * - `unauthenticated`: no identity verification (rejected upstream in most paths)
 *
 * This is orthogonal to `AgentTrustLevel`, which is who the sender is in the trust DB.
 * Affinity features that assume the sender can't be impersonated on the wire
 * MUST gate on `kind === 'verified'`.
 */
export type RelayTrustLevel = {
    kind: 'verified';
    senderFingerprint: string;
} | {
    kind: 'plaintext-tofu';
    senderFingerprint: string;
} | {
    kind: 'unauthenticated';
};
/** Relay context passed from InboundMessageGate when message arrives via relay */
export interface RelayMessageContext {
    /** Transport-level authentication kind — see RelayTrustLevel. */
    trust: RelayTrustLevel;
    /** Sender's cryptographic fingerprint (display/key use; for authenticated-only use, read trust.senderFingerprint) */
    senderFingerprint: string;
    /** Sender's display name */
    senderName: string;
    /** Trust level of the sender */
    trustLevel: AgentTrustLevel;
    /** Who granted trust */
    trustSource?: string;
    /** When trust was granted */
    trustDate?: string;
    /** Original source fingerprint (for multi-hop) */
    originFingerprint?: string;
    /** Original source name */
    originName?: string;
    /**
     * Warm-session A2A (dark-ship): when true, the relay decided this inbound is
     * eligible for a keep-alive interactive worker (non-topic-bound, trust ≥ floor,
     * feature enabled). The router requests an interactive (persistent) spawn and
     * admits it to the WarmSessionPool instead of the headless `-p` cold-spawn.
     * Absent/false → existing cold-spawn behavior, byte-for-byte.
     */
    preferWarmSession?: boolean;
}
/** Result of handling an inbound threaded message */
export interface ThreadlineHandleResult {
    /** Whether this message was handled as a threadline message */
    handled: boolean;
    /** The thread ID (existing or newly created) */
    threadId?: string;
    /** Whether a new session was spawned (vs. resumed) */
    spawned?: boolean;
    /** Whether an existing session was resumed */
    resumed?: boolean;
    /** Whether the message was injected directly into a live session
     *  (PR-4: avoids the overhead of spawning/resuming when a session is
     *  already running for this thread). */
    injected?: boolean;
    /** The tmux session name handling this thread */
    sessionName?: string;
    /** Error message if handling failed */
    error?: string;
    /** Gate decision (if autonomy gate is active) */
    gateDecision?: string;
    /** Approval ID (if message was queued for approval) */
    approvalId?: string;
}
interface ReceiverAffinityEntry {
    threadId: string;
    firstUsedAt: number;
    lastUsedAt: number;
}
/**
 * Truncate a message body to a byte budget with an explicit marker. Pure +
 * exported for unit testing. No-op when already under budget.
 */
export declare function capMessageBody(body: string, maxBytes: number): string;
/**
 * Build the bounded "Recent thread history" block. Walks newest→oldest so the
 * most recent context always survives, truncates any single oversized message,
 * and stops adding older messages once the byte budget is hit (always keeping at
 * least the newest one). Pure + exported for unit testing.
 */
export declare function buildBoundedHistorySection(messages: Array<{
    agent: string;
    createdAt: string;
    body: string;
}>, totalCount: number, opts: {
    maxBytes: number;
    perMessageBytes: number;
}): string;
/**
 * True when `level` meets or exceeds `floor` per the explicit TRUST_ORDER.
 * Pure + exported for unit testing both sides of the boundary.
 */
export declare function trustMeetsFloor(level: string, floor: string): boolean;
export declare class ThreadlineRouter {
    private readonly messageRouter;
    private readonly spawnManager;
    private readonly threadResumeMap;
    private readonly messageStore;
    private readonly config;
    private readonly autonomyGate;
    /** Optional live-session injection delivery (PR-4). When set, the router
     *  will try to inject inbound messages directly into an already-running
     *  session for the thread before falling back to spawn/resume. */
    private readonly messageDelivery;
    /** Optional ledger-event sink (Integrated-Being v1). Signal-only. */
    private readonly onLedgerEvent;
    /**
     * Warm-session A2A (dark-ship). When non-null AND warmEnabled, a relay inbound
     * flagged `preferWarmSession` spawns an interactive keep-alive worker and admits
     * it here, so follow-ups inject into the live session. Null when the feature is
     * disabled (the dark-ship invariant — behavior is byte-for-byte unchanged).
     */
    private readonly warmSessionPool;
    private readonly warmEnabled;
    /** Trust floor (abuse/resource control) a peer must meet to pin a warm session. */
    private readonly warmTrustFloor;
    /**
     * Server-owned primitive to kill a warm session by its tmux name (cap eviction
     * on admit). Null when the feature is disabled. The server resolves the tmux
     * name → instar session id → killSession; the router never touches tmux itself.
     */
    private readonly killWarmSession;
    /**
     * Optional topic-linkage handler (THREAD-TOPIC-LINKAGE-SPEC.md). When set,
     * inbound replies on threads with an `originTopicId` are routed to the
     * originating Telegram topic session instead of the standard thread-worker
     * path. Wired via setter post-construction so existing callers don't break.
     */
    private topicLinkageHandler;
    /** Track in-flight spawn requests to prevent concurrent spawns for the same thread */
    private readonly pendingSpawns;
    /**
     * Verified-path session affinity (§4.1 D3 fix).
     *
     * Maps a verified peer's senderFingerprint → the threadId we last used
     * for them, with sliding + absolute TTLs. Only read/written when
     * `relayContext.trust.kind === 'verified'`. Uses insertion-order iteration
     * as LRU proxy: setting an existing key deletes-then-sets to bump it to
     * the tail, and eviction at cap drops the head.
     *
     * Process-local, never persisted. Confirmed by test asserting no files
     * appear under `.instar/` during a send burst.
     */
    private readonly recentThreadByPeer;
    /** Test seam: override `Date.now()` for deterministic TTL tests. */
    private readonly nowFn;
    constructor(messageRouter: MessageRouter, spawnManager: SpawnRequestManager, threadResumeMap: ThreadResumeMap, messageStore: MessageStore, config: Partial<ThreadlineRouterConfig> & Pick<ThreadlineRouterConfig, 'localAgent' | 'localMachine'>, autonomyGate?: AutonomyGate | null, messageDelivery?: IMessageDelivery | null, onLedgerEvent?: (evt: ThreadlineLedgerEvent) => void, nowFn?: () => number, warmSessionPool?: WarmSessionPool | null, warmEnabled?: boolean, trustFloor?: string, killWarmSession?: ((sessionName: string) => void) | null);
    /**
     * Look up a recent affinity entry for a verified peer. Returns the threadId
     * iff an entry exists AND both sliding + absolute TTLs are satisfied.
     * Expired entries are evicted as a side-effect. Non-verified trust kinds
     * short-circuit to null — the map is untouched.
     */
    private peekAffinity;
    /**
     * Record a (fingerprint → threadId) affinity for a verified peer. Bumps
     * LRU recency; evicts oldest entries when over cap. Non-verified trust
     * kinds are no-ops.
     */
    private recordAffinity;
    /**
     * Test seam: inspect the affinity map. Returns a snapshot; callers must
     * not mutate. Not intended for production use.
     */
    getAffinitySnapshotForTests(): ReadonlyMap<string, ReceiverAffinityEntry>;
    /**
     * Attach a TopicLinkageHandler post-construction. Optional — when unset,
     * the router behaves identically to pre-spec behavior. Per
     * THREAD-TOPIC-LINKAGE-SPEC.md.
     */
    setTopicLinkageHandler(handler: import('./TopicLinkageHandler.js').TopicLinkageHandler | null): void;
    /** Fire a ledger event swallowing any exception (signal-only). */
    private emitLedger;
    /**
     * Sweep thread-resume entries older than the TTL and emit synthetic
     * `thread-abandoned` ledger events. Bounded, idempotent via dedupKey on
     * the ledger side.
     */
    sweepAbandonedThreads(ttlMs?: number): number;
    /**
     * Handle an inbound cross-agent message that has a threadId.
     *
     * Decision tree:
     * - No threadId → not a threadline message, return { handled: false }
     * - Has threadId + existing resume entry → resume session
     * - Has threadId + no resume entry → spawn new session
     */
    handleInboundMessage(envelope: MessageEnvelope, relayContext?: RelayMessageContext, opts?: {
        /**
         * Resolved canonical fingerprint for the inbound sender, supplied by a
         * LOCAL-delivery ingress where `message.from.agent` is a NAME (the relay
         * path carries the fingerprint via relayContext). Consumed ONLY by the
         * anti-hijack identity comparison below — a narrow hint, NOT a full
         * relayContext, so it has zero effect on grounding, history depth,
         * affinity, or persisted records. Null/absent → name-based fallback
         * (fail-safe isolation). Spec:
         * docs/specs/threadline-local-delivery-fingerprint-attribution.md
         */
        inboundSenderFingerprint?: string;
    }): Promise<ThreadlineHandleResult>;
    /**
     * Notify the router that a thread's session has ended.
     * Persists the UUID back to ThreadResumeMap for future resume.
     */
    onSessionEnd(threadId: string, uuid: string, sessionName: string): void;
    /**
     * Notify the router that a bound worker session has completed.
     * Reverse-maps the tmux session to all active thread entries and demotes only
     * threads that are no longer legitimately waiting on the remote peer.
     */
    onSessionComplete(sessionName: string, uuid?: string): {
        demoted: number;
        skippedAwaitingReply: number;
    };
    /**
     * Notify the router that a thread has been resolved (conversation complete).
     *
     * Emits a `thread-closed` ledger event wrapped in try/finally so the ledger
     * emitter fires even if the resolve() call itself throws. Signal-only.
     */
    onThreadResolved(threadId: string): void;
    /**
     * Notify the router that a thread has failed (unrecoverable error).
     */
    onThreadFailed(threadId: string): void;
    private resumeThread;
    private spawnNewThread;
    /**
     * Warm-session A2A keep-alive spawn (spec §3.5). Like spawnNewThread but:
     *  - requests an INTERACTIVE persistent worker (`interactive: true`) so it
     *    stays alive between messages and accepts injected follow-ups;
     *  - uses the dedicated stay-alive prompt (THREAD_WARM_SPAWN_PROMPT_TEMPLATE);
     *  - admits the session to the WarmSessionPool, killing any cap-evicted
     *    sessions and falling back to cold-spawn on a peer-conflict.
     *
     * Returns a ThreadlineHandleResult on a successful warm spawn+admit, or null
     * to signal the caller should fall back to the normal cold-spawn (peer
     * conflict, spawn denied, or admit failure). NEVER throws to the caller —
     * the warm path is best-effort over the proven cold-spawn.
     */
    private spawnWarmThread;
    /**
     * Attempt to deliver an inbound message directly into an already-running
     * session for this thread, avoiding the spawn/resume path entirely.
     * Returns a successful ThreadlineHandleResult on success, or null to
     * signal the caller should fall back to resume/spawn.
     */
    private tryInjectIntoLiveSession;
    /**
     * Build a cloned envelope whose `message.body` is the inbound body wrapped in
     * the relay grounding header/footer (spec §3.5). `deliverToSession` formats
     * `envelope.message.body`, so wrapping there is what lands the boundary in the
     * injected text. Trust context comes from `relayContext` when present, else
     * falls back to the thread entry's known peer (still framed as external).
     * Pure (no I/O) — exported behavior covered by a unit test asserting the
     * grounding boundary appears in the injected body.
     */
    private wrapInjectEnvelopeWithGrounding;
    private buildHistoryContext;
    private buildPrompt;
}
export {};
//# sourceMappingURL=ThreadlineRouter.d.ts.map