docs/book/src/architecture/channel-runtime-lifecycle.md
Channels sit at the edge of ZeroClaw. They talk to chat platforms, webhooks, editors, and event sources, then hand normalized work to the agent runtime.
Use this page when a change touches channel listeners, gateway webhooks, message dispatch, reply intent, streaming drafts, per-channel reload, health/backoff behavior, or the boundary between platform-specific adapters and runtime-owned turn processing.
The target boundary is simple:
The current code is still in transition. zeroclaw-channels contains a
large orchestrator module with ChannelRuntimeContext,
run_message_dispatch_loop, and process_channel_message. That code
currently performs runtime-sized work: message routing, hooks, self-loop
guards, passive context, media/link enrichment, autosave, memory recall,
reply intent, tool-loop invocation, draft updates, cancellation, receipts,
cost tracking, and final delivery.
That is working code, not a reason to block every channel change. The review rule is narrower: new channel, webhook, or streaming work should reuse this shared lifecycle where possible and should not add another local mini-orchestrator.
| Surface | Owner | Review rule |
|---|---|---|
| Platform listener or channel inbound adapter | Channel module or channel plugin | Keep signature checks, payload decoding, platform retries, provider verification, challenge handling, and ChannelMessage construction local to the transport adapter. |
| Gateway webhook route | Gateway handler | Keep route hosting, proxying, timeout behavior, fast acknowledgement, and generic HTTP response policy local to the gateway. Do not grow new platform-specific parsing there except as documented transition debt. |
| Normalized inbound message | ChannelMessage from zeroclaw-api | Preserve sender, reply target, channel, alias, thread, attachments, subject, passive context, and conversation scope. Add structured metadata rather than hiding routing signals in user-visible text. |
| Agent ownership for a channel alias | start_channels / AgentRouter and active channel bindings | Resolve the owning agent from configured bindings. Do not silently fall back to an unrelated agent when a channel is unowned or disabled. |
| Message dispatch and cancellation | Shared channel dispatch loop | Reuse in-flight tracking, /stop, sender/thread cancellation, max in-flight limits, and worker concurrency. |
| Turn processing | Shared runtime/channel lifecycle | Hooks, self-loop guard, passive context, media/link enrichment, runtime commands, model routing, autosave, memory recall, reply intent, tool execution, receipts, cost, and delivery should live in one path. |
| Gateway webhook acknowledgement | Gateway handler | Fast-ack transports may return HTTP 200 before the model finishes, but the background work should still enter the shared channel lifecycle. |
| Channel health and reconnect | Listener supervisor | Retryable listener failures use bounded exponential backoff and cancellation-aware shutdown. Non-retryable failures should stop or surface clearly. |
| Runtime reload | Daemon reload and channel restart path | A config save is not enough. Long-running listeners adopt channel/provider/scheduler changes only when the daemon reloads or the process restarts. |
Long-running channels and webhook-backed channels have different transport entry points, but they should converge on the same message shape:
flowchart LR
A["Platform event"] --> B["Transport adapter"]
B --> C["ChannelMessage"]
C --> D["Channel dispatch loop"]
D --> E["Agent turn lifecycle"]
E --> F["Channel send / draft / reply"]
Adapters should keep the work that only the platform can understand:
After that, hand off a normalized ChannelMessage. Do not copy the rest of
the lifecycle into the adapter unless the exception is narrow, documented,
and tested.
The shared lifecycle should own behavior that must be consistent across channels:
Channel::self_handle() and
drop_self_messages;/new, /model, /models, /config, and
/stop;If a PR changes one of those responsibilities for only one channel, reviewers should ask whether it belongs in the shared lifecycle or in typed channel capability metadata.
Gateway webhooks have one legitimate special requirement: the HTTP request may need to return quickly even when the agent turn is slow. Nextcloud Talk is the clearest example because slow local models can exceed provider webhook timeouts.
That fast acknowledgement requirement should not make the gateway own a
separate agent lifecycle. Some current gateway-backed handlers still carry
duplicated parse -> autosave -> chat -> send chains. Treat that as migration
debt and transition context, not as the pattern for new webhook-backed channel
work. A webhook handler can:
ChannelMessage values;The message processing behind step 4 should reuse the channel lifecycle: autosave/session policy, agent dispatch, quickstart fallback, reply/error delivery, cancellation, and future ingress stamping should not be re-copied for each webhook-backed channel.
When reviewing webhook changes, compare synchronous handlers and fast-ack handlers separately:
parse -> autosave -> chat -> send
chain if a shared helper can own that work.Channel config can be saved before the running listener sees it. The daemon owns the long-lived subsystem graph, so channel listener changes apply when the daemon reloads or restarts the relevant subsystem. Standalone gateway starts may require process restart for channel listener changes.
Review reload-sensitive changes by checking:
config.toml;Streaming is a capability boundary. A channel may support draft edits, multi-message streaming, typing indicators, or only final-send behavior. The shared lifecycle decides how those capabilities are used during a turn.
Review streaming changes by asking:
/stop cancel the correct sender/thread scope?Long-running listeners must fail in a way operators can understand. Retryable platform failures should back off and retry; non-retryable configuration or authentication failures should surface clearly instead of looping forever.
For listener changes, prove the relevant path:
For channel, webhook, or channel-runtime changes, answer these before reviewer sign-off:
ChannelMessage?/stop,
no-reply, and send failure?Canonical docs:
Key code entry points:
crates/zeroclaw-api/src/channel.rscrates/zeroclaw-api/src/ingress.rscrates/zeroclaw-channels/src/orchestrator/mod.rscrates/zeroclaw-runtime/src/agent/turn/crates/zeroclaw-runtime/src/agent/loop_.rscrates/zeroclaw-gateway/src/lib.rs