ADR-0063: Channel-scoped agent sockets and the bot relay
| Status | Accepted |
| Date | 2026-10-14 |
| Issue | #64, #67 |
| Pull request | (this PR) |
| Related | ADR-0024, ADR-0046, ADR-0062 |
Context and problem statement
Protocol v1 specified channel-scoped sockets (section 10) and the bot relay (section 9) but left them
Planned. Without them an AI backend can only be a webhook (request/response, no streaming, no
interrupts), mekik cannot run behind Converger with Converger as the transcript (#64 Mode B), and the
agent SDKs of #67 have nothing to connect to. The end user's bot-only frames (resume,
genui_event, ...) were answered with bot_unavailable.
Three questions had to be settled: what a conversation's "bot channel" is, how one connection follows thousands of conversations, and how several agent processes share a channel without answering one turn twice.
Decision drivers
- Reuse routing: which channel serves which conversation is already expressed by routing rules and reply-back routing (ADR-0046).
- One transcript and one
seq: the agent's frames are stored like any message (ADR-0062). - Work across nodes without new infrastructure.
- No duplicate replies when several agent processes connect.
Considered options
- Bot channel = routing target with a connected agent; a channel-scoped native session that
follows conversations on demand;
:pggroups with rendezvous hashing for sharing. - A per-conversation "bot channel" setting on the conversation or channel config.
- Sticky assignment stored in the database (a conversation → connection table).
Pros and cons of the options
Option 1
- Good, because no new configuration: a
websocketchannel routed from WhatsApp and a connected agent is the whole setup, and the same rules already route replies back. - Good, because
:pgand Phoenix PubSub already span the cluster. - Bad, because membership changes move conversations between members (only those of the member that came or went), and during a membership change two members may briefly both serve a conversation.
Option 2
- Good, because it is explicit.
- Bad, because it duplicates what routing rules say, and goes stale when rules change.
Option 3
- Good, because assignments survive restarts.
- Bad, because every turn needs a database lookup, and dead connections must be reaped.
Decision
Chosen option: "Option 1".
ConvergerWeb.Protocol.ChannelSessionruns inside the nativeSessionwhen the token hasscope: "channel"and no conversation. The token's channel must be awebsocketchannel.- Conversations are followed when they are listed in
hello.watermarks, when something is delivered for them to the channel, or when the agent sends a frame for them. - Access is the Phoenix binding's rule, now
ConvergerWeb.Protocol.Access: the conversation is owned (of this channel) or routed (a rule targets this channel from its channel). - Owned conversations receive every committed activity. Routed ones receive the pipeline's
deliveries, already through the channel's middleware; replays and gap fills go through
Pipeline.transform/2(the feed'spreparehook). - At most
max_conversationsare followed (least recently active evicted withtoo_many_subscriptions).
- Conversations are followed when they are listed in
- From the agent's view
fromisuserfor activities that came in on the conversation's origin channel, so the canonical activity now carriessource_channel_id. - Agent frames are stored through
Converger.Inboundwith the agent's channel as source, so reply-back routing delivers them. A messageidis kept, and the idempotency key (bot:<sender>:<conversation>:<clientId>) names the conversation, because mekik numbersmsg-1, msg-2, ...per conversation. ConvergerWeb.Protocol.BotRelay:- Bot channels: a conversation's bot channels are the active targets of enabled rules from its
origin channel that are
websocketchannels with a channel-scoped connection (ConvergerWeb.Sockets.count_agents/1), or whose adapter has the:bot_relaycapability (the mekik adapter). - To the bot: forwarded frames and a turn's
metaare broadcast on the channel topic asbot_frameandturn_meta. The meta goes ahead of the turn, keyed by its idempotency key. - To the end users: transient bot frames are broadcast on the conversation topic as
bot_transient. - Interrupt answers: a delivered user turn whose
replyTonames an open interrupt becomesresume.
- Bot channels: a conversation's bot channels are the active targets of enabled rules from its
origin channel that are
hello.groupjoins the:pggroup{channel_id, group}(scopeConvergerWeb.Protocol.AgentGroups). Every member computes the owner of a conversation as the member with the highestphash2({conversation_id, pid}), and only the owner delivers.
Consequences
Positive
- A mekik server, a LangChain agent or a human console serves WhatsApp, Telegram and web users through one socket. The SDKs of #67 and mekik's transport (mekik#47) build on it.
- Agent processes scale horizontally behind one channel.
Negative and trade-offs
- A bot's
errorframes reach every connection of the conversation, not only the one whose frame caused it (the bot does not say which). - Owned conversations are discovered only when listed, delivered or written to; a console finds the rest over REST.
- Group membership is eventually consistent across nodes.
- The canonical activity gains
source_channel_id(tenant API, outbound webhooks, PubSub broadcast).
Follow-ups
- #64 Mode A: the
mekikadapter (:bot_relay) dials a mekik server per conversation. - #67: the agent SDKs.
Implementation
lib/converger_web/protocol/channel_session.ex,bot_relay.ex,access.ex;session.ex(mode:channel, forwarding,bot_transient,meta);feed.ex(prepare);frames.ex(channel_welcome/1,envelope/2, agent viewer);converger_channel.ex(forwarding and relay on the Phoenix binding).ConvergerWeb.Sockets.count_agents/1,Converger.Activities.interrupt_open?/2, the:pgscope inConverger.Application,max_conversationsinConvergerWeb.Protocol.- Schemas:
frames/channel/agent-frame.schema.json(new), the client and server envelopes,hello.group. - Tests:
test/converger_web/protocol_channel_session_test.exsover a real socket, every frame validated against the schemas.
Links
- Converger Protocol v1, sections 9 and 10
- WebSocket channel type