/**
 * Relay envelope dispatcher (CLI side).
 *
 * Translates inbound `RelayFrame`s from the backend into calls against the
 * pure REST handlers (`handlers/projects.ts`, `handlers/sessions.ts`) and
 * the live `SessionStreamManager`. Pure module — no socket, no timers,
 * no logging side-channels except what the caller passes in. The caller
 * (`commands/serve.ts`) owns lifecycle and the `RelayClient`.
 *
 * Two entry points, one per inbound frame kind we route:
 *
 *  - `handleRestRequest(frame, deps)` — synchronous-style request/response.
 *    Resolves to a `RestResponseFrame` the caller sends back via
 *    `relay.send(...)`. Errors thrown by handlers are mapped to status
 *    codes (400/404/500) and returned in the body, never re-thrown — the
 *    backend has a 30 s pending-request timeout and we want every request
 *    to terminate with a wire response so the browser doesn't sit on a
 *    stale spinner.
 *
 *  - `handleClientMessage(frame, deps)` — fire-and-forget. Attaches a
 *    `Subscriber` (the relay socket, wrapped) to the session if not
 *    already attached, then calls `manager.prompt()`. Outbound
 *    `ServerEvent`s flow back through the subscriber as `WsEventFrame`s
 *    via `deps.relay.send()`.
 *
 * Subscriber bookkeeping lives in `serve.ts`, NOT here. The dispatcher
 * is given the subscriber by the caller so it stays pure-ish (depends
 * only on `manager` + `relay.send`). This also keeps the lifecycle for
 * "detach all on relay disconnect" in one place.
 *
 * Path matching is a 30-line inline switch; we deliberately do not pull
 * in `path-to-regexp` for five literal patterns. The matcher returns
 * the parsed `id` param when applicable.
 *
 * Close-code policy (mirroring backend `router.ts`):
 *  - The dispatcher itself never closes the relay; that's serve.ts.
 *  - Backend cancels pending REST requests with `code:"timeout"` (30 s)
 *    or `code:"machine_offline"` (CLI socket dropped). Both are
 *    transparent to this layer — we just respond when we can.
 */
import type { AutoResearchFrame, CancelTurnFrame, ClientMessageFrame, MetaEvent, RestRequestFrame, RestResponseFrame, SubscribeFrame } from "@aexol/relay-protocol";
import type { McpExtensionState } from "../mcp/state.js";
import type { SessionStreamManager, Subscriber } from "../server/session-stream.js";
import type { SessionStore } from "../server/storage.js";
import type { RelayClient } from "./client.js";
/**
 * Result of matching a request path against a route table entry.
 * `id` is populated when the route had an `:id` placeholder.
 */
interface RouteMatch {
    route: "list_projects" | "create_project" | "update_project" | "delete_project" | "bind_studio" | "list_project_sessions" | "create_session" | "get_session" | "update_session" | "delete_session" | "get_session_memory" | "get_session_memory_details" | "compact_session" | "remember_and_delete_session" | "fork_session" | "list_path_autocomplete" | "enqueue_prompt" | "get_prompt_queue" | "remove_prompt" | "clear_prompt_queue" | "list_mcp_status" | "get_settings" | "put_settings";
    id?: string;
    /** Parsed query params, if the path carried a `?...` suffix. */
    query?: URLSearchParams;
    /** For `remove_prompt`: the queue item id extracted from the path. */
    itemId?: string;
}
/**
 * Inline path matcher. Returns `null` for any path/method combination we
 * don't recognise; the caller turns that into a `404 Unknown route`.
 *
 * Intentionally literal — a regex table would be marginally fancier but
 * also marginally slower and harder to read for ~11 routes.
 */
export declare function matchRoute(method: string, path: string): RouteMatch | null;
/**
 * Best-effort publisher for project/session lifecycle meta events. Bound
 * by `serve.ts` to `relay.send({kind:"meta_event", machineId, event})`;
 * tests pass a recording stub. Always called inside a try/catch by the
 * dispatcher so a publish failure never breaks the REST response — the
 * mutation already succeeded.
 *
 * Omit (`undefined`) to disable publishing entirely (used in the handler
 * unit tests where there's no relay).
 */
export type PublishMetaEvent = (event: MetaEvent) => void;
export interface RestRequestDeps {
    store: SessionStore;
    manager: SessionStreamManager;
    /** Working directory for this machine. Used by settings persistence. Defaults to homedir. */
    cwd?: string;
    /** Optional logger for unexpected errors. Defaults to console.error. */
    logger?: {
        error: (...args: unknown[]) => void;
    };
    /**
     * Optional MCP extension state. Passed through by `serve.ts` from the
     * `AgentBridge`. When set, `GET /api/mcp-status` returns the current
     * machine-local MCP snapshot.
     */
    mcpState?: McpExtensionState;
    /**
     * Optional meta-event publisher. Invoked AFTER each successful mutation
     * (create/update/delete project, create/update/delete session) so all
     * tabs of all browsers subscribed to this machine refetch. Read paths
     * (`list_*`, `get_*`) never publish.
     *
     * Failure handling: any throw is caught + logged; we never propagate
     * publish failures into the REST response (the SQLite mutation has
     * already committed and the caller deserves the success it asked for).
     */
    publishMetaEvent?: PublishMetaEvent;
}
/**
 * Dispatch a `rest_request` frame. Always resolves with a
 * `RestResponseFrame` — handler exceptions are caught and translated.
 *
 * Status mapping:
 *  - 200: handler returned successfully (body is the handler's return value
 *         or `{ ok: true }` for void returns)
 *  - 400: `BadRequestError` thrown by handler, OR malformed body for a
 *         route that requires one, OR unknown route
 *  - 404: `NotFoundError` thrown by handler
 *  - 405: route matches but method doesn't (returned as 404 since we don't
 *         distinguish — the matcher treats it as "no route", consistent
 *         with the original Hono router which also 404'd)
 *  - 500: anything else; `error` field carries a sanitized message
 */
export declare function handleRestRequest(frame: RestRequestFrame, deps: RestRequestDeps): Promise<RestResponseFrame>;
export interface ClientMessageDeps {
    manager: SessionStreamManager;
    /** Live relay client used to send `WsEventFrame`s back. */
    relay: Pick<RelayClient, "send">;
    /**
     * Subscriber registry. The dispatcher attaches per `(sessionId)` exactly
     * once and reuses the same `Subscriber` instance for subsequent
     * `client_message` frames addressed to the same session. The caller is
     * responsible for clearing this on relay disconnect via
     * `detachAll(deps)`.
     *
     * Keyed by `sessionId` (machine routing already happened at the backend;
     * the CLI only ever sees frames addressed to itself).
     */
    subscribers: Map<string, Subscriber>;
    /** Optional logger for prompt-side errors. Defaults to console.error. */
    logger?: {
        error: (...args: unknown[]) => void;
    };
}
/**
 * Dispatch a `client_message` frame. Idempotent w.r.t. attach: the same
 * `Subscriber` is reused across messages for a given session.
 *
 * Errors:
 *  - Unknown sessionId → emits a `ws_event` carrying an `error`-typed
 *    `ServerEvent` so the browser sees it on the established stream
 *    (mirrors how the old WS route surfaced `manager.attach` failures).
 *  - Unknown message shape → silently ignored with a logger warning;
 *    the wire contract is `{type:"user_message", content:string}` and
 *    anything else is a protocol violation we don't escalate.
 *  - `manager.prompt()` rejection → logged; the manager itself broadcasts
 *    an `error` event to subscribers, so we don't double-report.
 */
export declare function handleClientMessage(frame: ClientMessageFrame, deps: ClientMessageDeps): void;
/**
 * Dispatch a `subscribe` frame from the backend. Handles the case where a
 * browser enters an old session — we load history from SQLite immediately
 * via `manager.attach()` and synthesize `session_ready` so the browser sees
 * the full chat history without needing to send a first `client_message`.
 *
 * Idempotent: if a subscriber already exists for this session, we re-send
 * `session_ready` (history may have changed, and the newly-joined browser
 * tab needs it). The `ready.catch` handler is only registered once — on the
 * first subscriber creation — to avoid duplicate error events.
 */
export declare function handleSubscribe(frame: SubscribeFrame, deps: ClientMessageDeps): void;
/**
 * Dispatch a `cancel_turn` frame. Disposes the session's agent bridge and
 * removes the stream so the next user message creates a fresh one. The
 * bridge dispose triggers `agent_end` broadcast to all subscribers.
 *
 * Idempotent: no-ops when no stream exists for the session.
 */
export declare function handleCancelTurn(frame: CancelTurnFrame, deps: {
    manager: SessionStreamManager;
    logger?: {
        error: (...args: unknown[]) => void;
    };
}): void;
/**
 * Detach every subscriber the dispatcher has attached. Called by
 * `serve.ts` on relay disconnect / shutdown so the underlying spectral
 * processes don't keep an unreachable subscriber pinned.
 *
 * NOTE: this does NOT dispose the streams themselves — spectral keeps running
 * so a future browser reconnect can resume mid-turn. Use
 * `manager.dispose()` at full shutdown.
 */
export declare function detachAllSubscribers(manager: SessionStreamManager, subscribers: Map<string, Subscriber>): void;
export interface AutoResearchDeps {
    store: SessionStore;
    manager: SessionStreamManager;
    relay: Pick<RelayClient, "send">;
    /** Subscriber map shared with handleClientMessage. */
    subscribers: Map<string, Subscriber>;
    /** Project working directory. */
    cwd: string;
    /** Optional logger. Defaults to console.error. */
    logger?: {
        error: (...args: unknown[]) => void;
    };
}
/**
 * Dispatch an `auto_research` frame. Sends the auto-research task through
 * the existing AgentBridge (backend proxy) instead of spawning a separate spectral
 * subprocess. This ensures auto-research uses the same model and API keys
 * as the active session.
 *
 * Progress is streamed back as `ws_event` frames carrying
 * `auto_research_*` ServerEvent types on the `sessionId` channel.
 *
 * Errors are surfaced as `auto_research_error` events.
 */
export declare function handleAutoResearchFrame(frame: AutoResearchFrame, deps: AutoResearchDeps): void;
export {};
//# sourceMappingURL=dispatcher.d.ts.map