Skip to main content

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

GET /v1/ws/messages
Upgrade to WebSocket with standard auth headers.

Authentication

Authentication is performed before the WebSocket upgrade using the same middleware as REST endpoints.
MethodHeader
JWT BearerAuthorization: Bearer <token>
API KeyX-API-Key: <key>
websocat "ws://localhost:8080/v1/ws/messages" \
  -H "Authorization: Bearer <token>"

Connection Lifecycle

1

HTTP Upgrade

Client sends GET /v1/ws/messages with authentication headers. The server validates credentials before upgrading.
2

WebSocket Established

Server upgrades to WebSocket (gorilla/websocket, 4KB read/write buffers, CheckOrigin allows all origins).
3

Connection Confirmed

Server sends a connected message to confirm the connection is ready.
{"type": "connected"}
4

Bidirectional Messaging

Both sides exchange JSON messages. The server dispatches incoming messages; the client claims, processes, and replies.
5

Keep-Alive

Server sends WebSocket pings every 20 seconds. Client must respond with a pong within 60 seconds or the connection is closed.

Connection Parameters

ParameterValue
Ping interval20s
Pong timeout60s
Max message size64 KB
Write timeout10s
Read/write buffers4 KB

Message Envelope

All messages (both directions) follow a consistent envelope format:
{
  "type": "<message_type>",
  "message_id": "<uuid>",
  "payload": {}
}
FieldTypeDescription
typestringMessage type identifier
message_idstring (UUID)Unique message identifier (omitted for connected and disconnect)
payloadobjectType-specific data

Server-to-Client Messages

connected

Sent once immediately after the WebSocket connection is established.
{
  "type": "connected"
}

message

An inbound message dispatched for processing. This is the primary message type — it carries messages from channel webhooks (Slack, LINE) or system events.
{
  "type": "message",
  "message_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "payload": {
    "channel": "slack",
    "thread_id": "C07ABCDEF-1234567890.123456",
    "sender": "user@example.com",
    "text": "Hello, can you help me?",
    "agent_name": "research-agent",
    "timestamp": "2026-03-10T10:00:00Z"
  }
}

MessagePayload Fields

FieldTypeDescription
channelstringOriginating channel type: "slack", "line", etc.
thread_idstringThread identifier for the conversation
senderstringSender identifier (email, user ID, etc.)
textstringThe message content
agent_namestringTarget agent for processing
timestampstring (ISO 8601)When the message was received

system

System-level notification from Shannon.
{
  "type": "system",
  "message_id": "f7e8d9c0-b1a2-3456-7890-abcdef123456",
  "payload": {
    "text": "Agent research-agent is now available"
  }
}

claim_ack

Response to a client’s claim request, indicating whether the claim was granted.
{
  "type": "claim_ack",
  "message_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "payload": {
    "granted": true
  }
}
FieldTypeDescription
grantedbooleantrue 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.
{
  "type": "claim",
  "message_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}

progress

Send a heartbeat/progress update while processing a claimed message. This extends the claim lease, preventing timeout.
{
  "type": "progress",
  "message_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "payload": {
    "status": "processing",
    "percent": 50
  }
}

reply

Send the completed response for a claimed message. Shannon routes this back to the originating channel (Slack, LINE, etc.).
{
  "type": "reply",
  "message_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "payload": {
    "channel": "slack",
    "thread_id": "C07ABCDEF-1234567890.123456",
    "text": "Here is my response...",
    "format": "text"
  }
}

ReplyPayload Fields

FieldTypeDescription
channelstringTarget channel type
thread_idstringThread to reply in
textstringResponse content
formatstringOutput format: "text" or "markdown"

disconnect

Gracefully close the connection.
{
  "type": "disconnect"
}

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.
1

Message Dispatch

When a message arrives (via channel webhook or system), the Gateway dispatches it to all eligible WebSocket connections indexed by tenant:user.
2

Claim Race

Each daemon that wants to process the message sends a claim request with the message_id.
3

Atomic Resolution

The Gateway atomically claims the message in Redis (SETNX). The first client wins; all others receive {"granted": false}.
4

Processing

The winning daemon processes the message. It can optionally send progress messages to extend the claim lease and signal activity.
5

Reply

The daemon sends a reply with the completed response. Shannon routes it back to the originating channel.

Claim Metadata

When a message is claimed, the Gateway stores metadata in Redis with a 60-second TTL:
FieldDescription
conn_idWebSocket connection identifier
channel_idOriginating channel ID
channel_typeChannel type (slack, line, etc.)
thread_idConversation thread ID
reply_tokenPlatform-specific reply token (if applicable)
timestampClaim timestamp
workflow_idAssociated Temporal workflow ID (if applicable)
workflow_run_idAssociated 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 a reply from a daemon, it routes the response based on claim metadata:
  1. Workflow reply — If workflow_id exists in claim metadata, the Gateway signals the associated Temporal workflow with the reply content
  2. Channel reply — Otherwise, the reply is routed back to the originating channel (Slack post, LINE push message, etc.)

Error Handling

ScenarioBehavior
Auth failure on upgradeHTTP 401 returned, WebSocket not established
Message exceeds 64 KBConnection 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 JSONMessage ignored

Next Steps