Channel Adapters - Z-M-Huang/openhive GitHub Wiki

Channel Adapters

Channel adapters connect external messaging systems to OpenHive. Each adapter implements IChannelAdapter and handles its own inbound message flow. The ChannelRouter coordinates adapters and tracks which adapter owns each channel ID for targeted response routing.

Adapter Types

Adapter Transport Channel ID Format Use Case
WsAdapter WebSocket (/ws) ws:{random-hex} Browser UIs, API clients, testing
DiscordAdapter Discord.js Discord snowflake (e.g., 123456789) Discord bot integration

The WebSocket adapter is config-driven: set ws: { enabled: true } in channels.yaml to activate it.

WebSocket Protocol

The WebSocket adapter (/ws) uses a progressive response protocol with six message types:

Message Types

Type Direction Purpose
ack Server → Client AI-generated acknowledgment (sent immediately)
progress Server → Client Intermediate streaming updates
response Server → Client Final response with full content
notification Server → Client Task completion notification (async)
error Server → Client Error message
topic_list Server → Client Active topics for the channel (see Conversation-Threading)

Request/Response Flow

sequenceDiagram
    participant Client
    participant Server

    Client->>Server: JSON {content: "..."}
    Server->>Server: Spawn AI session
    Server-->>Client: {type: "ack", ...}
    Server-->>Client: {type: "progress", ...} (optional)
    Server-->>Client: {type: "response", ...}

Message Format

All messages use JSON envelopes. Server-to-client messages include type, content, topic_id, and topic_name fields.

Direction Type Key Fields
Client → Server (none) content (message text)
Server → Client ack content (AI-generated acknowledgment), topic_id, topic_name
Server → Client response content (final response), topic_id, topic_name
Server → Client notification content (task completion), topic_id, topic_name
Server → Client error error (error message), topic_id (null), topic_name (null)
Server → Client topic_list topics (array of id, name, state objects)

Clients send messages unchanged — no topic metadata required on the client side. Responses from different topics interleave on the wire; the client demultiplexes by topic_id.

Topic-Aware Routing

Each WebSocket connection supports multiple concurrent topics. When a message arrives, the server classifies it to a topic (see Conversation-Threading) and routes it to the appropriate per-topic session. Multiple topics can be processed in parallel on the same connection.

Same-topic serialization. Within a single topic, messages are still processed one at a time — a second message to the same topic while the first is processing will be queued. Parallelism is between different topics, not within a single topic.

Channel Router

The ChannelRouter sits between adapters and the message handler:

  1. Each adapter registers a message callback via onMessage
  2. When a message arrives, the router records channelId → adapter in its #channelOwners map
  3. The message passes through TrustGate for sender trust evaluation — the gate checks the sender's identity against the channel's trust policy and either allows the message to proceed or rejects it before any LLM processing occurs (see Architecture-Decisions#ADR-30)
  4. If allowed, the router forwards the message to TopicClassifier and then to the application handler
  5. Responses are sent back through the owning adapter via sendResponse(channelId, content)

Sender ID per Adapter

Each adapter extracts a senderId that TrustGate uses for trust evaluation:

Adapter senderId Source Notes
DiscordAdapter message.author.id Discord snowflake Unique per Discord user (e.g., 291730823...). Evaluated against the channel's trust policy.
WsAdapter X-Sender-Id header WebSocket upgrade request Required for channels with a deny policy. Assertion-based — suitable for internal/trusted networks only. Not cryptographically verified.

Notification Routing

Task completion notifications are routed to the originating channel:

  • With sourceChannelId + topicId: notification routed to the originating channel and topic. The topicId allows the client to associate the notification with the correct conversation thread.
  • With sourceChannelId only (no topicId): notification sent to the channel without topic context (legacy behavior, triggers without conversational context).
  • Without sourceChannelId (null): expected for schedule triggers (non-notifying by default — see Triggers#Notification Routing & Policy). For keyword or message triggers, null sourceChannelId is logged as an error — these trigger types should always have an originating channel.

The sourceChannelId is threaded through the entire request chain: WS header X-Source-Channel → MCP handler → scopeQueue() → task DB options JSON → task-consumer → child session. For query_team (synchronous), the sourceChannelId is forwarded through the TeamQueryRunner to the child session.

Router Resilience

The ChannelRouter wraps each adapter.connect() call in a try-catch. If one adapter fails to connect (e.g., Discord timeout due to network issues), the router continues starting the remaining adapters. Failed adapters are removed from the active set so getConnectedCount() remains accurate. The server continues operating with the available adapters.

Discord Adapter

Topics are internal to OpenHive — the Discord adapter sends flat channel messages. No Discord threads are created. See Conversation-Threading#Channel Protocol.

The Discord adapter connects via Discord.js and supports:

  • Watched channels: optional allowlist of channel IDs to listen on
  • Message chunking: responses over 2000 characters are split into multiple messages
  • Progress streaming: first assistant text as quick ack, subsequent updates as channel messages

Configure Discord in channels.yaml with a bot token and an optional list of watched channel IDs. See Team-Configuration for the full channels configuration.

Interaction Logging

All adapters contribute to the channel_interactions SQLite table via InteractionStore. Three types of events are logged:

Event Logged By Fields
Inbound user message channel-handler-factory.ts direction=inbound, channelId, userId, contentSnippet (2000 char cap)
Outbound main-agent reply channel-handler-factory.ts direction=outbound, channelId, teamId=main, contentSnippet
Async task notification task-consumer.ts direction=outbound, channelId (from sourceChannelId), teamId (child team), contentSnippet

Intermediate WS frames (ack, progress) are not logged — only the final response.

The channel_interactions table also includes a trust_decision column that records the TrustGate verdict for each inbound message (allowed or denied). Denied messages are logged but never forwarded to TopicClassifier or the LLM.

Interactions are retained for 24 hours and cleaned up by a periodic interval in index.ts. They feed back into agent context via buildConversationHistorySection() in the system prompt, giving agents awareness of prior channel conversation.