/**
 * agent_bridge — event-bus → batch → session → agent-loop glue
 * (WAB.5 + WAB-SUB.2 mode dispatch, 0.5.106).
 *
 * Spec: `docs/tasks/T-warm-agent-chat-bridge.md` WAB.5 + WAB-SUB.2
 * §"dispatcher integration". Architecture:
 * `docs/tasks/WAB.1-architecture.md` decision (d).
 *
 * Responsibility:
 *   1. Subscribe to `bus.on('inbound')` and forward each event's text
 *      into the per-dispatcher `BatchCoordinator`.
 *   2. Provide the coordinator's `onFlush` handler — get/create the
 *      warm session, call EITHER `runAgentTurn` (api mode) OR
 *      `runAgentTurnSubscription` (subscription mode) with the coalesced
 *      text per the resolved `agentLoopOptions.mode` discriminator,
 *      persist via `SessionManager.appendTurn`, forward reply.
 *   3. Enforce the per-session mutex+queue policy: at most ONE in-flight
 *      turn per session (the `inFlight` map below) + ONE queued coalesced
 *      batch (the `pendingQueue` map, depth 1). Subsequent flushes
 *      coalesce into the queue slot up to `MAX_QUEUE_COALESCE_ATTEMPTS`
 *      (= 2); the next attempt DROPS with `onWarn` so operators can spot
 *      flooding. This dispatcher is the sole production caller of
 *      `SessionManager.appendTurn` (regression-locked by
 *      `appendturn_sole_caller.test.ts`).
 *
 * Mode dispatch (WAB-SUB.2):
 *   `agentLoopOptions` is a discriminated union — `.mode` is the
 *   discriminator. Switch happens ONCE per turn at the call site;
 *   exhaustiveness via `assertNever`. Both modes are first-class —
 *   neither is hardcoded as a default. Adding a future mode (local,
 *   mcp) requires extending the union AND the switch, which surfaces
 *   the gap at compile time.
 *
 * Reply surface: v1 invokes `onReply` (default no-op). WAB.6 wires it
 * to the legacy chat-daemon RPC.
 *
 * Shutdown: idempotent — unsubscribes, shuts down coordinator (drops
 * pending timers), awaits in-flight turns, drops queued batches.
 *
 * Imports from: ./agent_loop.js, ./agent_loop_subscription.js, ./batch.js,
 *   ./event_bus.js, ./session_manager.js, ./types.js.
 * Imported by: ./index.ts (barrel); daemon.ts.
 */
import { type RunAgentTurnOptions } from './agent_loop.js';
import { type RunAgentTurnSubscriptionOptions } from './agent_loop_subscription.js';
import { type BatchCoordinatorOptions } from './batch.js';
import type { AgentEventBus } from './event_bus.js';
import type { SessionManager } from './session_manager.js';
import type { SessionKey } from './types.js';
/**
 * Discriminated union of agent-loop invocation options. `.mode` is the
 * discriminator the dispatcher switches on per turn.
 *
 * - `'api'` carries the {@link RunAgentTurnOptions} payload (Anthropic
 *   client, model id, tools, system prompt, etc.). The dispatcher calls
 *   `runAgentTurn(state, text, payload)`.
 * - `'subscription'` carries {@link RunAgentTurnSubscriptionOptions}
 *   (CLI binary, args, mcp config path, system prompt). The dispatcher
 *   calls `runAgentTurnSubscription(state, text, payload)`.
 *
 * Both branches are first-class — the daemon picks one based on the
 * pack's `models.yaml` declaration and passes it through verbatim.
 */
export type DispatcherAgentLoopOptions = ({
    mode: 'api';
} & RunAgentTurnOptions) | ({
    mode: 'subscription';
} & RunAgentTurnSubscriptionOptions);
export interface ChatDispatcherOptions {
    bus: AgentEventBus;
    sessionManager: SessionManager;
    /** Agent-loop config shared across all sessions; `dispatcher` here is
     *  the TOOL dispatcher (distinct from this class). */
    agentLoopOptions: DispatcherAgentLoopOptions;
    /** Optional batch-coordinator delay overrides (tests). */
    batchOptions?: Pick<BatchCoordinatorOptions, 'fastDelayMs' | 'shortDelayMs' | 'defaultDelayMs' | 'splitDelayMs'>;
    /** Structured warn sink — drop-third-batch + agent-loop failures. */
    onWarn?: (message: string) => void;
    /** Reply hook (WAB.6 wires to chat-daemon RPC). Default no-op. */
    onReply?: (key: SessionKey, replyText: string, projectUuid: string) => void;
    /** Telemetry hook for agent-turn failures. */
    onTurnError?: (key: SessionKey, err: unknown) => void;
    /**
     * This daemon's own chat-answering session id (T-CHAT-AS-TERMINAL CAT.5).
     * For the always-on headless daemon this is `headless-<umbrellaId>`. The
     * arbitration only answers an inbound when the umbrella lease is fresh AND
     * owned by THIS id (the double-holder guard); a fresh lease owned by anyone
     * else (a human terminal, another daemon) means STAND DOWN. Omitted ⇒
     * legacy presence-only arbitration (skip when SOMEONE is live).
     */
    ownSessionId?: string;
    /**
     * Cross-session arbitration (T-DEL + CAT.5). Returns true when this daemon
     * must SKIP the turn because the lease is held by a DIFFERENT session.
     *
     * Default behavior:
     *   - umbrella-keyed event (`umbrellaId` set): read
     *     `umbrellaLiveSessionLease(umbrellaId)`. With `ownSessionId` set, skip
     *     unless the fresh lease is OURS (ownership guard). Without it, skip when
     *     the lease is fresh (presence-only, legacy semantics).
     *   - project-keyed event (no `umbrellaId`): read `liveSessionLease(uuid)`,
     *     presence-only (the original T-DEL behavior).
     *
     * Injectable for tests. Fail-open: a throw is treated as "do not skip" so a
     * lease-read bug can't mute the daemon.
     */
    shouldSkipTurn?: (arg: {
        umbrellaId: string | undefined;
        projectUuid: string;
        ownSessionId: string | undefined;
    }) => Promise<boolean>;
    /** Telemetry hook fired when a turn is skipped because another session owns the lease. */
    onSkip?: (key: SessionKey, ownerKey: string) => void;
}
export declare class ChatDispatcher {
    private readonly opts;
    private readonly coordinator;
    private readonly inboundListener;
    private readonly pendingQueue;
    private readonly inFlight;
    private readonly projectUuidBySlug;
    private readonly umbrellaIdBySlug;
    private readonly warn;
    private readonly onReply;
    private readonly onTurnError;
    private readonly ownSessionId;
    private readonly shouldSkipTurn;
    private readonly onSkip;
    private started;
    private stopped;
    constructor(opts: ChatDispatcherOptions);
    /** Wire the bus listener. Idempotent; throws if called after
     *  shutdown (single-use, matches InboxTransportBridge). */
    start(): void;
    /** Unsubscribe, drop pending, await in-flight. Idempotent. After
     *  shutdown resolves, no further onReply/onTurnError callbacks fire. */
    shutdown(): Promise<void>;
    get pendingBatchCount(): number;
    get inFlightTurnCount(): number;
    get queuedTurnCount(): number;
    private handleInbound;
    /**
     * Coordinator's onFlush — per-session mutex entry point.
     *
     * - No in-flight turn → start one immediately.
     * - In-flight + queue empty → enqueue (depth 1).
     * - In-flight + queue full → coalesce into queue until
     *   MAX_QUEUE_COALESCE_ATTEMPTS, then DROP + warn.
     */
    private handleFlush;
    /**
     * Run an agent turn for `key`, then drain any queued batch. The drain
     * is a recursive tail-call so the in-flight invariant holds across
     * the boundary. Errors surface via `onTurnError`; the in-flight flag
     * clears in `finally` so subsequent flushes proceed after a failure.
     */
    private runTurnChain;
    private runTurnOnce;
    /**
     * The `--resume <id>` to thread into a subscription turn so it continues the
     * umbrella's existing transcript. Priority:
     *   1. an explicit `resumeSessionId` already on the subscription options
     *      (caller continuing a claude session started outside opensquid),
     *   2. this daemon's `ownSessionId` (the headless id, umbrella-stable — same
     *      across every turn so resume always lands on the same session),
     *   3. undefined (api mode / general path with no resume binding).
     */
    private resolveResumeSessionId;
}
//# sourceMappingURL=dispatcher.d.ts.map