Skip to content

WebSocket Protocol

All communication between Qwack clients (TUI, web, plugin) and the Qwack server happens over WebSocket using JSON messages.

Every WebSocket message follows this envelope:

{
"type": "string",
"sessionId": "string",
"senderId": "string",
"timestamp": 1710000000000,
"payload": {}
}
FieldTypeDescription
typestringEvent type (e.g. prompt:sent, agent:output)
sessionIdstringSession this event belongs to
senderIdstringUser or system ID that sent the event
timestampnumberUnix timestamp in milliseconds
payloadobjectEvent-specific data (see below)

Sent immediately after WebSocket connection is established.

TypeDirectionPayload
auth:tokenClient → Server{ token: string }
auth:okServer → Client{ user: User }
auth:errorServer → Client{ message: string }

Track who’s online and their roles.

TypeDirectionPayload
presence:joinServer → All{ user: User, role: Role }
presence:leaveServer → All{ userId: string }
presence:typingClient → Server → All{ userId: string }
presence:listServer → Client (on join){ participants: Presence[] }
type Role = "host" | "collaborator"
interface Presence {
id: string
name: string
role: Role
isConnected: boolean
}

Side-channel chat between participants. These do not trigger the AI agent.

TypeDirectionPayload
collab:messageClient → Server → All{ authorName: string, content: string }

Rendered in the TUI as 👤 name: content.


When the host types a prompt, the plugin captures and mirrors it:

TypeDirectionPayload
prompt:sentHost Plugin → Server → All{ authorId: string, authorName: string, content: string }

When a non-host or web user sends a prompt:

TypeDirectionPayload
prompt:requestClient → Server{ authorId: string, authorName: string, content: string }
prompt:executeServer → Host Plugin{ content: string, requestedBy: string }

The server relays prompt:request to the host’s plugin as prompt:execute. Only the host’s agent processes the prompt.


Mirrored from the host’s plugin to all connected clients.

TypeDirectionPayload
agent:outputHost → Server → All{ content: string, partId: string }
agent:tool_useHost → Server → All{ tool: string, input: any }
agent:tool_resultHost → Server → All{ tool: string, output: string }
agent:permissionHost → Server → All{ tool: string, command: string, requestId: string }
agent:permission_responseClient → Server → Host{ requestId: string, allowed: boolean }
agent:completeHost → Server → All{ messageId: string }

Yjs CRDT updates for the shared plan document.

TypeDirectionPayload
plan:syncClient ↔ Server ↔ All{ update: string } (base64-encoded Yjs binary)
plan:awarenessClient ↔ Server ↔ AllYjs awareness protocol data

Session lifecycle and configuration changes.

TypeDirectionPayload
session:status_changeServer → All{ status: "active" | "paused" | "completed" }
session:host_changeServer → All{ newHostId: string }
session:settings_changeServer → All{ settings: object }
session:historyServer → Client (on join)Array of persisted events for replay
session:context_snapshotHost → ServerContext snapshot for host transfer
session:errorServer → Client{ message: string, code?: string }

All events (except presence:typing and plan:awareness) are persisted to the session_events database table. This enables:

  • Late joiner replay — new participants receive full conversation history via session:history
  • Host transfer — new host receives event history instead of lossy LLM compaction
  • Session persistence — reconnect after hours/days with full history intact
  • Offline sync — events queued locally during disconnect are flushed with replayed: true flag

Events are auto-deleted after the session TTL expires (default: 30 days).