# Channels Architecture

The channels module provides a transport-agnostic messaging layer for receiving and sending messages through external platforms. The design follows the same registry-plus-ABC pattern used throughout OpenJarvis: a `BaseChannel` interface defines the contract, and concrete implementations for each platform (Telegram, Discord, Slack, WhatsApp, etc.) are registered for runtime discovery.

---

## Design Principles

- **Transport-agnostic ABC.** `BaseChannel` defines six abstract methods covering the full lifecycle: connect, disconnect, send, status, list channels, and message handler registration.
- **Direct platform integration.** Each channel connects directly to its platform API -- there is no intermediate gateway.
- **Background listener thread.** Incoming messages are delivered via a daemon thread, not an event loop, so channels work from synchronous code without requiring async infrastructure.
- **Registry-driven discovery.** All channel implementations self-register via `@ChannelRegistry.register("name")` and are discoverable at runtime.

---

## BaseChannel ABC

```mermaid
classDiagram
    class BaseChannel {
        <<abstract>>
        +channel_id str
        +connect() None
        +disconnect() None
        +send(channel, content, conversation_id, metadata) bool
        +status() ChannelStatus
        +list_channels() list~str~
        +on_message(handler) None
    }
    class TelegramChannel {
        -_token str
        -_handlers list
        -_listener_thread Thread
        -_stop_event Event
        +connect() None
        +disconnect() None
        +send(...) bool
        +status() ChannelStatus
        +list_channels() list~str~
        +on_message(handler) None
    }
    class DiscordChannel {
        -_token str
        -_handlers list
        -_listener_thread Thread
        -_stop_event Event
    }
    class SlackChannel {
        -_bot_token str
        -_app_token str
        -_handlers list
    }
    BaseChannel <|-- TelegramChannel
    BaseChannel <|-- DiscordChannel
    BaseChannel <|-- SlackChannel
```

All `BaseChannel` subclasses must be registered via `@ChannelRegistry.register("name")` to be discoverable at runtime. For example, `TelegramChannel` is registered as `"telegram"`, `DiscordChannel` as `"discord"`, etc.

---

## Channel Lifecycle

The connection lifecycle for a typical channel implementation, from instantiation through to disconnection:

```mermaid
stateDiagram-v2
    [*] --> DISCONNECTED: __init__

    DISCONNECTED --> CONNECTING: connect() called
    CONNECTING --> CONNECTED: Platform connection OK\nlistener thread started
    CONNECTING --> CONNECTED: Platform SDK not installed\nsend-only mode
    CONNECTING --> ERROR: Exception during connect

    CONNECTED --> CONNECTING: listener loop error\nreconnect attempt
    CONNECTING --> CONNECTED: reconnect successful
    CONNECTING --> ERROR: reconnect failed

    CONNECTED --> DISCONNECTED: disconnect() called\nstop_event set\nthread joined
    ERROR --> DISCONNECTED: disconnect() called
```

The `ChannelStatus` enum (`CONNECTED`, `DISCONNECTED`, `CONNECTING`, `ERROR`) tracks this state and is exposed via `status()`.

---

## Listener Loop Pattern

Most channel implementations use a background daemon thread for receiving messages. The pattern is consistent across channels:

1. The listener thread is started in `connect()`.
2. It polls or listens for messages from the platform API.
3. Incoming messages are parsed into `ChannelMessage` dataclass instances.
4. All registered handlers are called sequentially.
5. If an `EventBus` is provided, a `CHANNEL_MESSAGE_RECEIVED` event is published.
6. On disconnect or error, the thread handles reconnection or exits cleanly.

Handler exceptions are caught individually so that a failing handler does not prevent subsequent handlers from running:

```python
for handler in self._handlers:
    try:
        handler(msg)
    except Exception:
        logger.exception("Channel handler error")
```

---

## Event Flow

Channel events are published to the `EventBus` using two event types:

| Event | Published By | When | Payload |
|-------|-------------|------|---------|
| `CHANNEL_MESSAGE_RECEIVED` | Listener loop | Message received from platform | `channel`, `sender`, `content`, `message_id` |
| `CHANNEL_MESSAGE_SENT` | `send()` | Message successfully delivered | `channel`, `content`, `conversation_id` |

These events allow other modules to react to channel activity without depending on the channel implementation directly. For example, a logging subscriber can record all sent and received messages, or an agent can be wired to respond to incoming channel messages by subscribing to `CHANNEL_MESSAGE_RECEIVED`.

```mermaid
flowchart TB
    A[TelegramChannel / DiscordChannel / ...] -->|CHANNEL_MESSAGE_RECEIVED| B[EventBus]
    A -->|CHANNEL_MESSAGE_SENT| B
    B --> C[TelemetryStore\nor other subscriber]
    B --> D[Custom handler\nvia bus.subscribe]
```

---

## Handler Registration

Multiple handlers can be registered. They are stored in a list and called sequentially within the listener thread. Returning a value from a handler has no effect on message routing -- the return type `Optional[str]` is reserved for future use (for example, auto-reply routing).

```python
# ChannelHandler type alias
ChannelHandler = Callable[[ChannelMessage], Optional[str]]
```

---

## Threading Model

Channel implementations use Python's `threading` module rather than asyncio. This is a deliberate choice: OpenJarvis's core inference path is synchronous, and daemon threads are simpler to compose with synchronous code than coroutines.

| Component | Thread | Notes |
|-----------|--------|-------|
| `connect()`, `send()`, `disconnect()` | Caller thread | All public methods are thread-safe |
| Listener loop | Background daemon thread | Started in `connect()`, joined in `disconnect()` |
| Handler callbacks | Background daemon thread | Called from listener thread -- use thread-safe data structures |

!!! warning "Handler thread safety"
    Handler callbacks run on the listener thread, not the thread that called `connect()`. If your handler modifies shared state, protect it with a lock or use thread-safe data structures such as `queue.Queue`.

---

## Adding a New Channel Backend

To add a new channel backend:

1. Create a new file in `src/openjarvis/channels/`.
2. Subclass `BaseChannel` and implement all six abstract methods.
3. Set `channel_id` as a class attribute.
4. Decorate with `@ChannelRegistry.register("name")`.
5. Add the module name to `_CHANNEL_MODULES` in `channels/__init__.py`.

```python
from openjarvis.channels._stubs import BaseChannel, ChannelMessage, ChannelStatus
from openjarvis.core.registry import ChannelRegistry

@ChannelRegistry.register("my_platform")
class MyPlatformChannel(BaseChannel):
    channel_id = "my_platform"

    def connect(self) -> None: ...
    def disconnect(self) -> None: ...
    def send(self, channel, content, *, conversation_id="", metadata=None) -> bool: ...
    def status(self) -> ChannelStatus: ...
    def list_channels(self) -> list[str]: ...
    def on_message(self, handler) -> None: ...
```

After registration, the backend is discoverable via `ChannelRegistry.get("my_platform")`.

---

## See Also

- [User Guide: Channels](../user-guide/channels.md) -- how to use channels in practice
- [API Reference: Channels](../api-reference/openjarvis/channels/index.md) -- complete class and type signatures
- [Architecture: Overview](overview.md) -- where channels fit in the overall system
- [Architecture: Design Principles](design-principles.md) -- registry pattern and ABC conventions
