/** * 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. * * Path matching + the REST request handler + the wizard login flow live in * sibling modules (`routing.ts`, `rest.ts`, `wizard-login.ts`) and are * re-exported below so existing imports from `relay/dispatcher.js` keep * working. */ import type { AutoOptimizeFrame, AutoResearchFrame, CancelTurnFrame, ClientMessageFrame, SubscribeFrame, StopToolCallFrame } from "@aexol/relay-protocol"; import type { SessionStreamManager, Subscriber } from "../server/session-stream.js"; import type { SessionStore } from "../server/storage.js"; import { coerceImages, hasImages, MAX_INLINE_IMAGE_BASE64_CHARS } from "../server/image-attachments.js"; import type { RelayClient } from "./client.js"; export { matchRoute } from "./routing.js"; export type { PublishMetaEvent, RestRequestDeps } from "./routing.js"; export { handleRestRequest } from "./rest.js"; export { startWizardLogin } from "./wizard-login.js"; export type { WizardLoginResult } from "./wizard-login.js"; /** * Wire-boundary coercion of an `images` payload. Accepts url-hosted * attachments (preferred — only the pointer travels over the relay) and * inline base64 up to `MAX_INLINE_IMAGE_BASE64_CHARS` (measured on the * base64 string, i.e. on the wire payload). Rejected entries are * reported through `onReject`. * * Re-exported from `../server/image-attachments.js` so the relay and the * local HTTP/queue paths share one implementation. */ export { coerceImages, hasImages, MAX_INLINE_IMAGE_BASE64_CHARS }; export interface ClientMessageDeps { manager: SessionStreamManager; /** Live relay client used to send `WsEventFrame`s back. */ relay: Pick; /** * 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; /** 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): Promise; /** * 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): Promise; /** * 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; export type { StopToolCallFrame }; /** * Dispatch a `stop_tool_call` frame. Stops ONE hung/long-running tool call * for the session — that call is aborted, the agent receives a synthesized * user-stop tool result, and the turn continues (unlike `cancel_turn`). * * Guarded: unknown toolCallId → no crash, the manager broadcasts a * best-effort `tool_stop_result` info frame. */ export declare function handleStopToolCall(frame: StopToolCallFrame, deps: { manager: SessionStreamManager; }): Promise; /** * 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): void; export interface AutoResearchDeps { store: SessionStore; manager: SessionStreamManager; relay: Pick; /** Subscriber map shared with handleClientMessage. */ subscribers: Map; /** 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; /** Dependencies for auto-optimizer — mirror of AutoResearchDeps. */ export interface AutoOptimizeDeps { store: SessionStore; manager: SessionStreamManager; relay: Pick; subscribers: Map; cwd: string; logger?: { error: (...args: unknown[]) => void; }; } /** * Dispatch an `auto_optimize` frame. Two-phase flow controlled by `action`: * - "analyze" scans the project and streams back `auto_optimize_analysis_complete`. * - "execute" applies approved recommendations and runs gate checks. * Progress/results are streamed as `auto_optimize_*` ws_events on `sessionId`. */ export declare function handleAutoOptimizeFrame(frame: AutoOptimizeFrame, deps: AutoOptimizeDeps): void; //# sourceMappingURL=dispatcher.d.ts.map