# 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) 1. **Channel types** — `ChannelAdapter` interface, `InboundMessage`, `OutboundMessage`, `ChannelStatus` 2. **Channel registry** — Register, start/stop, route messages, adapter lifecycle 3. **Telegram adapter** — Refactor existing `src/frontends/telegram/` into a `ChannelAdapter` 4. **WebChat adapter** — Wrap the existing gateway WS into a `ChannelAdapter` 5. **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 ```typescript 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; // Platform-specific extras } ``` ### OutboundMessage ```typescript interface OutboundMessage { text: string; // Response text (markdown) replyTo?: string; // Original message ID metadata?: Record; // Platform-specific extras (e.g. parse_mode) } ``` ### ChannelAdapter ```typescript interface ChannelAdapter { readonly name: string; readonly status: ChannelStatus; /** Start the adapter (connect to platform, begin listening). */ connect(): Promise; /** Stop the adapter (disconnect, clean up). */ disconnect(): Promise; /** Send a message to a specific peer. */ send(peerId: string, message: OutboundMessage): Promise; /** 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 ```typescript class ChannelRegistry { register(adapter: ChannelAdapter): void; unregister(name: string): void; get(name: string): ChannelAdapter | undefined; list(): ChannelAdapter[]; /** Start all registered adapters. */ startAll(): Promise; /** Stop all registered adapters. */ stopAll(): Promise; /** Set the message handler that all adapters route to. */ setMessageHandler(handler: (msg: InboundMessage, reply: (msg: OutboundMessage) => Promise) => Promise): void; } ``` ## Telegram Adapter Design The `TelegramAdapter` wraps the existing `createTelegramBot()` logic: - `connect()`: Creates grammy Bot, starts long polling - `disconnect()`: Stops the bot - `send()`: Calls `bot.api.sendMessage(peerId, text, { parse_mode: 'Markdown' })` - `onMessage()`: Sets up `bot.on('message:text', ...)` to convert grammy context to `InboundMessage` - Preserves existing confirmations, commands (/start, /reset, /status, /local, /cloud, /model) - Preserves chat ID allowlist check as middleware - Tool status display: adapter handles the `onToolUse` events by posting/editing Telegram messages Constructor takes: ```typescript 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 peer - `onMessage()`: Hooks into the gateway's agent.send handler to intercept messages Constructor takes: ```typescript 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: 1. Report WebChat as a registered channel in the registry 2. Allow the daemon to manage all channels uniformly 3. Provide status/metrics via a common interface ## Daemon Integration The daemon currently: 1. Creates a grammy Bot directly 2. Creates a GatewayServer directly 3. Starts both independently After refactor: 1. Creates a ChannelRegistry 2. Creates TelegramAdapter + WebChatAdapter 3. Registers both with the registry 4. 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 1. `src/channels/types.ts` — Pure types (no runtime) 2. `src/channels/registry.ts` — Registry class 3. `src/channels/registry.test.ts` — Registry unit tests 4. `src/channels/telegram/adapter.ts` — Telegram adapter 5. `src/channels/telegram/adapter.test.ts` — Telegram adapter tests 6. `src/channels/webchat/adapter.ts` — WebChat adapter 7. `src/channels/webchat/adapter.test.ts` — WebChat adapter tests 8. `src/channels/index.ts` + sub-barrel exports 9. `src/daemon/index.ts` — Wire registry 10. 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*