Aleph
Interfaces

Interfaces Overview

Multi-channel messaging architecture powering Aleph's polymorphic communication

Aleph communicates with users through interfaces (also called channels) — pluggable adapters that bridge messaging platforms to the Gateway's unified message bus. A single Aleph instance can simultaneously serve Telegram, Discord, iMessage, WebChat, CLI, and the rest of the supported channel family, all sharing the same agent brain, memory, and tool ecosystem.

Architecture

Every interface follows the same data flow:

Platform API ──> Interface Adapter ──> InboundMessage ──> ChannelRegistry

                                                    InboundMessageRouter
                                                  (single source: access,
                                                   pairing, allowlist,
                                                   session key, agent binding)

                                                            Harness

Platform API <── Interface Adapter <── OutboundMessage <── Response

The ChannelRegistry (src/gateway/channel_registry.rs) manages the lifecycle of all active interfaces. At startup it reads the channel configuration, instantiates each enabled interface via its ChannelFactory, calls start(), and exposes a shared broadcast::Receiver<InboundMessage> plus a typed outbound send path. From that point every inbound message lands on one queue, and every outbound reply is dispatched through the same adapter.

The InboundMessageRouter (src/gateway/inbound_router/) is the single source for access policy, pairing, allowlist enforcement, deduplication, group-chat handling, and session-key resolution. Each channel adapter hands the router a raw message; the adapter only performs a coarse pre-filter (drop obvious traffic before reaching the router); the router is the only place that mints pairing codes and decides who reaches the agent. Pairing state is persisted in the single PairingStore (src/gateway/pairing_store.rs).

The Channel Trait

All interfaces implement the Channel trait defined in src/gateway/channel.rs:738. It provides a uniform API regardless of the underlying platform:

#[async_trait]
pub trait Channel: Send + Sync {
    /// Channel metadata (id, name, type, status, capabilities)
    fn info(&self) -> &ChannelInfo;

    /// Shared mutable state (status + inbound broadcast sender/receiver)
    fn state(&self) -> &ChannelState;

    /// Channel id (default: `info().id`)
    fn id(&self) -> &ChannelId { &self.info().id }
    /// Channel type (default: `info().channel_type`)
    fn channel_type(&self) -> &str { &self.info().channel_type }
    /// Current status (default: `state().status()`)
    fn status(&self) -> ChannelStatus { self.state().status() }
    /// Current health probe (default: `state().health()`)
    async fn health(&self) -> ChannelHealth { self.state().health().await }
    /// Capability table (default: `info().capabilities`)
    fn capabilities(&self) -> &ChannelCapabilities { &self.info().capabilities }

    /// Approval delivery capability (None by default; channels opt in)
    fn approval_capability(&self) -> Option<Arc<dyn ChannelApprovalCapability>> { None }

    /// Pairing payload (QR, code, ...) for channels that surface pairing
    async fn get_pairing_data(&self) -> ChannelResult<PairingData> { Ok(PairingData::None) }
    /// Active (non-expired) pairing codes — `(code, remaining_ttl_secs)`
    async fn list_active_pairing_codes(&self) -> ChannelResult<Vec<(String, u64)>> { Ok(vec![]) }

    /// Start the channel (connect, authenticate, begin polling/listening)
    async fn start(&mut self) -> ChannelResult<()>;
    /// Stop the channel (disconnect, cleanup)
    async fn stop(&mut self) -> ChannelResult<()>;

    /// Send a message through this channel
    async fn send(&self, message: OutboundMessage) -> ChannelResult<SendResult>;

    /// Subscribe to inbound messages — multiple subscribers each get their own
    /// `broadcast::Receiver<InboundMessage>`
    fn inbound_subscribe(&self) -> broadcast::Receiver<InboundMessage> {
        self.state().inbound_subscribe()
    }

    /// Send a typing indicator
    async fn send_typing(&self, conversation_id: &ConversationId) -> ChannelResult<()>;
    /// Mark a message as read
    async fn mark_read(&self, message_id: &MessageId) -> ChannelResult<()>;
    /// React to a message
    async fn react(&self, conversation_id: &ConversationId,
                   message_id: &MessageId, reaction: &str) -> ChannelResult<()>;
    /// Edit a previously sent message
    async fn edit(&self, conversation_id: &ConversationId,
                  message_id: &MessageId, new_text: &str) -> ChannelResult<()>;
    /// Delete a message
    async fn delete(&self, conversation_id: &ConversationId,
                    message_id: &MessageId) -> ChannelResult<()>;

    /// List conversations the agent can address (read-only, routing metadata)
    async fn list_conversations(&self, query: &str, limit: usize)
        -> ChannelResult<ConversationPage>;

    /// Native streaming handler (channels with `StreamProtocol::Native`)
    fn native_stream_handler(&self) -> Option<Arc<dyn NativeStreamHandler>> { None }
}

Each interface also has a ChannelFactory that creates instances from JSON configuration:

#[async_trait]
pub trait ChannelFactory: Send + Sync {
    /// Channel type this factory creates (e.g. "telegram", "discord")
    fn channel_type(&self) -> &str;
    /// Create a channel instance from configuration
    async fn create(&self, config: serde_json::Value) -> ChannelResult<Box<dyn Channel>>;
}

Channel Capabilities

Not every platform supports every feature. Each channel declares a ChannelCapabilities struct (src/gateway/channel.rs:384) so the agent can adapt its behavior:

pub struct ChannelCapabilities {
    pub attachments: bool,
    pub images: bool,
    pub audio: bool,
    pub video: bool,
    pub reactions: bool,
    pub replies: bool,
    pub editing: bool,
    pub deletion: bool,
    pub typing_indicator: bool,
    pub read_receipts: bool,
    pub rich_text: bool,
    pub max_message_length: usize,    // 0 = unlimited
    pub max_attachment_size: u64,     // 0 = unlimited
    pub stream_protocol: StreamProtocol, // None | EditBased | Native
}

A separate StreamProtocol enum distinguishes channels that buffer until completion (None), edit a single draft message as tokens arrive (EditBased — Telegram, Discord), or handle streaming natively (Native — Teams).

Channel Comparison Table

FeatureTelegramDiscordiMessage (Local)iMessage (BlueBubbles)WebChatCLI
Rich text (Markdown)YesYesNoNoYesYes
AttachmentsYesYesYesYesYesNo
ImagesYesYesYesYesYesNo
Audio/VideoYesYesYesYesNoNo
ReactionsYesYesTapbacks (inbound)TapbacksNoNo
Reply threadingYesYesNoYesNoNo
Message editingYesYesNoNoNoNo
Message deletionYesYesNoNoNoNo
Typing indicatorYesYesNoYesVisualNo
Read receiptsNoNoNoYesNoNo
Inline keyboardsYesYesNoNoNoNo
Max message length4,0962,000~20,0004,000UnlimitedUnlimited
Max attachment size50 MB25 MB100 MB100 MBConfigurableN/A
Stream protocolEditBasedEditBasedNoneNoneEditBasedNone
Setup difficultyEasyMediummacOS-onlyMediumIncludedIncluded

Other channels shipped in the same adapter tree (src/gateway/interfaces/): CLI, Email, Feishu, IRC, Line, Matrix, Mattermost, MSteams, Nostr, QQ, Signal, Slack, Webhook, WeChat, WhatsApp, XMPP.

InteractionManifest

Beyond raw capabilities, Aleph uses an InteractionManifest system to inform the AI about what the current channel can do. Each channel belongs to an InteractionParadigm:

ParadigmChannelsCapabilities
CLICLIRich text, code highlighting, streaming
WebRichWebChat, Desktop appsFull interactive UI, canvas, mermaid charts, streaming
MessagingTelegram, Discord, iMessage, …Rich text, inline images
BackgroundCron jobs, webhooksSilent reply only
EmbeddedMinimal UI contextsNo special capabilities

The manifest tells the AI whether it can use Mermaid diagrams, interactive buttons, canvas drawing, or streaming — so it tailors its output format to what actually renders on the user's screen.

Session Key Resolution

When a message arrives on any interface, the Gateway resolves a SessionKey that determines which conversation history to load. Session keys encode the full context:

Session TypeKey FormatExample
Mainagent:{id}:mainagent:main:main
DM (per-peer)agent:{id}:dm:{peer}agent:main:dm:user123
DM (per-channel-peer)agent:{id}:{channel}:dm:{peer}agent:main:telegram:dm:user123
Groupagent:{id}:{channel}:group:{peer}agent:main:discord:group:guild456
Taskagent:{id}:{type}:{task}agent:main:cron:daily-summary
Ephemeralagent:{id}:ephemeral:{uuid}agent:main:ephemeral:a1b2c3

The DmScope strategy controls how direct messages are isolated:

  • Main — All DMs share the main session (conversation merges across users)
  • PerPeer — Each user gets their own session, shared across channels (default)
  • PerChannelPeer — Each user on each channel gets a separate session

Identity Links allow you to map the same person across channels. If user 123 on Telegram and user 456 on Discord are both "john", their DM sessions can be unified under agent:main:dm:john.

Route Binding

You can bind specific channels, users, or guilds to different agents using route bindings. The resolution follows a priority chain:

  1. Peer — Specific user/chat match (highest priority)
  2. Guild — Discord guild or Slack workspace match
  3. Team — Team-level match
  4. Account — Specific account match
  5. Channel — Channel-type match (e.g., all Telegram to one agent)
  6. Default — Falls back to the default agent
# Route all Telegram messages to the "personal" agent
[[bindings]]
agent_id = "personal"
[bindings.match_rule]
channel = "telegram"
account_id = "*"

# Route a specific Discord guild to the "work" agent
[[bindings]]
agent_id = "work"
[bindings.match_rule]
channel = "discord"
guild_id = "123456789"

Feature Compilation

Aleph compiles every channel adapter by default; channels are enabled via configuration (config.toml), not compile-time features. The iMessage local transport is gated by #[cfg(target_os = "macos")] — the BlueBubbles transport runs on any OS, so BlueBubblesChannel is available everywhere.

Channel Configuration

All channels are configured through the Aleph config file (~/.aleph/config.toml). Hot-reload applies changes within a short interval:

[channels.telegram]
enabled = true
token = "${TELEGRAM_BOT_TOKEN}"

[channels.discord]
enabled = true
token = "${DISCORD_BOT_TOKEN}"
allowed_guilds = [123456789]

[channels.imessage]
# Local (macOS only): SQLite polling of ~/Library/Messages/chat.db
# AND/OR
# BlueBubbles (any OS): REST + webhook against a BlueBubbles server
enabled = true
db_path = "~/Library/Messages/chat.db"
poll_interval_ms = 1000

Configuration supports ${ENV_VAR} expansion for secrets, so tokens never need to be stored in plaintext.

What's Next

Dive into the individual interface guides:

  • Telegram — teloxide-based bot
  • Discord — serenity-based bot
  • iMessage — macOS local + BlueBubbles transports
  • WebChat — WebAssembly Panel + Gateway WebSocket

On this page