Skip to content

06 Channels

Nikolay Vyahhi edited this page Feb 19, 2026 · 3 revisions

Channels

Relevant source files

The following files were used as context for generating this wiki page:

Channels are ZeroClaw's pluggable communication interfaces that connect the agent to external messaging platforms. Each channel implements the Channel trait, providing a uniform interface for receiving messages from users and sending responses back through the appropriate platform (Telegram, Discord, Slack, Email, etc.). The channel system handles message routing, per-sender conversation history, allowlist-based security, and optional streaming responses.

For detailed information about:

Sources: src/channels/mod.rs:1-32, src/channels/traits.rs:1-103

The Channel Trait

All communication platforms implement the Channel trait, which defines a standardized interface for bidirectional message exchange. The trait includes methods for sending messages, listening for incoming messages, health checks, and optional streaming support for progressive response updates.

classDiagram
    class Channel {
        <<trait>>
        +name() String
        +send(SendMessage) Result
        +listen(mpsc_Sender) Result
        +health_check() bool
        +start_typing(recipient) Result
        +stop_typing(recipient) Result
        +supports_draft_updates() bool
        +send_draft(SendMessage) Result~Option~String~~
        +update_draft(recipient, message_id, text) Result
        +finalize_draft(recipient, message_id, text) Result
    }
    
    class ChannelMessage {
        +id: String
        +sender: String
        +reply_target: String
        +content: String
        +channel: String
        +timestamp: u64
    }
    
    class SendMessage {
        +content: String
        +recipient: String
        +subject: Option~String~
    }
    
    Channel ..> ChannelMessage : receives
    Channel ..> SendMessage : sends
    
    class TelegramChannel {
        +bot_token: String
        +allowed_users: Vec~String~
        +pairing: Option~PairingGuard~
        +mention_only: bool
    }
    
    class DiscordChannel {
        +bot_token: String
        +guild_id: Option~String~
        +allowed_users: Vec~String~
        +mention_only: bool
    }
    
    class EmailChannel {
        +config: EmailConfig
        +seen_messages: HashSet~String~
    }
    
    class CliChannel
    
    TelegramChannel ..|> Channel
    DiscordChannel ..|> Channel
    EmailChannel ..|> Channel
    CliChannel ..|> Channel
Loading

Sources: src/channels/traits.rs:46-103, src/channels/telegram.rs:299-310, src/channels/discord.rs:9-35, src/channels/email_channel.rs:108-111, src/channels/cli.rs:7-13

Core Structures

The channel system uses three primary data structures:

Structure Purpose Key Fields
ChannelMessage Incoming message from a user id, sender, reply_target, content, channel, timestamp
SendMessage Outgoing message to a user content, recipient, subject (optional)
Channel trait Platform abstraction send(), listen(), health_check(), streaming methods

Sources: src/channels/traits.rs:3-44

Message Processing Flow

When a message arrives from any channel, it flows through a supervised listener, into a message dispatch loop, through the agent's tool call loop, and back out to the channel as a response. The system supports concurrent message processing with backpressure control.

sequenceDiagram
    participant User
    participant ChannelImpl as "Channel Implementation<br/>(Telegram/Discord/Email)"
    participant SupervisedListener as "spawn_supervised_listener"
    participant MessageQueue as "mpsc::channel<br/>(ChannelMessage)"
    participant Dispatcher as "run_message_dispatch_loop"
    participant ProcessMsg as "process_channel_message"
    participant RuntimeCmd as "handle_runtime_command"
    participant Memory as "Memory Backend"
    participant AgentLoop as "run_tool_call_loop"
    participant ChannelResp as "Channel::send"
    
    User->>ChannelImpl: Send message
    ChannelImpl->>ChannelImpl: Check allowlist
    ChannelImpl->>MessageQueue: tx.send(ChannelMessage)
    
    SupervisedListener->>MessageQueue: Forwards to dispatcher
    MessageQueue->>Dispatcher: Receive message
    Dispatcher->>ProcessMsg: Spawn worker task
    
    ProcessMsg->>RuntimeCmd: Parse /models or /model?
    
    alt Runtime command
        RuntimeCmd->>RuntimeCmd: Handle provider/model switch
        RuntimeCmd->>ChannelResp: Send command response
    else Regular message
        ProcessMsg->>Memory: Recall relevant context
        Memory-->>ProcessMsg: Historical entries
        
        ProcessMsg->>ProcessMsg: Build history with memory context
        ProcessMsg->>AgentLoop: chat(messages, tools)
        
        loop Tool iterations
            AgentLoop->>AgentLoop: Execute tools
        end
        
        AgentLoop-->>ProcessMsg: Final response
        ProcessMsg->>Memory: Auto-save conversation
        ProcessMsg->>ChannelResp: Send response
    end
    
    ChannelResp->>User: Deliver message
Loading

Sources: src/channels/mod.rs:471-509, src/channels/mod.rs:556-814, src/channels/mod.rs:816-844

Supervised Listeners

Each channel runs in a supervised listener task that automatically restarts on failure with exponential backoff. This ensures resilient operation even when facing transient network errors or API rate limits.

// Key constants from src/channels/mod.rs
const DEFAULT_CHANNEL_INITIAL_BACKOFF_SECS: u64 = 2;
const DEFAULT_CHANNEL_MAX_BACKOFF_SECS: u64 = 60;
const CHANNEL_MESSAGE_TIMEOUT_SECS: u64 = 300;

The spawn_supervised_listener function src/channels/mod.rs:471-509 wraps each channel's listen() method and handles:

  • Clean exits (OK result → reset backoff)
  • Error exits (Err result → exponential backoff)
  • Health status reporting via crate::health module
  • Restart counter tracking

Sources: src/channels/mod.rs:61-69, src/channels/mod.rs:471-509

Message Dispatch Concurrency

The dispatcher controls how many messages can be processed simultaneously using a semaphore-based backpressure mechanism:

// From src/channels/mod.rs
const CHANNEL_PARALLELISM_PER_CHANNEL: usize = 4;
const CHANNEL_MIN_IN_FLIGHT_MESSAGES: usize = 8;
const CHANNEL_MAX_IN_FLIGHT_MESSAGES: usize = 64;

The maximum in-flight messages is computed based on the number of active channels src/channels/mod.rs:511-518, allowing the system to scale processing capacity while preventing resource exhaustion.

Sources: src/channels/mod.rs:66-68, src/channels/mod.rs:511-518, src/channels/mod.rs:816-844

Supported Platforms

ZeroClaw supports 13+ communication channels out of the box. Each implementation handles platform-specific authentication, message formats, and delivery semantics.

Channel Implementation Protocol Key Features
telegram TelegramChannel HTTP long-polling Media attachments, streaming, pairing, mention-only mode
discord DiscordChannel WebSocket Gateway Real-time events, mention-only mode, guild filtering
slack SlackChannel HTTP polling Conversations API, allowlist control
email EmailChannel IMAP IDLE + SMTP Push notifications, domain allowlists
whatsapp WhatsAppChannel Webhook (Meta API) Business Cloud API, signature verification
lark LarkChannel WebSocket or webhook Feishu/Lark support, protobuf frames
matrix MatrixChannel Matrix Client-Server API E2EE support via matrix-sdk
imessage IMessageChannel AppleScript bridge macOS only, local Messages.app integration
mattermost MattermostChannel HTTP polling Self-hosted team chat
dingtalk DingTalkChannel Stream WebSocket Enterprise IM, session webhooks
qq QQChannel WebSocket QQ Bot API
signal SignalChannel D-Bus (signal-cli) Signal Protocol, local daemon
irc IrcChannel IRC protocol Classic IRC networks
cli CliChannel stdin/stdout Zero dependencies, always available

Sources: src/channels/mod.rs:1-31, src/channels/telegram.rs:299-310, src/channels/discord.rs:9-35, src/channels/slack.rs:5-17, src/channels/email_channel.rs:108-111, src/channels/whatsapp.rs:11-30, src/channels/lark.rs:130-148, src/channels/matrix.rs:22-32, src/channels/imessage.rs:8-22

Channel Module Exports

// From src/channels/mod.rs
pub use cli::CliChannel;
pub use dingtalk::DingTalkChannel;
pub use discord::DiscordChannel;
pub use email_channel::EmailChannel;
pub use imessage::IMessageChannel;
pub use irc::IrcChannel;
pub use lark::LarkChannel;
pub use matrix::MatrixChannel;
pub use mattermost::MattermostChannel;
pub use qq::QQChannel;
pub use signal::SignalChannel;
pub use slack::SlackChannel;
pub use telegram::TelegramChannel;
pub use traits::{Channel, SendMessage};
pub use whatsapp::WhatsAppChannel;

Sources: src/channels/mod.rs:17-31

Conversation History Management

The channel system maintains per-sender conversation histories to provide context continuity across interactions. Each channel-sender pair has an isolated history cache that is managed independently.

graph TB
    subgraph "History Storage"
        HistoryMap["ConversationHistoryMap<br/>Arc&lt;Mutex&lt;HashMap&lt;String, Vec&lt;ChatMessage&gt;&gt;&gt;&gt;"]
    end
    
    subgraph "History Key Generation"
        HistoryKey["conversation_history_key(msg)<br/>format: {channel}_{sender}"]
    end
    
    subgraph "Message Processing"
        IncomingMsg["Incoming ChannelMessage"]
        RetrieveHistory["Retrieve prior turns<br/>from HistoryMap"]
        BuildPrompt["Build system + history + user message"]
        LLMCall["run_tool_call_loop"]
        SaveHistory["Append user + assistant turns<br/>Trim to MAX_CHANNEL_HISTORY"]
    end
    
    IncomingMsg --> HistoryKey
    HistoryKey --> RetrieveHistory
    RetrieveHistory --> HistoryMap
    HistoryMap --> BuildPrompt
    BuildPrompt --> LLMCall
    LLMCall --> SaveHistory
    SaveHistory --> HistoryMap
Loading

Sources: src/channels/mod.rs:53-56, src/channels/mod.rs:129-131, src/channels/mod.rs:614-624, src/channels/mod.rs:732-744

History Limits and Compaction

// From src/channels/mod.rs
const MAX_CHANNEL_HISTORY: usize = 50;

Each sender's history is limited to 50 messages (25 user-assistant turn pairs). When the limit is exceeded, the oldest messages are removed using a simple FIFO strategy src/channels/mod.rs:740-743.

The per-sender history is distinct from the memory system's long-term storage. For persistent memory and retrieval across sessions, see the Memory System documentation.

Sources: src/channels/mod.rs:56, src/channels/mod.rs:732-744

Memory Context Integration

Before processing each message, the system queries the memory backend for relevant context:

// From src/channels/mod.rs:443-469
async fn build_memory_context(
    mem: &dyn Memory,
    user_msg: &str,
    min_relevance_score: f64,
) -> String

This function recalls up to 5 relevant memory entries and prepends them to the user's message as enriched context src/channels/mod.rs:588-608. Only entries meeting the min_relevance_score threshold are included.

Sources: src/channels/mod.rs:443-469, src/channels/mod.rs:588-608

Streaming Response Support

Channels that support progressive updates can display the agent's response as it's being generated, providing a more responsive user experience. This is implemented via draft message creation, incremental updates, and finalization.

Streaming-Capable Channels

Channel Streaming Support Implementation Method
telegram ✅ Yes Edit message API
discord ✅ Yes Edit message API
Others ❌ No Send complete message only

Sources: src/channels/mod.rs:630-634, src/channels/telegram.rs:341-349

Streaming Flow Diagram

sequenceDiagram
    participant Channel
    participant ProcessMsg as "process_channel_message"
    participant DraftSender as "Draft Update Task"
    participant StreamRx as "delta_rx channel"
    participant LLM as "run_tool_call_loop"
    
    ProcessMsg->>Channel: supports_draft_updates()?
    Channel-->>ProcessMsg: true
    
    ProcessMsg->>Channel: send_draft("...")
    Channel-->>ProcessMsg: draft_message_id
    
    ProcessMsg->>StreamRx: Create mpsc channel
    ProcessMsg->>DraftSender: Spawn updater task
    ProcessMsg->>LLM: run_tool_call_loop(delta_tx)
    
    loop Streaming tokens
        LLM->>StreamRx: Send delta
        StreamRx->>DraftSender: Receive delta
        DraftSender->>DraftSender: Accumulate text
        DraftSender->>Channel: update_draft(draft_id, accumulated)
    end
    
    LLM-->>ProcessMsg: Final response
    ProcessMsg->>DraftSender: Close delta_tx
    DraftSender->>Channel: finalize_draft(draft_id, final_text)
Loading

Sources: src/channels/mod.rs:630-686, src/channels/mod.rs:698-720

Draft Update Configuration

Telegram's streaming behavior is configured via StreamMode and draft_update_interval_ms:

// From src/channels/telegram.rs
pub fn with_streaming(
    mut self,
    stream_mode: StreamMode,
    draft_update_interval_ms: u64,
) -> Self

The draft_update_interval_ms parameter controls the minimum time between successive edit API calls, preventing rate limiting src/channels/telegram.rs:341-349.

Sources: src/channels/telegram.rs:341-349, src/config/schema.rs:1-143

Typing Indicators

For channels that don't support streaming, a periodic typing indicator is sent during processing:

// From src/channels/mod.rs
const CHANNEL_TYPING_REFRESH_INTERVAL_SECS: u64 = 4;

The spawn_scoped_typing_task function src/channels/mod.rs:526-554 creates a background task that repeatedly calls start_typing() every 4 seconds until the response is ready, then calls stop_typing().

Sources: src/channels/mod.rs:69, src/channels/mod.rs:526-554, src/channels/mod.rs:688-696

Runtime Model Switching

Users can dynamically change the LLM provider and model on a per-sender basis using special commands. This enables experimentation with different models without restarting the daemon or editing configuration files.

Commands

Command Syntax Effect
/models /models Show current provider/model and list available providers
/models <provider> /models anthropic Switch to specified provider (clears history)
/model /model Show current model and cached model list for active provider
/model <model-id> /model gpt-4o Switch to specified model (clears history)

Sources: src/channels/mod.rs:83-88, src/channels/mod.rs:146-184

Runtime Command Flow

flowchart TD
    IncomingMsg["Incoming ChannelMessage"]
    ParseCmd["parse_runtime_command(channel, content)"]
    CheckSupport{"supports_runtime_model_switch?<br/>(telegram, discord only)"}
    
    IncomingMsg --> CheckSupport
    CheckSupport -->|No| NormalFlow["Process as normal message"]
    CheckSupport -->|Yes| ParseCmd
    
    ParseCmd --> CheckCmdType{Command Type}
    
    CheckCmdType -->|"/models"| ShowProviders["build_providers_help_response"]
    CheckCmdType -->|"/models provider"| SwitchProvider["SetProvider(provider)"]
    CheckCmdType -->|"/model"| ShowModel["build_models_help_response<br/>(loads cached preview)"]
    CheckCmdType -->|"/model id"| SwitchModel["SetModel(model)"]
    CheckCmdType -->|None| NormalFlow
    
    SwitchProvider --> ResolveAlias["resolve_provider_alias"]
    ResolveAlias --> GetOrCreate["get_or_create_provider"]
    GetOrCreate --> CacheProvider["Cache in provider_cache"]
    CacheProvider --> UpdateRoute["set_route_selection"]
    UpdateRoute --> ClearHistory["clear_sender_history"]
    ClearHistory --> SendResponse["Send confirmation"]
    
    SwitchModel --> UpdateRoute
    ShowProviders --> SendResponse
    ShowModel --> SendResponse
    
    SendResponse --> Done["Return true (handled)"]
    NormalFlow --> Done2["Return false (continue processing)"]
Loading

Sources: src/channels/mod.rs:142-144, src/channels/mod.rs:146-184, src/channels/mod.rs:365-441

Provider and Model Storage

Runtime routing state is maintained in thread-safe collections:

// From src/channels/mod.rs
type ProviderCacheMap = Arc<Mutex<HashMap<String, Arc<dyn Provider>>>>;
type RouteSelectionMap = Arc<Mutex<HashMap<String, ChannelRouteSelection>>>;

struct ChannelRouteSelection {
    provider: String,
    model: String,
}

Each sender has a unique route override key generated by conversation_history_key() src/channels/mod.rs:129-131. If no override exists, the default provider and model from Config are used src/channels/mod.rs:207-212.

Sources: src/channels/mod.rs:73-80, src/channels/mod.rs:129-131, src/channels/mod.rs:207-221

Model Cache Preview

The /model command displays a preview of available models cached in state/models_cache.json:

// From src/channels/mod.rs
const MODEL_CACHE_FILE: &str = "models_cache.json";
const MODEL_CACHE_PREVIEW_LIMIT: usize = 10;

The cache is populated by zeroclaw models refresh --provider <name> and stores up to 10 model IDs per provider for quick display src/channels/mod.rs:243-264. If no cache exists, users are instructed to run the refresh command.

Sources: src/channels/mod.rs:70-71, src/channels/mod.rs:243-264, src/channels/mod.rs:310-338

System Prompt and Channel-Specific Instructions

Each message processed through a channel includes a system prompt built from workspace identity files, tool descriptions, and optional channel-specific delivery instructions.

Channel Delivery Instructions

Some channels require special formatting for media attachments or other platform features:

// From src/channels/mod.rs:133-140
fn channel_delivery_instructions(channel_name: &str) -> Option<&'static str> {
    match channel_name {
        "telegram" => Some(
            "When responding on Telegram, include media markers..."
        ),
        _ => None,
    }
}

For example, Telegram supports inline media markers like [IMAGE:path-or-url], [DOCUMENT:path], etc., which are parsed and converted to proper API calls during message delivery src/channels/mod.rs:626-628.

Sources: src/channels/mod.rs:133-140, src/channels/mod.rs:626-628

System Prompt Construction

The full system prompt is built via build_system_prompt() src/channels/mod.rs:888-1041, which assembles:

  1. Tool descriptions (from build_tool_instructions)
  2. Safety guidelines
  3. Skills list (on-demand loading)
  4. Workspace directory
  5. Bootstrap files (AGENTS.md, SOUL.md, TOOLS.md, IDENTITY.md, USER.md, MEMORY.md)
  6. Current date/time
  7. Runtime metadata (host, OS, model)

Sources: src/channels/mod.rs:888-1041, src/agent/loop_.rs:1-100 (referenced for build_tool_instructions)

Configuration

Channels are configured in config.toml under the [channels_config] section. Each channel has its own subsection with platform-specific authentication and behavior settings.

ChannelsConfig Structure

// From src/config/schema.rs (simplified)
pub struct ChannelsConfig {
    pub telegram: Option<TelegramConfig>,
    pub discord: Option<DiscordConfig>,
    pub slack: Option<SlackConfig>,
    pub matrix: Option<MatrixConfig>,
    pub email: Option<EmailConfig>,
    pub whatsapp: Option<WhatsAppConfig>,
    pub lark: Option<LarkConfig>,
    pub mattermost: Option<MattermostConfig>,
    pub imessage: Option<IMessageConfig>,
    pub dingtalk: Option<DingTalkConfig>,
    pub qq: Option<QQConfig>,
    pub signal: Option<SignalConfig>,
    pub irc: Option<IrcConfig>,
}

Sources: src/config/schema.rs:1-143 (search for ChannelsConfig)

Example Configuration

[channels_config.telegram]
bot_token = "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
allowed_users = ["alice", "bob"]
mention_only = false
stream_mode = "on"
draft_update_interval_ms = 1000

[channels_config.discord]
bot_token = "MTA1N..."
guild_id = "123456789012345678"
allowed_users = ["987654321098765432"]
mention_only = true

[channels_config.email]
imap_host = "imap.gmail.com"
smtp_host = "smtp.gmail.com"
username = "bot@example.com"
password = "app-specific-password"
from_address = "bot@example.com"
allowed_senders = ["@example.com", "trusted@other.com"]

Configuration Wizard

The onboarding wizard provides an interactive setup flow for channel configuration:

// From src/onboard/wizard.rs (referenced)
pub fn run_wizard() -> Result<Config>

Users can run zeroclaw onboard --interactive to configure channels with guided prompts src/onboard/wizard.rs:61-195, or zeroclaw onboard --channels-only to update only channel settings without re-running the full onboarding flow src/onboard/wizard.rs:199-255.

Sources: src/onboard/wizard.rs:61-195, src/onboard/wizard.rs:199-255

Bootstrap and Workspace Files

Channel system prompts can inject workspace files to provide agent identity and context:

// From src/channels/mod.rs:856-870
let bootstrap_files = [
    "AGENTS.md",
    "SOUL.md", 
    "TOOLS.md",
    "IDENTITY.md",
    "USER.md"
];

These files, along with BOOTSTRAP.md (first-run ritual) and MEMORY.md (curated long-term memory), are loaded and included in the system prompt for every message src/channels/mod.rs:846-870. Each file is truncated to BOOTSTRAP_MAX_CHARS (20,000 characters by default) to prevent context overflow src/channels/mod.rs:59.

Sources: src/channels/mod.rs:59, src/channels/mod.rs:846-870, src/channels/mod.rs:888-1041


Clone this wiki locally