/**
 * SessionRouter — the L4 inbound-message dispatch engine (Multi-Machine Session
 * Pool §L4 "Message Routing to Owner" + "Ownership CAS at Dispatch"). For every
 * inbound message the router resolves session ownership and dispatches:
 *
 *   1. Owned + owner alive + owner == self  → handle locally (no MeshRpc hop).
 *   2. Owned + owner alive + owner != self  → forward via deliverMessage over MeshRpc
 *      (signed, recipient-bound, idempotent on messageId; ACK before offset advance).
 *   3. Owned + owner DEAD                    → owner-dead re-placement (re-place + claim).
 *   4. Transient (placing/transferring)      → queue (ownership-contention).
 *   5. Unowned (new)                         → PlacementExecutor.decide() → SYNCHRONOUS
 *      CAS-claim → spawn on the winner (or, on CAS loss, forward to the winner / queue).
 *
 * Per-session ordering: messages for ONE sessionKey are dispatched strictly in
 * inbound order, at-most-one-in-flight (a per-session promise chain). Different
 * sessions dispatch concurrently. All I/O is injected so the dispatch logic itself
 * is deterministic + unit-testable; the deliverMessage dep owns its own per-attempt
 * timeout (it throws on timeout) — the router owns the retry/backoff/fallback loop.
 */
import type { PlacementExecutor, TopicPlacement } from './PlacementExecutor.js';
import type { MachineCapacity } from './types.js';
export interface OwnershipView {
    owner: string | null;
    epoch: number;
    status: 'active' | 'placing' | 'transferring' | null;
    target?: string;
}
export interface DeliverAck {
    messageId: string;
    /**
     * `sender-rejected` (Durable Inbound Message Queue §3.4 remote path): a
     * typed, NON-retryable NACK — the receive side re-validated a carried
     * senderEnvelope.userId against ITS OWN users registry and refused. It does
     * NOT mark the owner suspect (the peer is healthy; it answered), is never
     * retried or re-placed (a re-placed owner's registry would reject
     * identically), and maps in the queue drain to terminal
     * `sender-deauthorized`. Old peers never emit it (version skew named).
     */
    accepted: 'queued' | 'duplicate' | 'stale-ownership' | 'sender-rejected';
}
/** Sender identity captured at ingress (Durable Inbound Message Queue §2.2) —
 *  persisted with queued custody so a later delivery still knows its real
 *  sender; carried on the mesh envelope for drained forwards. */
export interface InboundSenderEnvelope {
    userId?: string | number;
    username?: string;
    firstName?: string;
}
export interface InboundMessage {
    sessionKey: string;
    messageId: string;
    payload: unknown;
    topicMetadata?: TopicPlacement;
    senderEnvelope?: InboundSenderEnvelope | null;
    /** The channel scope this message arrived on — threaded into placement so a Slack channel
     *  is never placed on a machine not connected to its workspace (spec:
     *  placement-platform-workspace-aware). Populated at the inbound from the platform event
     *  (Slack: team_id → workspaceId + channelId). Absent (telegram — a SHARED chat — or a legacy
     *  inbound) → placement resolves `unknown` → fail-open. */
    channel?: import('./machineServesChannel.js').ChannelScope & {
        channelId?: string;
    };
}
/** Tri-state custody outcome from the queueMessage dep (§2.2): the router
 *  sets `acked` true ONLY for `queued`/`already-queued` — `refused` keeps
 *  today's un-acked fall-through. */
export type QueueMessageResult = 'queued' | 'already-queued' | 'refused';
export type RouteAction = 'handled-locally' | 'forwarded' | 'spawned' | 'queued' | 'duplicate' | 'owner-dead-replaced' | 'placement-blocked';
export interface RouteOutcome {
    action: RouteAction;
    owner?: string | null;
    detail?: string;
    /** True once the inbound is durably accepted (ledger ACK or local handling) — the
     * caller may advance the platform offset ONLY when this is true (§L4 ACK protocol). */
    acked: boolean;
}
/**
 * Did this route() outcome place/forward the session onto ANOTHER machine — i.e.
 * the inbound dispatch caller must NOT also handle it locally?
 *
 * `'forwarded'`/`'duplicate'` always mean delivered to a remote owner. A fresh
 * placement on a remote machine returns `'spawned'` (self-placement returns
 * `'handled-locally'`, never `'spawned'`), and owner-dead re-placement returns
 * `'owner-dead-replaced'` for EITHER self or remote — so those two are remote only
 * when the resolved owner is not us. Everything else ('handled-locally', a
 * self 'owner-dead-replaced', 'queued', 'placement-blocked') is local/no-op and
 * falls through to local dispatch.
 *
 * Pure + caller-agnostic so the inbound dispatch decision is unit-testable. Before
 * this, the caller only treated 'forwarded'/'duplicate' as remote, so a just-moved
 * topic was spawned on the target AND injected into the stale local session
 * (double-dispatch — the bug the first live transfer test surfaced, 2026-05-31).
 */
export declare function isRemotelyHandled(outcome: RouteOutcome, selfMachineId: string | null): boolean;
export interface SessionRouterConfig {
    deliverMessageMaxRetries: number;
    deliverMessageRetryBackoffStartMs: number;
    deliverMessageRetryBackoffMaxMs: number;
    /** Bound on stale-ownership re-resolution to avoid an unbounded chase. */
    maxReResolveDepth: number;
}
export declare const DEFAULT_ROUTER_CONFIG: SessionRouterConfig;
export interface SessionRouterDeps {
    selfMachineId: string;
    placement: PlacementExecutor;
    machineRegistry: () => MachineCapacity[];
    resolveOwnership: (sessionKey: string) => OwnershipView;
    isMachineAlive: (machineId: string) => boolean;
    /** WS1.1 (MULTI-MACHINE-SEAMLESSNESS-SPEC invariant 5): does the owner peer
     *  advertise the ws11DeliverReceive capability in its heartbeat?
     *  true  → forward normally.
     *  false → the peer is ALIVE but cannot durably receive (older version or
     *          its queue is dark): forwarding would 501→retry→failover-STEAL
     *          from a live owner — instead the message waits in OUR durable
     *          queue (the conservative side; bounded by the queue's own shelf
     *          life + the owner's flags flipping true on upgrade/next heartbeat).
     *  null  → unknown (no flags signal wired) — proceed as before (back-compat).
     *  Absent dep → proceed as before. */
    ownerSupportsForward?: (machineId: string) => boolean | null;
    /** SYNCHRONOUS per-session CAS-claim (the §L−1 single-ref fast-forward push). */
    casClaimOwnership: (sessionKey: string, machineId: string, expectedEpoch: number) => {
        ok: boolean;
        epoch: number;
    };
    /** ONE deliverMessage attempt over MeshRpc; throws on transport error/timeout. */
    deliverMessage: (target: string, env: {
        sessionKey: string;
        messageId: string;
        payload: unknown;
        ownershipEpoch: number;
        senderEnvelope?: InboundSenderEnvelope | null;
    }) => Promise<DeliverAck>;
    /** Router == owner: process the message on this machine. */
    handleLocally: (msg: InboundMessage) => Promise<void>;
    /** Instruct the chosen machine to spawn/resume the session. */
    spawnOnMachine: (machineId: string, msg: InboundMessage) => Promise<void>;
    /**
     * Confirm a just-placed REMOTE session as the owner (status placing→active) once
     * the spawn has been dispatched to it. Without this the ownership stays 'placing'
     * forever and every later message for the session queues (bug #11). Best-effort —
     * a confirm failure leaves the placement to recover via the normal fences.
     * (The owner-side resume runs on the target; in the single-router topology the
     * router holds the authoritative ownReg, so it confirms on the target's behalf —
     * the FSM only permits a claim whose machineId equals the placed owner.)
     */
    confirmClaim?: (sessionKey: string, machineId: string) => void;
    /** Tri-state custody taking (Durable Inbound Message Queue §2.2). The
     *  production dep is QueueDrainLoop.enqueueLive (never throws; a storage
     *  failure maps to 'refused' → today's fall-through). */
    queueMessage: (msg: InboundMessage, reason: string) => QueueMessageResult;
    raiseAttention: (title: string, body: string) => void;
    markOwnerSuspect?: (machineId: string) => void;
    /**
     * A deliverMessage attempt REACHED the owner (queued/duplicate/stale acks all
     * prove the peer is responsive). Wired to OwnerSuspectBreaker.recordSuccess
     * so the per-peer suspect window closes the moment the peer answers — the
     * other half of the markOwnerSuspect breaker (P19).
     */
    onOwnerResponsive?: (machineId: string) => void;
    sleep: (ms: number) => Promise<void>;
    log?: (line: string) => void;
}
export declare class SessionRouter {
    private readonly deps;
    private readonly cfg;
    private readonly chains;
    constructor(deps: SessionRouterDeps, cfg?: SessionRouterConfig);
    /** Dispatch an inbound message; serialized per-session (in-order, one-in-flight). */
    route(msg: InboundMessage): Promise<RouteOutcome>;
    /**
     * The queue drain's maxAttempts escape hatch (Durable Inbound Message Queue
     * §3.3): ONE forced re-place that bypasses hold/deliver verdicts — a direct
     * placeAndClaim('failover'), serialized on the session's chain like every
     * dispatch. Returns true when the message ended up durably handled
     * (acked outcome that isn't another queued/blocked verdict).
     */
    forceReplace(msg: InboundMessage): Promise<boolean>;
    private backoff;
    private dispatchOne;
    private forwardToOwner;
    private placeAndClaim;
}
//# sourceMappingURL=SessionRouter.d.ts.map