SNA

WebSocket protocol

Request/response ops, push channels, reconnect semantics.

WebSocket is the live channel. It carries push events (assistant deltas, tool calls, permission requests) and the same control ops the HTTP API exposes, just without the per-call HTTP overhead.

Endpoint

ws://<host>:<port>/ws

The client connects once and multiplexes all sessions over that single connection.

Authentication

WebSocket upgrades require the same token as protected HTTP routes. Browser clients cannot set arbitrary upgrade headers, so the TypeScript client sends the token as ws://<host>:<port>/ws?token=<authToken>. Native clients can also use Authorization: Bearer <authToken> or X-SNA-Token on the upgrade request.

If the server has an origin allowlist, browser WebSocket connections must come from an allowed Origin. Token failures close the upgrade with 401; origin failures close it with 403.

Wire shape

Every message is a JSON object with type + payload:

// Client → server
{ "id": "rpc-7", "type": "agent.send", "payload": { "session": "abc", "message": "hi" } }

// Server → client (reply)
{ "id": "rpc-7", "type": "agent.send", "status": "ok", "data": { "queued": true } }

// Server → client (push, no id)
{ "type": "agent.event", "data": { "session": "abc", "event": { "type": "assistant_delta", "delta": "Hello" } } }

The id field is a client-chosen correlation token: present on request/response pairs, absent on push messages.

Control ops

Every HTTP route has a WS equivalent:

WS typeHTTP equivalent
agent.startPOST /agent/start
agent.sendPOST /agent/send
agent.interruptPOST /agent/interrupt
agent.set-modelPOST /agent/set-model
agent.set-permission-modePOST /agent/set-permission-mode
agent.restartPOST /agent/restart
agent.resumePOST /agent/resume
agent.killPOST /agent/kill
agent.run-oncePOST /agent/run-once
agent.completionPOST /agent/completion
agent.statusGET /agent/status
sessions.listGET /agent/sessions
sessions.createPOST /agent/sessions
sessions.updatePATCH /agent/sessions/:id
sessions.removeDELETE /agent/sessions/:id
permission.subscribe(push channel; no HTTP equivalent)
permission.respondPOST /agent/permission-respond

Push channels

Two channels are push-only:

  • agent.event: fires for every AgentEvent produced by every subscribed session. Subscribe with agent.subscribe, unsubscribe with agent.unsubscribe.
  • permission.request: fires when any session emits permission_needed. Subscribe with permission.subscribe.

Reconnect

If the connection drops, the client should reconnect and re-subscribe. Events buffered while disconnected are still in the per-session ring buffer (default: 1000 events); pass ?since=<cursor> on a fresh agent.subscribe to replay.

Why both HTTP and WS

  • HTTP for ordering and ACK. Mutations want a request/response contract that's easy to reason about. HTTP gives that for free.
  • WS for fanout. A single connection delivers events from N sessions plus permission requests across them. Far cheaper than N SSE streams.

The TypeScript client uses HTTP for all mutations and WS for the push channels. Either alone works (http: true, ws: false for short-lived scripts; ws: true, http: false for clients that can't do HTTP at all), but the dual-transport default is the smoothest.

On this page