Skip to main content

WebSocket Protocol

The SimpleClaw Gateway uses a binary WebSocket protocol for all client-server communication. The protocol supports request/response patterns and server-pushed events.

Connection Flow

1

Open WebSocket Connection

Connect to the Gateway WebSocket endpoint:
const ws = new WebSocket('ws://127.0.0.1:18789');
Remote connections: Use Tailscale Serve/Funnel for WSS:
const ws = new WebSocket('wss://gateway-name.tailnet.ts.net');
2

Send Connect Frame

Send a ConnectParams object as the first message:
{
  "minProtocol": 1,
  "maxProtocol": 1,
  "client": {
    "id": "web-ui",
    "displayName": "Web Dashboard",
    "version": "2026.3.1",
    "platform": "web",
    "mode": "interactive"
  },
  "caps": [],
  "locale": "en-US"
}
3

Receive Hello-OK

Server responds with a HelloOk frame:
{
  "type": "hello-ok",
  "protocol": 1,
  "server": {
    "version": "2026.3.1",
    "connId": "conn-abc123"
  },
  "features": {
    "methods": ["chat.send", "sessions.list", ...],
    "events": ["chat", "agent", "tick"]
  },
  "snapshot": {
    "agents": [...],
    "channels": [...]
  },
  "policy": {
    "maxPayload": 10485760,
    "maxBufferedBytes": 52428800,
    "tickIntervalMs": 30000
  }
}
4

Send Requests

After receiving HelloOk, send method calls as RequestFrame:
{
  "type": "req",
  "id": "req-001",
  "method": "sessions.list",
  "params": { "agentId": "main" }
}
5

Receive Responses and Events

The server sends:
  • Response frames matching request IDs
  • Event frames for asynchronous updates

Frame Types

Request Frame

Client-to-server method invocation:
interface RequestFrame {
  type: "req";
  id: string;           // Unique request ID (client-generated)
  method: string;       // Method name (e.g., "sessions.list")
  params?: unknown;     // Method parameters (optional)
}
Example:
{
  "type": "req",
  "id": "req-123",
  "method": "config.get",
  "params": { "path": "agent.model" }
}

Response Frame

Server response to a request:
interface ResponseFrame {
  type: "res";
  id: string;           // Matches request ID
  ok: boolean;          // Success indicator
  payload?: unknown;    // Result data (if ok=true)
  error?: ErrorShape;   // Error details (if ok=false)
}
Success Example:
{
  "type": "res",
  "id": "req-123",
  "ok": true,
  "payload": { "agent": { "model": "anthropic/claude-opus-4-6" } }
}
Error Example:
{
  "type": "res",
  "id": "req-123",
  "ok": false,
  "error": {
    "code": "INVALID_PARAMS",
    "message": "path is required",
    "retryable": false
  }
}

Event Frame

Server-pushed events (no request):
interface EventFrame {
  type: "event";
  event: string;         // Event name (e.g., "chat", "tick")
  payload?: unknown;     // Event data
  seq?: number;          // Optional sequence number
  stateVersion?: object; // Optional state version
}
Example:
{
  "type": "event",
  "event": "chat",
  "seq": 42,
  "payload": {
    "sessionKey": "main",
    "role": "assistant",
    "text": "Hello! How can I help?"
  }
}

Common Events

chat Event

Agent conversation events:
interface ChatEvent {
  sessionKey: string;
  role: "user" | "assistant" | "system";
  text?: string;
  tool?: string;
  thinking?: string;
  done?: boolean;
}

agent Event

Agent state changes:
interface AgentEvent {
  agentId: string;
  model?: string;
  thinkingLevel?: string;
  verboseLevel?: string;
}

tick Event

Periodic heartbeat:
interface TickEvent {
  ts: number; // Unix timestamp (ms)
}

shutdown Event

Gateway restart notification:
interface ShutdownEvent {
  reason: string;
  restartExpectedMs?: number;
}

Error Codes

Common error codes returned in ErrorShape:
CodeDescriptionRetryable
INVALID_PARAMSInvalid request parametersNo
METHOD_NOT_FOUNDUnknown methodNo
PERMISSION_DENIEDInsufficient permissionsNo
RATE_LIMITEDToo many requestsYes
TIMEOUTRequest timed outYes
INTERNAL_ERRORServer errorMaybe
AGENT_NOT_FOUNDUnknown agent IDNo
SESSION_NOT_FOUNDUnknown session keyNo

Message Size Limits

Default policy from HelloOk:
{
  "maxPayload": 10485760,       // 10MB per message
  "maxBufferedBytes": 52428800, // 50MB total buffer
  "tickIntervalMs": 30000       // 30s heartbeat
}
Exceeding maxPayload returns PAYLOAD_TOO_LARGE error.

Client Implementation

import WebSocket from 'ws';

class GatewayClient {
  private ws: WebSocket;
  private reqId = 0;
  private pending = new Map<string, (res: ResponseFrame) => void>();

  constructor(url: string) {
    this.ws = new WebSocket(url);
    this.ws.on('message', (data) => this.handleMessage(data));
  }

  async connect(clientInfo: ConnectParams): Promise<HelloOk> {
    this.ws.send(JSON.stringify(clientInfo));
    return new Promise((resolve) => {
      this.ws.once('message', (data) => {
        resolve(JSON.parse(data.toString()));
      });
    });
  }

  async request<T>(method: string, params?: unknown): Promise<T> {
    const id = `req-${this.reqId++}`;
    const frame: RequestFrame = { type: 'req', id, method, params };
    
    return new Promise((resolve, reject) => {
      this.pending.set(id, (res) => {
        if (res.ok) {
          resolve(res.payload as T);
        } else {
          reject(res.error);
        }
      });
      this.ws.send(JSON.stringify(frame));
    });
  }

  private handleMessage(data: WebSocket.Data) {
    const frame = JSON.parse(data.toString());
    
    if (frame.type === 'res') {
      const handler = this.pending.get(frame.id);
      if (handler) {
        handler(frame);
        this.pending.delete(frame.id);
      }
    } else if (frame.type === 'event') {
      this.emit(frame.event, frame.payload);
    }
  }
}

Reconnection Strategy

1

Exponential Backoff

Retry with increasing delays: 1s, 2s, 4s, 8s, max 30s
2

Re-authenticate

Send fresh ConnectParams after reconnect
3

Re-subscribe

Restore any stateful subscriptions (e.g., log tailing)

Next Steps

Protocol Reference

Complete schema documentation for all methods

Build a Client

SDK reference for custom integrations

Build docs developers (and LLMs) love