-
Notifications
You must be signed in to change notification settings - Fork 4.4k
06 Channels
Relevant source files
The following files were used as context for generating this wiki page:
- src/channels/cli.rs
- src/channels/dingtalk.rs
- src/channels/discord.rs
- src/channels/email_channel.rs
- src/channels/imessage.rs
- src/channels/lark.rs
- src/channels/matrix.rs
- src/channels/mod.rs
- src/channels/slack.rs
- src/channels/telegram.rs
- src/channels/traits.rs
- src/channels/whatsapp.rs
- src/config/schema.rs
- src/onboard/wizard.rs
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:
- The trait interface and message dispatching architecture, see Channel Architecture
- Individual platform implementations, see Channel Implementations
- Security controls and access management, see Channel Security
- Per-sender model selection, see Runtime Model Switching
Sources: src/channels/mod.rs:1-32, src/channels/traits.rs:1-103
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
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
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
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
Sources: src/channels/mod.rs:471-509, src/channels/mod.rs:556-814, src/channels/mod.rs:816-844
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::healthmodule - Restart counter tracking
Sources: src/channels/mod.rs:61-69, src/channels/mod.rs:471-509
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
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
// 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
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<Mutex<HashMap<String, Vec<ChatMessage>>>>"]
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
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
// 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
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,
) -> StringThis 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
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.
| 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
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)
Sources: src/channels/mod.rs:630-686, src/channels/mod.rs:698-720
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,
) -> SelfThe 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
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
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.
| 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
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)"]
Sources: src/channels/mod.rs:142-144, src/channels/mod.rs:146-184, src/channels/mod.rs:365-441
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
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
Each message processed through a channel includes a system prompt built from workspace identity files, tool descriptions, and optional channel-specific 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
The full system prompt is built via build_system_prompt() src/channels/mod.rs:888-1041, which assembles:
- Tool descriptions (from
build_tool_instructions) - Safety guidelines
- Skills list (on-demand loading)
- Workspace directory
- Bootstrap files (AGENTS.md, SOUL.md, TOOLS.md, IDENTITY.md, USER.md, MEMORY.md)
- Current date/time
- Runtime metadata (host, OS, model)
Sources: src/channels/mod.rs:888-1041, src/agent/loop_.rs:1-100 (referenced for build_tool_instructions)
Channels are configured in config.toml under the [channels_config] section. Each channel has its own subsection with platform-specific authentication and behavior settings.
// 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)
[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"]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
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