|
|
|
|
@@ -1,6 +1,8 @@
|
|
|
|
|
# WebSocket Protocol
|
|
|
|
|
|
|
|
|
|
Real-time bidirectional event bus. Single connection per client.
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
@@ -9,83 +11,313 @@ ws://{host}{BASE_PATH}/ws?token={access_token}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**Lifecycle:**
|
|
|
|
|
- Server sends `ping` every 54 seconds
|
|
|
|
|
- Server sends WebSocket-level `ping` frames every 54 seconds
|
|
|
|
|
- Client must respond with `pong` within 60 seconds or disconnection
|
|
|
|
|
- Max message size: 4 KB
|
|
|
|
|
- Write deadline: 10 seconds
|
|
|
|
|
- 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:
|
|
|
|
|
All messages are JSON. The field name is `event`, not `type`:
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"type": "event.type",
|
|
|
|
|
"payload": { ... }
|
|
|
|
|
"event": "message.created",
|
|
|
|
|
"payload": { ... },
|
|
|
|
|
"ts": 1710000000000
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Room Subscription
|
|
|
|
|
| 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 |
|
|
|
|
|
|
|
|
|
|
Clients subscribe to rooms for scoped event delivery:
|
|
|
|
|
### 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:**
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{ "type": "subscribe", "payload": { "room": "channel:uuid" } }
|
|
|
|
|
{ "type": "unsubscribe", "payload": { "room": "channel:uuid" } }
|
|
|
|
|
{ "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
|
|
|
|
|
|
|
|
|
|
| Prefix | Direction | Description |
|
|
|
|
|
|--------|-----------|-------------|
|
|
|
|
|
| `channel.created` | → client | New channel created |
|
|
|
|
|
| `channel.updated` | → client | Channel metadata changed |
|
|
|
|
|
| `channel.deleted` | → client | Channel removed |
|
|
|
|
|
| `message.created` | → client | New message in subscribed channel |
|
|
|
|
|
| `message.updated` | → client | Message content changed |
|
|
|
|
|
| `message.deleted` | → client | Message removed |
|
|
|
|
|
| `cursor.updated` | → client | Branch cursor moved |
|
|
|
|
|
| `typing.start` | ↔ both | Participant started typing (payload includes `participant_id`, `participant_type`) |
|
|
|
|
|
| `typing.stop` | ↔ both | Typing ended |
|
|
|
|
|
| `participant.joined` | → client | Participant added to channel |
|
|
|
|
|
| `participant.left` | → client | Participant removed from channel |
|
|
|
|
|
| `participant.updated` | → client | Participant role changed |
|
|
|
|
|
| `presence.update` | → client | Participant online/idle/offline status change |
|
|
|
|
|
| `model.changed` | → client | Channel model roster changed |
|
|
|
|
|
| `persona.updated` | → client | Persona metadata changed |
|
|
|
|
|
| `kb.updated` | → client | Knowledge base changed |
|
|
|
|
|
| `kb.document.status` | → client | Document processing status update |
|
|
|
|
|
| `note.created` | → client | Note created |
|
|
|
|
|
| `note.updated` | → client | Note content changed |
|
|
|
|
|
| `note.deleted` | → client | Note removed |
|
|
|
|
|
| `notification.new` | → client | New notification |
|
|
|
|
|
| `notification.read` | → client | Notification marked read |
|
|
|
|
|
| `memory.extracted` | → client | New memory extracted |
|
|
|
|
|
| `memory.status` | → client | Memory approved/rejected |
|
|
|
|
|
| `role.fallback` | → client | Role fallback triggered |
|
|
|
|
|
| `user.mentioned` | → client | User was @mentioned in a channel |
|
|
|
|
|
| `workflow.assigned` | → client | Workflow stage assigned to team/user |
|
|
|
|
|
| `workflow.claimed` | → client | Workflow assignment claimed |
|
|
|
|
|
| `workflow.advanced` | → client | Workflow stage advanced |
|
|
|
|
|
| `workflow.completed` | → client | Workflow instance completed |
|
|
|
|
|
| `workspace.updated` | → client | Workspace state changed |
|
|
|
|
|
| `project.updated` | → client | Project metadata changed |
|
|
|
|
|
| `extension.updated` | → client | Extension config changed |
|
|
|
|
|
| `settings.updated` | → client | Global settings changed |
|
|
|
|
|
| `user.updated` | → client | User profile changed |
|
|
|
|
|
| `file.created` | → client | File uploaded or generated (§17b.4) |
|
|
|
|
|
| `file.deleted` | → client | File removed |
|
|
|
|
|
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).
|
|
|
|
|
|
|
|
|
|
Direction: `→ client` = server-to-client only, `← client` = client-to-server
|
|
|
|
|
only, `↔ both` = bidirectional.
|
|
|
|
|
| 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 |
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 19. Appendix: Enums
|
|
|
|
|
## Event Payload Shapes
|
|
|
|
|
|
|
|
|
|
### `message.created`
|
|
|
|
|
|
|
|
|
|
User message (mention_only / DM relay):
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"id": "uuid",
|
|
|
|
|
"channel_id": "uuid",
|
|
|
|
|
"role": "user",
|
|
|
|
|
"content": "message text",
|
|
|
|
|
"user_id": "uuid"
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Assistant message (completion chain):
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"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`):
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"channel_id": "uuid",
|
|
|
|
|
"participant_id": "uuid",
|
|
|
|
|
"participant_type": "persona",
|
|
|
|
|
"display_name": "Persona Name"
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### `typing.user`
|
|
|
|
|
|
|
|
|
|
Human typing indicator (relayed to other channel participants):
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"channel_id": "uuid",
|
|
|
|
|
"user_id": "uuid",
|
|
|
|
|
"display_name": "Jane Doe"
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### `user.presence`
|
|
|
|
|
|
|
|
|
|
Emitted on WebSocket connect/disconnect:
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"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:
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"channel_id": "uuid",
|
|
|
|
|
"from_user": "uuid",
|
|
|
|
|
"content": "truncated to 120 chars..."
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### `notification.new`
|
|
|
|
|
|
|
|
|
|
Full notification object (same shape as REST `GET /notifications`):
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"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:
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{ "id": "uuid" }
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Mark-all-read:
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{ "action": "mark_all_read" }
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### `workflow.assigned`
|
|
|
|
|
|
|
|
|
|
Targeted via `SendToUser` to each team member:
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"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:
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"assignment_id": "uuid",
|
|
|
|
|
"claimed_by": "uuid",
|
|
|
|
|
"channel_id": "uuid"
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### `workflow.advanced`
|
|
|
|
|
|
|
|
|
|
Targeted via `SendToUser` to all user participants in the channel:
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"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:
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"channel_id": "uuid",
|
|
|
|
|
"workflow_id": "uuid",
|
|
|
|
|
"stage": "final_stage_key"
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### `role.fallback`
|
|
|
|
|
|
|
|
|
|
Published to bus with `Room: "admin"`:
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"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`):
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"tool_name": "...",
|
|
|
|
|
"call_id": "...",
|
|
|
|
|
"input": { ... }
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Client responds with `tool.result.*` containing the execution result.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Appendix: Enums
|
|
|
|
|
|
|
|
|
|
### Channel Types
|
|
|
|
|
|
|
|
|
|
`direct` (single-user), `group` (multi-user/multi-model), `workflow` (staged)
|
|
|
|
|
`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
|
|
|
|
|
|
|
|
|
|
@@ -93,11 +325,15 @@ only, `↔ both` = bidirectional.
|
|
|
|
|
|
|
|
|
|
### Participant Roles
|
|
|
|
|
|
|
|
|
|
`owner`, `member`, `observer`
|
|
|
|
|
`owner`, `member`, `observer`, `visitor`
|
|
|
|
|
|
|
|
|
|
### Presence Statuses
|
|
|
|
|
|
|
|
|
|
`online`, `idle`, `offline`
|
|
|
|
|
`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
|
|
|
|
|
|
|
|
|
|
@@ -129,11 +365,11 @@ only, `↔ both` = bidirectional.
|
|
|
|
|
|
|
|
|
|
### Memory Scopes
|
|
|
|
|
|
|
|
|
|
`user`, `persona`
|
|
|
|
|
`user`, `persona`, `persona_user`
|
|
|
|
|
|
|
|
|
|
### Memory Statuses
|
|
|
|
|
|
|
|
|
|
`pending`, `approved`, `rejected`
|
|
|
|
|
`active`, `pending_review`, `archived`
|
|
|
|
|
|
|
|
|
|
### Workspace Owner Types
|
|
|
|
|
|
|
|
|
|
@@ -141,11 +377,11 @@ only, `↔ both` = bidirectional.
|
|
|
|
|
|
|
|
|
|
### Workspace Statuses
|
|
|
|
|
|
|
|
|
|
`active`, `archived`
|
|
|
|
|
`active`, `archived`, `deleting`
|
|
|
|
|
|
|
|
|
|
### Index Statuses
|
|
|
|
|
|
|
|
|
|
`pending`, `indexing`, `ready`, `error`
|
|
|
|
|
`pending`, `indexing`, `ready`, `error`, `skipped`
|
|
|
|
|
|
|
|
|
|
### KB Document Statuses
|
|
|
|
|
|
|
|
|
|
@@ -180,6 +416,18 @@ only, `↔ both` = bidirectional.
|
|
|
|
|
|
|
|
|
|
See [enums.md](enums.md#notification-types) 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`
|
|
|
|
|
|