Implement Phase 3 channel adapters that decouple message sources from the agent via a uniform ChannelAdapter interface and ChannelRegistry. - Add ChannelAdapter/InboundMessage/OutboundMessage types - Add ChannelRegistry for adapter lifecycle and message routing - Add TelegramAdapter (grammy bot, auth middleware, confirmations, chunking) - Add WebChatAdapter (thin shim over GatewayServer) - Refactor daemon to use ChannelRegistry with per-channel-per-user agents - Add config.get/config.patch gateway handlers (Phase 2 loose end) - Add system.restart gateway handler (Phase 2 loose end) - Add implementation plans and design docs Tests: 225 passing (33 new channel adapter + gateway handler tests)
7.3 KiB
Phase 3: Channel Adapters — Implementation Plan
Goal
Introduce a ChannelAdapter abstraction that decouples message sources (Telegram, WebChat, future Discord/WhatsApp/Slack) from the agent. Each adapter handles platform-specific I/O and maps messages to a common interface. A ChannelRegistry manages adapter lifecycle and routes messages to/from the agent.
Scope (This Iteration)
- Channel types —
ChannelAdapterinterface,InboundMessage,OutboundMessage,ChannelStatus - Channel registry — Register, start/stop, route messages, adapter lifecycle
- Telegram adapter — Refactor existing
src/frontends/telegram/into aChannelAdapter - WebChat adapter — Wrap the existing gateway WS into a
ChannelAdapter - Daemon integration — Replace direct bot/gateway creation with registry-managed adapters
Discord, WhatsApp, and Slack adapters are deferred (require new dependencies + credentials).
Architecture
src/channels/
├── types.ts # ChannelAdapter interface, message types
├── registry.ts # ChannelRegistry: lifecycle + message routing
├── registry.test.ts # Registry tests
├── index.ts # Barrel exports
├── telegram/
│ ├── adapter.ts # TelegramAdapter implements ChannelAdapter
│ ├── adapter.test.ts # Adapter tests
│ └── index.ts # Barrel
└── webchat/
├── adapter.ts # WebChatAdapter implements ChannelAdapter
├── adapter.test.ts # Adapter tests
└── index.ts # Barrel
The existing src/frontends/telegram/ code (bot.ts, handlers.ts, confirmations.ts) stays in place and is wrapped by the adapter. The adapter delegates to the existing bot creation logic. No breaking changes to existing code.
Types Design
InboundMessage
interface InboundMessage {
id: string; // Platform message ID
channel: string; // Adapter name: "telegram", "webchat", etc.
senderId: string; // Platform user ID
senderName?: string; // Display name (optional)
text: string; // Message text
replyTo?: string; // ID of message being replied to
timestamp: number; // Unix ms
metadata?: Record<string, unknown>; // Platform-specific extras
}
OutboundMessage
interface OutboundMessage {
text: string; // Response text (markdown)
replyTo?: string; // Original message ID
metadata?: Record<string, unknown>; // Platform-specific extras (e.g. parse_mode)
}
ChannelAdapter
interface ChannelAdapter {
readonly name: string;
readonly status: ChannelStatus;
/** Start the adapter (connect to platform, begin listening). */
connect(): Promise<void>;
/** Stop the adapter (disconnect, clean up). */
disconnect(): Promise<void>;
/** Send a message to a specific peer. */
send(peerId: string, message: OutboundMessage): Promise<void>;
/** Register the inbound message handler. Called by registry. */
onMessage(handler: (msg: InboundMessage) => void): void;
/** Register a tool event handler for displaying tool execution status. */
onToolEvent?(handler: (peerId: string, event: ToolStatusEvent) => void): void;
}
type ChannelStatus = 'disconnected' | 'connecting' | 'connected' | 'error';
interface ToolStatusEvent {
type: 'start' | 'end';
tool: string;
args?: unknown;
result?: { success: boolean; output: string; error?: string };
}
ChannelRegistry
class ChannelRegistry {
register(adapter: ChannelAdapter): void;
unregister(name: string): void;
get(name: string): ChannelAdapter | undefined;
list(): ChannelAdapter[];
/** Start all registered adapters. */
startAll(): Promise<void>;
/** Stop all registered adapters. */
stopAll(): Promise<void>;
/** Set the message handler that all adapters route to. */
setMessageHandler(handler: (msg: InboundMessage, reply: (msg: OutboundMessage) => Promise<void>) => Promise<void>): void;
}
Telegram Adapter Design
The TelegramAdapter wraps the existing createTelegramBot() logic:
connect(): Creates grammy Bot, starts long pollingdisconnect(): Stops the botsend(): Callsbot.api.sendMessage(peerId, text, { parse_mode: 'Markdown' })onMessage(): Sets upbot.on('message:text', ...)to convert grammy context toInboundMessage- Preserves existing confirmations, commands (/start, /reset, /status, /local, /cloud, /model)
- Preserves chat ID allowlist check as middleware
- Tool status display: adapter handles the
onToolUseevents by posting/editing Telegram messages
Constructor takes:
interface TelegramAdapterConfig {
botToken: string;
allowedChatIds: number[];
hookEngine?: HookEngine;
}
The adapter does NOT take an agent directly — the registry routes messages to the agent.
WebChat Adapter Design
The WebChatAdapter is a thin shim since the gateway already handles WS connections.
connect(): No-op (gateway server is already running)disconnect(): No-op (gateway lifecycle managed by daemon)send(): Sends via the gateway's WS connection to the peeronMessage(): Hooks into the gateway's agent.send handler to intercept messages
Constructor takes:
interface WebChatAdapterConfig {
gateway: GatewayServer;
}
This adapter is simpler because the gateway already has its own session bridge and agent management. The adapter primarily exists to:
- Report WebChat as a registered channel in the registry
- Allow the daemon to manage all channels uniformly
- Provide status/metrics via a common interface
Daemon Integration
The daemon currently:
- Creates a grammy Bot directly
- Creates a GatewayServer directly
- Starts both independently
After refactor:
- Creates a ChannelRegistry
- Creates TelegramAdapter + WebChatAdapter
- Registers both with the registry
- Registry starts all adapters
The message handler in the registry creates per-channel agents via the session manager, same as the existing session bridge pattern.
Implementation Order
src/channels/types.ts— Pure types (no runtime)src/channels/registry.ts— Registry classsrc/channels/registry.test.ts— Registry unit testssrc/channels/telegram/adapter.ts— Telegram adaptersrc/channels/telegram/adapter.test.ts— Telegram adapter testssrc/channels/webchat/adapter.ts— WebChat adaptersrc/channels/webchat/adapter.test.ts— WebChat adapter testssrc/channels/index.ts+ sub-barrel exportssrc/daemon/index.ts— Wire registry- Run full test suite
Existing Code Impact
src/frontends/telegram/— NOT deleted. The adapter wraps these existing modules.src/gateway/— NOT modified. WebChat adapter wraps the existing gateway.src/daemon/index.ts— Modified to use ChannelRegistry.src/backends/native/agent.ts— NOT modified. Agent creation happens in the registry message handler.
Test Strategy
- Unit tests for ChannelRegistry (mock adapters)
- Unit tests for TelegramAdapter (mock grammy Bot)
- Unit tests for WebChatAdapter (mock GatewayServer)
- Existing tests remain unchanged (frontends/telegram, gateway)
Plan Version: 1.0 Created: 2026-02-05 Parent: docs/plans/2026-02-05-openclaw-parity-design.md Phase 3