@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
TypeScript
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