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/channels.md
2026-03-11 14:45:37 +00:00

17 KiB

Channels & Conversations

The channel is the universal conversation container. Messages, tool activity, attachments, and KB bindings all hang off a channel. Channels support multiple participants — users, personas, and sessions — making them the foundation for collaborative and multi-model workflows.

Channel CRUD

List channels — paginated, sorted by last activity.

GET /channels?page=1&per_page=50

Returns pagination envelope. Each channel:

{
  "id": "uuid",
  "title": "My Chat",
  "type": "direct|dm|group|channel|workflow|service",
  "description": "",
  "model": "claude-sonnet-4-20250514",
  "provider_config_id": "uuid|null",
  "system_prompt": "",
  "project_id": "uuid|null",
  "folder": "string|null",
  "tags": ["tag1", "tag2"],
  "participant_count": 1,
  "created_at": "...",
  "updated_at": "..."
}

Channel types:

Type Description
direct Single-user chat (default, backward compatible)
dm Human-to-human direct message, AI silent unless @mentioned
group Multi-user/multi-model collaborative channel
channel Named persistent space, configurable ai_mode
workflow Staged channel driven by workflow definitions
service Autonomous task execution, no human participant

Create channel:

POST /channels
{
  "title": "New Chat",
  "type": "direct",
  "description": "",
  "model": "claude-sonnet-4-20250514",
  "provider_config_id": "uuid",
  "system_prompt": "",
  "folder": null,
  "tags": []
}

On create, the authenticated user is automatically added as a participant with role: "owner". If model and provider_config_id are provided, the channel model roster (channel_models) is auto-populated with an initial entry.

Get channel:

GET /channels/:id

Returns single channel object.

Update channel:

PUT /channels/:id

Accepts partial updates — only fields present in the body are changed. Same field set as create.

Delete channel:

DELETE /channels/:id

Cascades: deletes messages, attachments, channel_models, channel_kbs.

Message Tree

Switchboard uses a tree model for messages, not a flat list. Each message has a parent_id (NULL for root). Editing creates a sibling branch; regeneration creates an alternative sibling. The cursor tracks which child is "active" at each branch point.

Get active path — the primary endpoint for loading chat history:

GET /channels/:id/path

Returns the messages along the active branch from root to leaf, following the cursor at each fork. This is what the UI renders.

{
  "messages": [
    {
      "id": "uuid",
      "channel_id": "uuid",
      "parent_id": "uuid|null",
      "role": "user|assistant|system|tool",
      "content": "...",
      "model": "claude-sonnet-4-20250514|null",
      "provider_config_id": "uuid|null",
      "participant_id": "uuid|null",
      "participant_type": "user|persona|session|null",
      "thinking": "...|null",
      "tool_calls": [...],
      "tool_results": [...],
      "attachments": [...],
      "has_siblings": true,
      "sibling_index": 0,
      "sibling_count": 2,
      "created_at": "..."
    }
  ]
}

participant_id and participant_type identify who authored the message. For user messages, this is the authenticated user. For assistant messages, this is the persona (if set) or null for raw model responses. Existing messages without participants return null.

List all messages — includes all branches, flat:

GET /channels/:id/messages

Legacy/debug endpoint. Returns every message in the channel regardless of branch. Not used by the standard frontend.

Create message:

POST /channels/:id/messages
{
  "role": "user",
  "content": "Hello",
  "parent_id": "uuid|null",
  "attachments": ["attachment-uuid-1"]
}

Edit message — creates a sibling at the same level:

POST /channels/:id/messages/:msgId/edit
{ "content": "Revised question" }

Creates a new message with the same parent_id as the original. Automatically updates the cursor to point to the new sibling.

Regenerate — creates an alternative assistant response:

POST /channels/:id/messages/:msgId/regenerate
{
  "model": "claude-sonnet-4-20250514",
  "provider_config_id": "uuid"
}

Creates a new empty assistant message as a sibling of msgId, then streams the completion into it. The cursor is updated.

Update cursor — switch to a different branch:

PUT /channels/:id/cursor
{
  "message_id": "uuid",
  "child_id": "uuid"
}

Sets which child is active at the message_id branch point. Subsequent GET /channels/:id/path will follow the new branch.

List siblings — all children of a message's parent:

GET /channels/:id/messages/:msgId/siblings

Returns { "siblings": [...] } with abbreviated message objects (id, role, content preview, created_at).

Completions & Streaming

The single most complex endpoint. Sends a message to an LLM provider and streams the response via Server-Sent Events.

POST /chat/completions

Request:

{
  "channel_id": "uuid",
  "message": "User's message text",
  "model": "claude-sonnet-4-20250514",
  "provider_config_id": "uuid",
  "persona_id": "uuid|null",
  "parent_id": "uuid|null",
  "tools_enabled": true,
  "tool_ids": ["web_search", "calculator"],
  "attachments": ["attachment-uuid"],
  "extra_body": {}
}

channel_id is required.

extra_body is a freeform JSON object merged into the provider request (provider-specific parameters like temperature, reasoning, etc.).

Participant scoping: The requesting user must be a participant in the channel. Messages are attributed to the authenticated user (or session). In group channels, all participants see all messages in real time via WebSocket.

Persona resolution: The primary completion path. If persona_id is set (explicitly or via @mention resolution from message content), the handler loads the persona's system prompt, model, provider config, and KB bindings. Per-message model/provider_config_id override the persona's defaults. In multi-persona channels, @mention in the message content is parsed to resolve the target persona from the channel's participant list.

Routing resolution: After config resolution, the routing policy evaluator (evaluateRouting()) may redirect to a different provider based on active policies (see §10.4). The winning provider config's credentials are loaded and used for the actual API call.

SSE Stream Format:

The response is Content-Type: text/event-stream. Events:

data: {"content":"Hello"}

data: {"content":" world"}

data: {"reasoning":"Let me think..."}

event: tool_use
data: {"id":"call_1","name":"web_search","input":{"query":"..."}}

event: tool_result
data: {"id":"call_1","content":"Search results..."}

data: {"content":"Based on the search..."}

event: finish
data: {"message_id":"uuid","model":"claude-sonnet-4-20250514","usage":{"input_tokens":150,"output_tokens":42}}

data: [DONE]
Event event: field Payload
Content delta (unnamed) { "content": "text" }
Reasoning delta (unnamed) { "reasoning": "text" }
Tool invocation tool_use { "id", "name", "input" }
Tool result tool_result { "id", "content" }
Stream complete finish { "message_id", "model", "usage" }
End sentinel (unnamed) [DONE] (literal string)
Error mid-stream error { "error": "message" }

Tool cycle: Tool use and tool result events can repeat up to max_tool_iterations (configured server-side). The handler executes tools, feeds results back to the model, and continues streaming.

Response headers:

X-Switchboard-Provider: providerID/configID
X-Switchboard-Route: policy-name    (when routing policy is active)

List available tools:

GET /tools

Returns { "tools": [...] } where each tool has name, description, parameters (JSON Schema), and category.

Multi-Model Roster (Raw Access)

The primary way to add AI to a channel is by adding a persona as a participant (§3.7). Adding a persona automatically populates the channel model roster with the persona's underlying model.

For the rare case where a user needs raw model access without a persona wrapper, the channel_models table provides direct model roster management. This is the 1% escape hatch — most users should never need it.

GET    /channels/:id/models              → { "models": [...] }
POST   /channels/:id/models              ← { "model": "...", "provider_config_id": "...", "display_name": "..." }
PATCH  /channels/:id/models/:modelId     ← { "display_name": "..." }
DELETE /channels/:id/models/:modelId

Each roster entry:

{
  "id": "uuid",
  "channel_id": "uuid",
  "model": "claude-sonnet-4-20250514",
  "provider_config_id": "uuid",
  "display_name": "Claude",
  "is_default": true,
  "persona_id": "uuid|null",
  "created_at": "..."
}

persona_id: set when this roster entry was auto-created by adding a persona participant. Null for manually-added raw models.

@mention resolution: When a user types @ in a channel, the autocomplete shows persona participants first (by display name), then raw model roster entries. The target of a @mention is always resolved to a specific provider_config_id + model pair — never an ambiguous bare model name.

Channel Utilities

Summarize channel (compaction):

POST /channels/:id/summarize

Triggers conversation summarization via the utility model role. Returns { "message": "summarized", "summary_id": "uuid" }.

Generate title:

POST /channels/:id/generate-title

Uses the utility model to auto-generate a title from the first exchange. Returns { "title": "Generated Title" }.

Files

All binary content associated with channels — user uploads, tool- generated artifacts, system files — lives in the unified files table backed by the ObjectStore (see §X). The old attachments table and routes are removed.

Upload (user):

POST /channels/:id/files
Content-Type: multipart/form-data

Field: file. Sets origin: "user_upload". Returns the file object.

Create (tool output):

POST /files
{
  "channel_id": "uuid",
  "message_id": "uuid",
  "filename": "generated_image.png",
  "content_type": "image/png",
  "display_hint": "inline",
  "metadata": { "tool_name": "image_generation" }
}

Body: multipart file field or base64 data field. Sets origin: "tool_output". Returns file object.

List by channel:

GET /channels/:id/files?origin=user_upload

Returns { "files": [...] }. Filterable by origin, content_type.

List by message:

GET /messages/:id/files  → { "files": [...] }

How the frontend discovers generated artifacts to render inline.

List by user (file manager):

GET /files?page=1&per_page=50  → { "files": [...], "total": N }

All files owned by the authenticated user, paginated.

Get metadata:

GET /files/:id
{
  "id": "uuid",
  "channel_id": "uuid",
  "message_id": "uuid|null",
  "user_id": "uuid",
  "origin": "user_upload|tool_output|system",
  "filename": "report.pdf",
  "content_type": "application/pdf",
  "size_bytes": 1048576,
  "display_hint": "inline|download|thumbnail",
  "extracted_text": "...|null",
  "metadata": {},
  "created_at": "...",
  "updated_at": "..."
}

Download:

GET /files/:id/download

Returns the file with appropriate Content-Type and Content-Disposition headers.

Thumbnail:

GET /files/:id/thumbnail

Delete:

DELETE /files/:id

Deletes metadata + blob. Channel deletion cascades via FK + DeletePrefix.

Channel Participants

The participant system is how users and AI interact in a channel. Personas are the primary way to add AI — adding a persona as a participant brings its full identity (name, avatar, system prompt, model, provider config, KB bindings) into the channel. This is the 99% path. Raw model access without a persona is available via the model roster (§3.4) for power users.

Every channel has at least one participant (the owner). direct channels have exactly one user participant and optionally one or more persona participants. group and workflow channels support multiple participants of mixed types.

List participants:

GET /channels/:id/participants

Returns { "participants": [...] }:

{
  "id": "uuid",
  "channel_id": "uuid",
  "participant_type": "user|persona|session",
  "participant_id": "uuid",
  "role": "owner|member|observer",
  "display_name": "Jane Doe",
  "avatar_url": "...|null",
  "joined_at": "..."
}

Participant types:

Type Description
user Authenticated user. participant_id = users.id
persona AI persona added to channel. participant_id = personas.id. Brings model, system prompt, KB bindings
session Anonymous/session participant (workflow intake). participant_id = session token

Participant roles:

Role Capabilities
owner Full control: add/remove participants, delete channel, all member capabilities
member Send messages, trigger completions, view history
observer Read-only: view messages, no send

Add participant:

POST /channels/:id/participants
{
  "participant_type": "user|persona",
  "participant_id": "uuid",
  "role": "member"
}

Requires owner role in the channel. Adding a persona participant:

  • Makes the persona available as an @mention target
  • Adds the persona's model to the channel model roster automatically
  • Scopes the persona's KB bindings into the channel's KB resolution chain
  • The persona's avatar and display name appear in the participant list

Update participant role:

PATCH /channels/:id/participants/:participantId
{ "role": "observer" }

Remove participant:

DELETE /channels/:id/participants/:participantId

Cannot remove the last owner. Removing a persona participant also removes its auto-created model roster entry.

Completion routing: When @PersonaName appears in message content, the completion handler resolves to that persona's model, provider config, and system prompt. When no @mention is present in a multi-persona channel, the channel's default persona (first added) handles the response. The persona_id field in the completion request (§3.3) can also be set explicitly to override @mention resolution.

Backward compatibility: Existing direct channels that predate the participant system are auto-migrated on first access — a single user participant with role: "owner" is created from the channel's legacy user_id column.

Presence

Heartbeat-based online status. Not channel-scoped — tracks global user presence.

Heartbeat:

POST /presence/heartbeat

Client sends every 30 seconds. Server upserts user_presence row. No request body needed. Returns { "ok": true }.

Auth: Authenticated.

Bulk query:

GET /presence?users=alice,bob,charlie

Returns online status for listed usernames. Threshold: online if last heartbeat < 90 seconds ago.

{
  "users": {
    "alice": { "status": "online", "last_seen": "..." },
    "bob": { "status": "offline", "last_seen": "..." }
  }
}

Subsequent updates delivered via WebSocket presence.changed events.

Typing indicators are sent via WebSocket (see websocket.md). The typing event payload includes participant_id and participant_type.

Chat Folders

User-scoped grouping for chats. Folders have no semantic weight — they are named drawers.

GET    /folders             → list user's folders
POST   /folders             ← { "name": "Research" }
PUT    /folders/:id         ← { "name": "Renamed" }
DELETE /folders/:id         → chats become unfiled

Folder object:

{
  "id": "uuid",
  "user_id": "uuid",
  "name": "Research",
  "parent_id": "uuid|null",
  "sort_order": 0,
  "created_at": "...",
  "updated_at": "..."
}

Moving a chat into/out of a folder is done via PUT /channels/:id with { "folder_id": "uuid" } or { "folder_id": null }.

Auth: Authenticated (user-scoped).

Channel Configuration

Additional channel fields (set via PUT /channels/:id):

Field Type Default Description
ai_mode string auto auto, mention_only, or off
topic string null Short description shown in header
allow_anonymous boolean false Session participants can join (workflow channels)
kb_auto_inject boolean false Top-K KB chunks auto-prepend to context

ai_mode behavioral matrix:

Channel type Default ai_mode Notes
direct auto Existing behavior
dm mention_only AI silent unless @mentioned
group auto Configurable per channel
channel auto Configurable per channel

Mark Read

POST /channels/:id/mark-read

Updates the user's last_read_at cursor. Used for unread count calculation.

Auth: Authenticated.

GET /users/search?q=alice

Returns { "data": [{ "id", "username", "handle", "display_name", "avatar_url" }] }. Used for DM creation and participant picker. Matches on handle.

Auth: Authenticated.