This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/docs/ICD/websocket.md
2026-03-15 23:43:36 +00:00

13 KiB

WebSocket Protocol

Real-time event delivery. Single connection per client. All event delivery uses targeted SendToUser — events are pushed to every WebSocket connection belonging to the recipient user.

Connection

Preferred (v0.28.8+): Ticket exchange

POST /api/v1/ws/ticket     (authenticated via JWT Bearer header)
→ 200  { "ticket": "<opaque-hex-string>" }

Then connect with the opaque ticket:

ws://{host}{BASE_PATH}/ws?ticket={ticket}

Tickets are single-use (deleted on validation), 30-second TTL, and stored in server memory. This avoids exposing the JWT in server logs, proxy logs, and browser history.

Legacy (deprecated): JWT query parameter

ws://{host}{BASE_PATH}/ws?token={access_token}

Still accepted for backward compatibility with pre-v0.28.8 clients. Server logs a deprecation warning on each legacy connection. Will be removed in a future version.

Non-browser clients may also use the Authorization: Bearer <jwt> header on the upgrade request.

Origin validation: The WebSocket upgrader validates the Origin header against CORS_ALLOWED_ORIGINS. Connections from unlisted origins are rejected at upgrade time. In development (no CORS_ALLOWED_ORIGINS set), all origins are accepted.

Lifecycle:

  • Server sends WebSocket-level ping frames every 54 seconds
  • Client must respond with pong within 60 seconds or disconnection
  • Max inbound message size: 4 KB
  • Write deadline: 10 seconds per frame
  • Connection ID: {user_id}-{timestamp} (internal, not exposed to client)

Event Envelope

All messages are JSON. The field name is event, not type:

{
  "event": "message.created",
  "payload": { ... },
  "ts": 1710000000000
}
Field Type Description
event string Dot-delimited event label
room string Optional — room scope (currently unused client-side, see § Room Model)
payload object Event-specific data
ts int64 Unix milliseconds

Client → Server Events

The client can send JSON events to the server. Only events with DirFromClient or DirBoth routing are accepted; all others are silently dropped.

Application-level ping:

{ "event": "ping" }

Server responds with { "event": "pong", "ts": ... }. This is separate from the WebSocket-level ping/pong frames.

Typing (human → other participants):

Client sends a chat.typing.{channelID} event. The server tags it with SenderID and ConnID, then rebroadcasts to other participants via SendToUser per channel membership.

Room Model (Planned)

JoinRoom and LeaveRoom methods exist on the connection struct but are not yet wired — no client-side subscribe/unsubscribe mechanism is implemented. The subscribeToBus handler has room filtering logic (if e.Room != "" && !c.rooms[e.Room]) but since connections never join rooms, all room-scoped Bus.Publish calls are effectively no-ops for WebSocket delivery.

Current workaround: all user-facing events use Hub.SendToUser() which bypasses room filtering. Room-scoped Bus.Publish() calls (e.g. workflow.claimed with Room: assignmentID) exist as forward-looking plumbing but do not reach WebSocket clients today.

Event Routing Table

Direction is determined by events.routeTable in server/events/types.go. Prefix matching: longest matching prefix wins; unmatched labels default to DirLocal (server-only, never crosses the wire).

Label Direction Delivery Description
message.created → client SendToUser per channel New message in channel
message.updated → client SendToUser Message content changed
message.deleted → client SendToUser Message removed
channel.created → client SendToUser New channel created
channel.updated → client SendToUser Channel metadata changed
channel.deleted → client SendToUser Channel removed
channel.member.* → client SendToUser Participant added/removed/role changed
typing.start → client SendToUser AI persona started generating
typing.stop → client SendToUser AI persona finished generating
typing.user → client SendToUser per channel Human typing indicator
chat.typing.* ↔ both Bus (prefix match) Legacy typing relay
user.presence → client SendToUser Online/offline status change
user.mentioned → client SendToUser User @mentioned in a channel
notification.new → client SendToUser New persisted notification
notification.read → client SendToUser Badge sync across tabs
role.fallback → client Bus (Room: admin) Role fallback activated (admin-targeted)
workflow.assigned → client SendToUser per team New assignment for team members
workflow.claimed → client SendToUser per channel Assignment claimed
workflow.advanced → client SendToUser per channel Stage advanced
workflow.completed → client SendToUser per channel Workflow finished
workspace.file.* → client Bus Workspace file changed
tool.call.* → client SendToUser Browser tool invocation
tool.result.* ← client Bus Browser tool result
system.notify → client Bus System broadcast
ping ← client Direct Application-level keepalive
pong → client Direct Response to application ping

Event Payload Shapes

message.created

User message (mention_only / DM relay):

{
  "id": "uuid",
  "channel_id": "uuid",
  "role": "user",
  "content": "message text",
  "user_id": "uuid"
}

Assistant message (completion chain):

{
  "id": "uuid",
  "channel_id": "uuid",
  "role": "assistant",
  "content": "response text",
  "model": "model-id",
  "participant_type": "persona",
  "participant_id": "uuid",
  "display_name": "Persona Name",
  "avatar": "/path/to/avatar",
  "tokens_used": 1234,
  "chain_depth": 1
}

typing.start / typing.stop

AI typing indicator (targeted via SendToUser):

{
  "channel_id": "uuid",
  "participant_id": "uuid",
  "participant_type": "persona",
  "display_name": "Persona Name"
}

typing.user

Human typing indicator (relayed to other channel participants):

{
  "channel_id": "uuid",
  "user_id": "uuid",
  "display_name": "Jane Doe"
}

user.presence

Emitted on WebSocket connect/disconnect:

{
  "user_id": "uuid",
  "status": "online|offline"
}

Note: the DB CHECK constraint allows online, away, offline but the WebSocket hub currently only emits online (on connect) and offline (on last connection closed). away is reserved for future idle detection.

user.mentioned

Targeted to the mentioned user:

{
  "channel_id": "uuid",
  "from_user": "uuid",
  "content": "truncated to 120 chars..."
}

notification.new

Full notification object (same shape as REST GET /notifications):

{
  "id": "uuid",
  "user_id": "uuid",
  "type": "kb.ready",
  "title": "Knowledge base ready",
  "body": "optional detail",
  "resource_type": "knowledge_base",
  "resource_id": "uuid",
  "is_read": false,
  "created_at": "..."
}

notification.read

Single notification marked read:

{ "id": "uuid" }

Mark-all-read:

{ "action": "mark_all_read" }

workflow.assigned

Targeted via SendToUser to each team member:

{
  "channel_id": "uuid",
  "team_id": "uuid",
  "stage_name": "Review",
  "assigned_to": "uuid|empty"
}

workflow.claimed

Targeted via SendToUser to all user participants in the channel:

{
  "assignment_id": "uuid",
  "claimed_by": "uuid",
  "channel_id": "uuid"
}

workflow.advanced

Targeted via SendToUser to all user participants in the channel:

{
  "channel_id": "uuid",
  "workflow_id": "uuid",
  "stage": "stage_key",
  "stage_name": "Stage Name"
}

workflow.completed

Targeted via SendToUser to all user participants in the channel:

{
  "channel_id": "uuid",
  "workflow_id": "uuid",
  "stage": "final_stage_key"
}

role.fallback

Published to bus with Room: "admin":

{
  "role": "primary",
  "operation": "chat|embedding",
  "primary_model": "model-a",
  "fallback_model": "model-b",
  "message": "Role \"primary\" primary (model-a) failed — using fallback (model-b)"
}

tool.call.*

Browser tool invocation (targeted via SendToUser):

{
  "tool_name": "...",
  "call_id": "...",
  "input": { ... }
}

Client responds with tool.result.* containing the execution result.


Appendix: Enums

Channel Types

direct (single-user), dm (human-to-human, AI mention-only), group (multi-user/multi-model), channel (named persistent), workflow (staged), service (autonomous task execution)

Channel AI Modes

auto (default), mention_only (default for dm), off

Participant Types

user, persona, session

Participant Roles

owner, member, observer, visitor

Presence Statuses

online, away, offline

DB CHECK constraint: online, away, offline. Runtime: only online and offline are emitted by the WebSocket hub. away is reserved for future idle-timeout detection.

Message Roles

user, assistant, system, tool

Scopes

personal, team, global

Visibility (model catalog)

enabled, disabled, team

User Roles

admin, user

Team Member Roles

admin, member

Provider Types

openai, anthropic, openrouter, venice (extensible via registry)

Model Types

chat, embedding, image

Memory Scopes

user, persona, persona_user

Memory Statuses

active, pending_review, archived

Workspace Owner Types

user, project

Workspace Statuses

active, archived, deleting

Index Statuses

pending, indexing, ready, error, skipped

KB Document Statuses

pending, chunking, embedding, ready, error

Provider Health Statuses

healthy, degraded, down

Routing Policy Types

provider_prefer, team_route, cost_limit, model_alias

Extension Tiers

browser (implemented), starlark (future), sidecar (future)

Grant Types

team_only, global, groups

Resource Grant Scopes

persona, kb

Notification Types

role.fallback, kb.ready, kb.error, grant.changed, memory.extracted, user.mentioned, workflow.assigned, workflow.claimed, task.completed, task.failed, task.budget_exceeded

See enums.md for full descriptions.

Workflow Assignment Statuses

unassigned, claimed, completed

Task Run Statuses

queued, running, completed, failed, budget_exceeded, cancelled

Workflow Instance Statuses

active, completed, stale, cancelled

Git Auth Types

https_pat, https_basic, ssh_key

Policies

Key Default Description
allow_registration "true" Allow new user self-registration
allow_user_byok "true" Allow users to add personal API keys
allow_user_personas "true" Allow users to create personal Personas
allow_team_providers "true" Allow team admins to configure team providers
kb_direct_access "true" Show KB picker in channel (false = strict enterprise mode)
require_email "false" Require email on registration
default_system_prompt "" Injected into all conversations (admin override)

Page Routes (Non-API)

Server-rendered Go template surfaces. Not REST endpoints — these return HTML pages.

Route Surface Description
/login Login Standalone login/register page
/ Chat Main chat interface
/chat/:chatID Chat Chat with specific channel loaded
/editor Editor Workspace file editor
/editor/:wsId Editor Editor with specific workspace
/notes Notes Notes interface
/notes/:noteId Notes Notes with specific note loaded
/admin Admin Platform administration
/admin/:section Admin Admin with specific section
/settings Settings User settings
/settings/:section Settings Settings with specific section

All page routes (except /login) require authentication via AuthOrRedirect middleware (reads sb_token cookie). Admin routes additionally require RequireAdminPage().

Dynamic surface routing for admin-installed extension surfaces is not yet implemented. The current five surfaces are hardcoded in Go route registration. Future: extension manifests declare surface routes, the page engine dynamically registers them with appropriate data loaders. See EXTENSIONS.md for the design direction.