# 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<string, unknown>;  // Platform-specific extras
}
```

### OutboundMessage
```typescript
interface OutboundMessage {
  text: string;                  // Response text (markdown)
  replyTo?: string;              // Original message ID
  metadata?: Record<string, unknown>;  // 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<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
```typescript
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 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*
