Channels and Gateway
Corresponding source
| Subject | Source |
|---|---|
| Channel abstraction contract | nanobot/channels/base.py::BaseChannel |
| Discovery, registration, and lifecycle | nanobot/channels/registry.py, nanobot/channels/manager.py::ChannelManager |
| Pairing and access control | nanobot/pairing/store.py |
| Gateway startup and runtime | nanobot/cli/commands.py::_run_gateway, nanobot/gateway/runtime.py, nanobot/gateway/service.py |
| Streaming and reasoning adaptation | nanobot/channels/manager.py and concrete Channel implementations |
Learning objectives
After this lesson, you should be able to:
- explain the minimum BaseChannel contract;
- describe discovery of built-in and external Channels;
- explain the ordering of allowFrom, Pairing, and inbound publication;
- describe how ChannelManager starts, stops, and delivers outbound messages;
- explain why Gateway is the long-running assembly entry point.
Prerequisites
- Understand MessageBus.
- Understand InboundMessage and OutboundMessage.
- Read Cron and Heartbeat.
1. BaseChannel is an adapter contract
Location:
nanobot/channels/base.py::BaseChannel
Required methods:
start()stop()send(OutboundMessage)
Optional overrides:
login()send_delta()send_reasoning_delta()/send_reasoning_end()send_file_edit_events()default_config()
start() should be a long-running operation that connects to the platform, receives messages, and passes them through _handle_message() to the shared inbound path.
2. Inbound messages pass access control first
BaseChannel._handle_message():
flowchart TD M["Platform message"] --> A{"Sender allowed?"} A -->|No, DM| P["Generate Pairing code<br/>send instructions directly"] A -->|No, not DM| D["Reject and log"] A -->|Yes| S{"Channel supports streaming?"} S -->|Yes| W["Add _wants_stream to metadata"] S -->|No| I["Create InboundMessage"] W --> I I --> B["bus.publish_inbound"]
is_allowed() precedence:
allowFromcontains*;- sender ID exactly matches an allowlist entry;
- Pairing store has approved the sender;
- otherwise reject.
Security implications:
allowFromentries are opaque tokens compared exactly;- without
allowFrom, a Channel may operate in pairing-only mode; - a DM can return a pairing code;
- a group chat must not expose a pairing code publicly.
3. Streaming capability is configuration intersected with implementation
supports_streaming is true only when:
- Channel configuration enables streaming;
- the subclass actually overrides
send_delta().
Writing streaming: true in configuration does not create streaming support.
A stateful Channel should separate segments with _stream_id; accumulating only by chat_id can join unrelated segments.
4. Channel discovery
Built-in Channels:
- enumerate modules under
nanobot.channelswithpkgutil.iter_modules; - import only enabled entries, reducing failures from optional dependencies.
External Channels:
- use Python entry-point group
nanobot.channels.
Merge rule:
- built-in implementations win;
- a plugin cannot silently replace a built-in Channel with the same name.
Related source:
nanobot/channels/registry.py
5. ChannelManager initialization
ChannelManager._init_channels():
- discovers candidate names;
- merges plugin keys from Pydantic extra configuration;
- selects enabled Channels;
- imports only those classes;
- constructs additional Gateway services for WebSocket;
- resolves progress, tool-hint, and reasoning overrides;
- validates allowFrom;
- stores the instances.
If one optional Channel cannot initialize, Manager logs a warning rather than preventing every Channel from starting.
6. Start and stop
start_all():
- creates the outbound-dispatcher Task;
- creates one long-running Task per Channel;
- waits with gather;
- expects a healthy Channel
start()not to return immediately.
stop_all():
- cancels the dispatcher first;
- waits for cancellation;
- calls each Channel’s
stop(); - respects real Task cancellation.
7. Outbound dispatcher
_dispatch_outbound() continuously consumes MessageBus outbound and routes by metadata:
- complete messages;
- content deltas;
- stream end;
- reasoning delta/end;
- file-edit events;
- progress/tool hints.
It also handles:
- selecting the target Channel;
- retries;
- delta coalescing;
- duplicate-message suppression;
- failed-send buffering and logs.
Channel send() should raise on failure so Manager can apply retry centrally instead of silently swallowing the error.
8. Delivery retry
ChannelManager centralizes outbound-delivery retries. Current configuration:
channels.sendMaxRetries
Verify the delay sequence and actual attempt count in _send_with_retry().
This retry is distinct from Provider retry:
Provider retry: model request failed
Channel retry: final message delivery failed9. Gateway assembly
nanobot/cli/commands.py::_run_gateway creates:
- MessageBus;
- RuntimeEventBus;
- Provider snapshot;
- SessionManager;
- CronService;
- AgentLoop;
- WebuiTurnCoordinator;
- ChannelManager.
It then starts AgentLoop, Cron, Channels, and the optional health server.
Gateway is not another Agent. It is the process entry point that gives these long-running components one shared lifecycle.
10. Recommended debugging order
If a Channel does not respond:
- Does
nanobot agent -msucceed? - Is the Channel enabled?
- Can optional dependencies import?
- Is
start()still running? - Did the sender pass allowFrom/Pairing?
- Did
_handle_message()publish inbound? - Did AgentLoop consume it?
- Did outbound reach the dispatcher?
- Did
send()raise and retry? - Did the platform API accept the message format?
Source and tests
nanobot/channels/base.pynanobot/channels/registry.pynanobot/channels/manager.pynanobot/pairing/store.pynanobot/cli/commands.py::_run_gatewaytests/channels/test_base_channel.pytests/channels/test_channel_plugins.pytests/channels/test_channel_manager_delta_coalescing.pytests/channels/test_channel_manager_reasoning.pytests/pairing/
Self-check
- Why must
_handle_message()perform access control before publishing? - Why import only enabled Channels?
- Why is enabling streaming in configuration insufficient?
- Why must Provider retry and Channel retry remain separate?
- Why is Gateway an assembly entry point rather than another Agent?
Next step
Read HTTP API and Python SDK.