/** * Gateway HTTP server — exposes the cumulus message pipeline as a REST API * with SSE streaming. Channel adapters and remote clients connect here. * * Endpoints: * POST /api/thread/:name/message — submit message, SSE stream response * POST /api/thread/:name/sync — sync messages from local mode (store only, no Claude) * GET /api/thread/:name/history — recent messages * GET /api/thread/:name/status — thread info + adaptive state * GET /api/threads — list all threads * DELETE /api/thread/:name — delete thread (requires X-Confirm: delete) * POST /api/thread/:name/rename — rename a thread or co-thread (body: { name }) * POST /api/migrate — sync thread + clone repo + set projectDir * POST /api/media/upload — upload file to media directory (returns URL) * GET /media/* — serve uploaded media files (no auth) * GET /health — health check (no auth) */ import { type GatewayAgentsConfig } from '../lib/gateway.js'; import type { HooksConfig, NamespaceConfig, AppliedConfig } from './config.js'; import { type JobRegistryHandle } from './jobs.js'; import { type DemoCapDenial } from './namespaces.js'; import { type PreviewRouterConfig } from './preview/router.js'; import { type VapidConfig } from './push.js'; export interface GatewayServerOptions { previewDelivery?: PreviewRouterConfig; basePath?: string; port: number; apiKeys: string[]; claudePath?: string; sharedMcpPort?: number; /** Root directory for project folders — passed through to sendMessage for lazy creation */ projectRoot?: string; /** Auto-include gateway-agents MCP server with these settings */ gatewayAgentsConfig?: GatewayAgentsConfig; /** Broadcast to WebSocket clients watching a thread (set by webchat adapter after startup) */ broadcastToThread?: (threadName: string, event: Record) => void; /** VAPID config for web push notifications */ vapid?: VapidConfig; /** Inbound webhook configurations */ hooks?: HooksConfig; /** HuggingFace API key for non-Claude models */ hfApiKey?: string; /** OpenAI API key for provider "openai" model entries (task 118) */ openaiApiKey?: string; /** Custom OpenAI-compatible provider registry (task 147) */ customProviders?: import('./config.js').CustomProviderEntry[]; /** Available models for GET /api/models endpoint */ models?: import('./config.js').ModelEntry[]; /** Claude CLI model variants (Opus/Sonnet/...) for GET /api/models */ claudeModels?: import('./config.js').ClaudeModelEntry[]; /** Global default model (from gateway config) */ model?: string; /** Gateway-wide voice-mode model (task 152). Read/written by /api/config only — * voice turns are served by the webchat adapter, which holds its own copy. */ voiceModel?: string; /** Stall-classifier sub-model (task 164). Read/written by /api/config only — the stall * check runs in this module, which holds its own copy via `agentPipelineOpts`. */ stallClassifierModel?: string; /** Background-worker model (task 179). Read/written by /api/config only — workers run * in the webchat adapter, which shares this object by reference. */ workerModel?: string; /** Path to gateway.config.json — where live PATCH /api/config writes (task 104). */ configPath?: string; /** Notified after a live config PATCH so the daemon can propagate the applied * model catalog / default / provider creds to its OTHER in-memory spawn-path * holders (webchat pipelineOpts, daemon config) — the no-restart guarantee * across every path (task 104). */ onConfigApplied?: (applied: AppliedConfig) => void; /** Public base URL for the gateway (e.g., "https://dev.soapko.com"). Used for media URLs. */ baseUrl?: string; /** Agent-native app bridge — mounts /bridge WS routing to live browser tabs (task 097). Default off. */ bridge?: import('./config.js').BridgeConfig; /** Thread namespaces — prefix-scoped list visibility for app/test threads (task 097 P2). */ namespaces?: NamespaceConfig[]; /** Lucky Draw licence key (task 127). Absent = demo mode: namespaces capped at * DEMO_VISITOR_THREAD_CAP visitor threads. Verified offline at startup. */ licenseKey?: string; } export interface GatewayServerHandle { port: number; url: string; /** The underlying HTTP server (used by WebChat adapter for WebSocket upgrade) */ server: import('http').Server; close: () => Promise; /** Hot-reload API keys without restarting the server */ updateApiKeys?: (newKeys: string[]) => void; /** Set broadcast function — routes agent inject notifications to WebSocket clients */ setBroadcastToThread: (fn: (threadName: string, event: Record) => void) => void; /** Set scheduler reference — used by schedule REST endpoints to reload timers */ setScheduler: (s: { reloadThread: (name: string) => void; }) => void; /** Set job registry reference — used by the background-job REST endpoints (task 139) */ setJobRegistry: (r: JobRegistryHandle) => void; /** Set shutdown/restart callbacks — used by admin REST endpoints (cross-platform restart) */ setAdminCallbacks: (cbs: { restart: () => void; shutdown: () => void; }) => void; /** Set federation router — enables cross-gateway agent messaging */ setFederationRouter: (router: import('./federation.js').FederationRouter) => void; } export declare const STALL_QUIET_WINDOW_MS = 75000; export declare const MAX_STALL_NUDGES = 2; export declare function setThreadActivityListener(fn: ((threadName: string, busy: boolean) => void) | null): void; export declare function setThreadRenameListener(fn: ((from: string, to: string) => void) | null): void; export interface QueuedUserMessage { id: string; text: string; images?: Array<{ mimeType: string; base64: string; }>; timestamp: number; /** * Was this spoken in voice mode (task 152 P4)? Carried through the queue because * a drained batch is delivered as a fresh turn: without it, a message spoken * during a worker-report turn comes back as a silent markdown wall to a * hands-free user. */ voiceMode?: boolean; } /** * Delivers a drained batch of user messages as a real turn. Registered by the * webchat adapter at startup, because only it can stream the turn back to the * WebSocket clients watching the thread (same shape as `broadcastToThread` and * `setFederationRouter`, both also set by an adapter after startup). */ type UserQueueDelivery = (threadName: string, batch: QueuedUserMessage[]) => void; export declare function setUserQueueDelivery(fn: UserQueueDelivery): void; /** Queue a deferred user message. Returns the full pending list for the thread. */ export declare function enqueueUserMessage(threadName: string, msg: Omit & { id?: string; }): QueuedUserMessage[]; /** Pending deferred user messages for a thread (empty array when none). */ export declare function getUserQueue(threadName: string): QueuedUserMessage[]; /** Edit a queued message's text in place. Returns the updated pending list. */ export declare function editUserMessage(threadName: string, id: string, text: string): QueuedUserMessage[]; /** * Remove one queued message and return it — the shared half of "cancel" and * "send now". Send-now is cancel plus an ordinary send, so it cannot leave a * copy behind in the queue. */ export declare function takeUserMessage(threadName: string, id: string): { taken?: QueuedUserMessage; remaining: QueuedUserMessage[]; }; /** * Render a drained batch as the text of one turn. * * A single message delivers as its bare text — indistinguishable from one sent * normally, because on the common case a wrapper is pure noise. Several deliver * as one turn (Karl's choice over one-at-a-time: the model sees the whole * picture before it starts answering). * * Deliberately carries no `[sender → recipient]` framing and no reply hint: * this is the user talking, not an agent. */ export declare function formatQueuedUserBatch(batch: QueuedUserMessage[]): string; /** * Attempt a drain outside the busy→idle transition. * * Needed because delivery can decline: with no client watching the thread there * is nobody to stream the turn to, so the batch is put back. Without this, those * messages would wait for the *next* busy→idle — which may never come on an * idle thread. A client re-opening the thread calls this. */ export declare function flushUserQueue(threadName: string): boolean; /** * Mark a thread as busy/idle from any transport (SSE turns, webchat adapter, * agent injects). Busy→idle is THE drain point for queued agent messages — * every transport that ends a turn lands here, so a message queued via one * transport can never be stranded by a turn that ran on another. */ export declare function markThreadBusy(threadName: string, busy: boolean): void; /** * Queue an agent message for a busy thread. Single queue for ALL transports * (REST /api/agents/inject and webchat WS inject). Returns queue position. */ export declare function enqueueAgentMessage(threadName: string, msg: AgentQueuedMessage): number; /** Check if a thread is currently busy (from any source). */ export declare function isThreadBusy(threadName: string): boolean; /** * Deliver an automated turn to a thread, honouring the single busy gate. * * THE one place a non-user, non-transport sender (a scheduled trigger, a * finished background job) starts a turn. Task 120 had to add this gate to the * scheduler after it spawned four concurrent turns on one thread; task 139 * needed the identical gate for job completions. Two copies of it is exactly * the shape of task 100's stranded-queue bug, so there is one. * * Busy ⇒ queue (drained at the shared `markThreadBusy(false)` point). * Idle ⇒ hold the thread busy for the duration, so the other paths see it. */ export declare function deliverAgentTurn(threadName: string, text: string, sender: string, pipelineOpts: Record, delivery?: AgentDelivery): Promise<'queued' | 'sent' | 'failed'>; /** * True if `err` is a benign socket error caused by a client disconnecting * mid-stream (EPIPE/ECONNRESET/etc.). Used by the daemon's uncaughtException * net to swallow these without crashing while still surfacing real faults. */ export declare function isBenignSocketError(err: unknown): boolean; /** Thread names that currently exist on disk (one `.jsonl` per thread). */ export declare function listExistingThreadNames(): string[]; /** The 402 body for a demo-cap refusal. Exported so tests assert one string. */ export declare function demoCapMessage(d: DemoCapDenial): string; export interface AgentQueuedMessage { text: string; sender: string; type: string; targets: string[]; visibility?: string | { hidden: string[]; }; timestamp: number; delivery?: AgentDelivery; } /** Durable sender owns validity and acknowledgment; the shared queue owns admission. */ export interface AgentDelivery { valid(): boolean; settled(persisted: boolean): void; } /** * Is this sender something a recipient could actually reply to? * * Task 110: injects come from three kinds of sender — real threads, detached shell * scripts (the task 109 watcher), and app backends/webhooks. Only the first can receive * a reply; `send_to_agent` to the other two calls getOrCreateThread() and mints a * phantom thread. So the predicate is deliberately the same condition that decides * whether replying would create one. */ export declare function isAgentSender(senderName: string): boolean; /** * Format an agent message for a specific recipient, applying visibility rules. * * Visibility modes: * - "cc" (default): recipient sees all other targets in CC line * - "blind": recipient sees only [sender → self]: msg (no CC) * - { hidden: ["agent-d"] }: asymmetric — hidden agents are invisible to visible * recipients, but hidden agents CAN see the visible recipients (observer mode) */ export declare function formatAgentMessageForRecipient(messageText: string, senderName: string, recipientName: string, opts: { type: string; targets: string[]; visibility?: string | { hidden: string[]; }; /** Task 110: false ⇒ sender is a script/webhook, not a repliable thread. */ senderIsAgent?: boolean; }): string; /** * Render several drained agent messages as one turn. * * Exported because the widget has to RECOGNISE this shape to render it as an * agent message rather than a user bubble (task 144) — and a hand-copied * fixture of it is exactly what task 103 got wrong: the tests passed against a * shape that never occurs in production. The widget test drives this function. */ export declare function formatAgentBatch(queue: AgentQueuedMessage[]): string; /** Override the threads directory (used by tests to isolate from production) */ export declare function setThreadsDir(dir: string): void; export declare function startGatewayServer(options: GatewayServerOptions): Promise; export {}; //# sourceMappingURL=server.d.ts.map