Overview
The Daemon WebSocket API provides a persistent, bidirectional connection for daemon clients to receive and process messages in real time. Unlike the REST API where clients poll for updates, the WebSocket connection enables Shannon to push incoming messages (from Slack, LINE, or system events) directly to connected daemons. The core protocol revolves around a claim-based message dispatch model: Shannon broadcasts messages to eligible connections, and daemons race to claim exclusive processing rights before replying.Endpoint
Authentication
Authentication is performed before the WebSocket upgrade using the same middleware as REST endpoints.| Method | Header |
|---|---|
| JWT Bearer | Authorization: Bearer <token> |
| API Key | X-API-Key: <key> |
Connection Lifecycle
HTTP Upgrade
Client sends
GET /v1/ws/messages with authentication headers. The server validates credentials before upgrading.WebSocket Established
Server upgrades to WebSocket (gorilla/websocket, 4KB read/write buffers, CheckOrigin allows all origins).
Bidirectional Messaging
Both sides exchange JSON messages. The server dispatches incoming messages; the client claims, processes, and replies.
Connection Parameters
| Parameter | Value |
|---|---|
| Ping interval | 20s |
| Pong timeout | 60s |
| Max message size | 64 KB |
| Write timeout | 10s |
| Read/write buffers | 4 KB |
Message Envelope
All messages (both directions) follow a consistent envelope format:| Field | Type | Description |
|---|---|---|
type | string | Message type identifier |
message_id | string (UUID) | Unique message identifier (omitted for connected and disconnect) |
payload | object | Type-specific data |
Server-to-Client Messages
connected
Sent once immediately after the WebSocket connection is established.
message
An inbound message dispatched for processing. This is the primary message type — it carries messages from channel webhooks (Slack, LINE) or system events.
MessagePayload Fields
| Field | Type | Description |
|---|---|---|
channel | string | Originating channel type: "slack", "line", etc. |
thread_id | string | Thread identifier for the conversation |
sender | string | Sender identifier (email, user ID, etc.) |
text | string | The message content |
agent_name | string | Target agent for processing |
timestamp | string (ISO 8601) | When the message was received |
system
System-level notification from Shannon.
claim_ack
Response to a client’s claim request, indicating whether the claim was granted.
| Field | Type | Description |
|---|---|---|
granted | boolean | true if this client was granted exclusive processing rights |
Client-to-Server Messages
claim
Claim exclusive processing rights for a message. Only one client can successfully claim a given message.
progress
Send a heartbeat/progress update while processing a claimed message. This extends the claim lease, preventing timeout.
reply
Send the completed response for a claimed message. Shannon routes this back to the originating channel (Slack, LINE, etc.).
ReplyPayload Fields
| Field | Type | Description |
|---|---|---|
channel | string | Target channel type |
thread_id | string | Thread to reply in |
text | string | Response content |
format | string | Output format: "text" or "markdown" |
disconnect
Gracefully close the connection.
Claim Flow
The claim flow is the core protocol for distributed message processing. It ensures exactly one daemon processes each message, even when multiple daemons are connected.Message Dispatch
When a message arrives (via channel webhook or system), the Gateway dispatches it to all eligible WebSocket connections indexed by
tenant:user.Atomic Resolution
The Gateway atomically claims the message in Redis (
SETNX). The first client wins; all others receive {"granted": false}.Processing
The winning daemon processes the message. It can optionally send
progress messages to extend the claim lease and signal activity.Claim Metadata
When a message is claimed, the Gateway stores metadata in Redis with a 60-second TTL:| Field | Description |
|---|---|
conn_id | WebSocket connection identifier |
channel_id | Originating channel ID |
channel_type | Channel type (slack, line, etc.) |
thread_id | Conversation thread ID |
reply_token | Platform-specific reply token (if applicable) |
timestamp | Claim timestamp |
workflow_id | Associated Temporal workflow ID (if applicable) |
workflow_run_id | Associated Temporal workflow run ID (if applicable) |
Pending message metadata has a 90-second TTL. If a claimed message is not replied to within 60 seconds, the claim expires and the message can be re-dispatched.
Hub Architecture
The WebSocket Hub manages all active connections with these routing strategies:- Tenant-user indexing — Connections are indexed by
"tenant:user"key for targeted dispatch - Sticky thread routing — Messages from the same thread (
"channel_type:thread_id") are routed to the same connection when possible - Redis-backed claims — Distributed claim resolution ensures consistency across multiple Gateway instances
Reply Routing
When the Gateway receives areply from a daemon, it routes the response based on claim metadata:
- Workflow reply — If
workflow_idexists in claim metadata, the Gateway signals the associated Temporal workflow with the reply content - Channel reply — Otherwise, the reply is routed back to the originating channel (Slack post, LINE push message, etc.)
Error Handling
| Scenario | Behavior |
|---|---|
| Auth failure on upgrade | HTTP 401 returned, WebSocket not established |
| Message exceeds 64 KB | Connection closed |
| Pong timeout (60s) | Connection closed by server |
| Write timeout (10s) | Message dropped, connection may close |
| Claim expired (60s TTL) | Message eligible for re-dispatch |
| Invalid JSON | Message ignored |