UNPKG

@essentialai/cogent-bridge

Version:

Cogent Bridge — cross-agent comms for Claude Code, OpenAI Codex and Slack. Codex users: install with `curl -fsSL https://cogent.tools/install.sh | sh`, then start with `cogent-codex` for real-time peer wake.

207 lines • 8.36 kB
import type { HttpClient } from "./http-client.js"; import type { MessageRecord } from "../types.js"; /** * WebSocket connection state machine states. */ export type WsState = "disconnected" | "connecting" | "connected" | "polling" | "reconnecting"; /** * Configuration for CloudWsClient. */ export interface CloudWsClientOptions { /** Server base URL (http:// or https://). */ endpoint: string; /** Cloud session ID. */ sessionId: string; /** Peer ID. Can be empty string initially (deferred connection). */ peerId: string; /** Bearer auth token. */ token: string; /** HttpClient instance for poll fallback. */ http: HttpClient; /** Callback for a single message from a "message" frame. */ onMessage: (msg: MessageRecord) => void; /** Callback for a batch of messages from "queued_messages" frame. */ onMessages: (msgs: MessageRecord[]) => void; /** Callback when a peer connects. */ onPeerConnected: (peer: { peerId: string; name: string; project: string; }) => void; /** Callback when a peer disconnects. */ onPeerDisconnected: (peerId: string) => void; /** Callback for peers snapshot frame. */ onPeersSnapshot: (peers: unknown[]) => void; /** Callback for state changes. */ onStateChange: (state: WsState) => void; /** Optional callback for error frames from the server. */ onError?: (frame: { code: string; message: string; }) => void; /** Optional callback for delivery acknowledgment frames from the server. */ onDeliveryAck?: (payload: { messageId: string; peerId: string; }) => void; /** Optional callback when peer has been evicted by server (stale cleanup). */ onPeerEvicted?: () => void; /** Poll interval in milliseconds (default: 5000). */ pollIntervalMs?: number; } /** * WebSocket client with reconnection logic and poll fallback. * * Manages the full connection lifecycle: * - Connects to server WS endpoint with token + peerId auth * - Dispatches all 6 WsServerFrame types to callbacks * - Falls back to HTTP polling on disconnect * - Reconnects with exponential backoff (1s-30s with 10% jitter) * - Supports deferred peerId via setPeerId() for lazy connection * - Supports runtime credential updates via updateToken/updateSessionId */ export declare class CloudWsClient { private ws; private state; private readonly backoff; private reconnectTimer; private pollTimer; /** * DELIVERY-PATH EVIDENCE. Before 3.23.9 a poll failure was caught and only logger.warn'd, so the * bridge had no idea whether its FALLBACK delivery path worked — and `healthy` could not be * computed honestly because the data did not exist. Captured evidence that nothing reads is the * defect shape this codebase keeps hitting. */ private lastPollOkAt; private lastPollError; private lastWsErrorSticky; private pingTimer; private lastMessageId; /** * Advance the poll cursor AND persist it. * * Every assignment must go through here. `lastMessageId` was in-memory only, so a restart sent * no cursor and the relay replayed the whole channel ("If lastMessageId is not provided (first * poll), returns all messages") — re-injecting days-old broadcasts on every restart. */ private advanceCursor; /** * Seed the cursor from disk when this process has not consumed anything yet. * * Only ever fills a NULL cursor: a live in-memory position is always newer than the stored one, * so it must never be overwritten by a restore. */ private restoreCursor; private lastWsError; private connectionGeneration; private opts; /** * Wall-clock timestamp (ms since epoch) of the last frame received on this * WebSocket. Survives reconnects and disconnects -- intentionally NOT reset * on close, so an operator can see how stale the transport is. * Surfaced to cogent_health_check as transport.wsLastFrameAt (ISO string). */ private lastFrameAt; constructor(options: CloudWsClientOptions); /** * Initiate WebSocket connection to the server. * Guards against empty peerId -- call setPeerId() first. */ connect(): void; /** * Disconnect from the WebSocket and stop all timers. */ disconnect(): void; /** * Set or update the peerId. * This is the trigger mechanism for deferred WS connection. * Plan 05 creates CloudWsClient at startup with empty peerId (peerId is * only known after register-peer succeeds). After registration, it calls * this method to initiate the WS connection. */ setPeerId(peerId: string): void; /** * Get the current connection state. */ getState(): WsState; /** * True iff the WebSocket is in the "connected" state. * Surfaced to cogent_health_check as transport.wsConnected. */ isConnected(): boolean; /** * ISO 8601 string of the last frame arrival time, or null if no frame has * been received on this client instance yet. Survives reconnects -- * deliberately NOT reset on close so an operator can see staleness across * reconnect cycles. * Surfaced to cogent_health_check as transport.wsLastFrameAt. */ getLastFrameAt(): string | null; /** * Raw wall-clock timestamp (ms since epoch) of the last frame, or null if * no frame has arrived. Sibling of `getLastFrameAt()` -- same source field, * but returns the underlying number so the consumer can compute deltas * (e.g. transport.wsAgeMs in cogent_health_check) without re-parsing the * ISO string. Survives reconnects, same as `getLastFrameAt()`. * Audit L2. */ /** * What this bridge's DELIVERY situation actually is — the input `healthy` needs in order to * describe THIS BRIDGE rather than the relay's uptime. */ getDeliveryDiagnostics(): { pollActive: boolean; lastPollOkAt: number | null; lastPollError: string | null; lastWsError: string | null; }; getLastFrameAtMs(): number | null; /** * Update the last seen message ID (e.g., from HttpBackend operations). */ updateLastMessageId(id: string): void; /** * Update the auth token for reconnection. * If currently connected, disconnects and reconnects with the new token. */ updateToken(newToken: string): void; /** * Point the socket at a DIFFERENT relay host. * * 🔴 WHY THIS EXISTS. The two relays are different HOSTS (cogent.tools vs app.cogent.tools), not * just different credentials. `updateToken`/`updateSessionId` existed and `opts.endpoint` did * not — so an agent that joined a FREE channel and then a TEAM channel in the SAME PROCESS kept * dialling the OLD host while presenting the NEW token and session id. The upgrade 401s, the * backoff retries it every 1-4s forever, and the inbound queue never drains — while HTTP looks * perfectly healthy, because reinitCloudBackend builds a fresh HttpClient with the right base. * Reported live by peer marvin-mobile on 2026-09-05 (queueLength stuck at 410). * * No-ops when the host is unchanged: re-joining another channel on the SAME relay is the common * case, and tearing down a healthy socket for it would be a self-inflicted reconnect storm. */ updateEndpoint(newEndpoint: string): void; /** * Update the session ID. * If currently connected, disconnects and reconnects. */ updateSessionId(newSessionId: string): void; /** * Replace the HttpClient used for polling. * The next poll cycle will use the new client automatically. */ updateHttp(newHttp: HttpClient): void; /** * Send a delivery acknowledgment frame to the server, which routes it * to the original sender so they know the message was received. */ sendDeliveryAck(messageId: string, fromPeerId: string, toPeerId: string): void; private setState; private dispatchFrame; private scheduleReconnect; private startPolling; private stopPolling; private clearTimers; private clearReconnectTimer; private clearPingTimer; } //# sourceMappingURL=ws-client.d.ts.map