ADR-0065: chativa as the reference client, a mekik/1 profile, and a seq cursor for history
| Status | Accepted |
| Date | 2026-10-14 |
| Issue | #65 (client side: AimTune/chativa#86) |
| Pull request | (this PR) |
| Related | ADR-0024, ADR-0062, ADR-0018, ADR-0006 |
Context and problem statement
ADR-0024 made Converger Protocol v1 a superset of
mekik/1 so that chativa's @chativa/connector-mekik would work unchanged. Nothing checked it. Running chativa's
real connector against the native endpoint showed two breakages and three gaps:
- Converger-only frames render as chat bubbles. mekik/1's open rule (section 4.5) says an unknown frame type
is a rich message rendered by name. connector-mekik follows it: every frame that is not one of its own falls
through to
onMessage. chativa stamps every outgoing message with anid, Converger treats that as aclientIdand answers withack, thendeliveryStatus; presence of other participants arrives aspresence. Each of those became an empty bubble. The v1 text claimed mekik/1 clients "ignore the additions by the unknown-frame rule", which is not what mekik/1 says. - Idle widgets were disconnected. The server closes a connection after 60 s without inbound frames (4408).
mekik/1 has no
pingframe, and the server's textheartbeatwas a bubble, not a keepalive. - chativa's
loadHistory(cursor)opens at the newest messages and pages backwards; the REST list only paged forward from a watermark, and its activities carried noseq. - Agent-panel mode needs a conversation list; the client API had none (the tenant API's list needs the tenant API key and lists every channel).
sendFileneeds the attachment message the upload created, in the shape the socket delivers.
The issue also asks that a broken chativa blocks Converger protocol changes in CI.
Decision drivers
- An unmodified, released chativa connector must work: no Converger-specific code in connector-mekik.
- Full-protocol clients (
@converger/client, the SDKs, the future@chativa/connector-converger) keep every frame. - The compatibility check runs chativa's own code, not a re-implementation of its rules, and needs no browser.
- REST stays additive: existing request and response shapes keep working.
Considered options
For the frames:
- A mekik/1 profile on the native endpoint, selected by "no subprotocol and no
hello.protocol". - Fix it in chativa: make connector-mekik drop unknown frames.
- Stop sending the additions to everyone unless a client opts in.
For the compatibility check:
- Vendor chativa's TypeScript and run it in Node (type stripping) against a real server from ExUnit.
- Headless browser running the chativa sandbox in CI.
- Port
parseChatFrameto Elixir and test against the port.
For history:
before=<seq>/direction=backwardon the existing list,cursor= seq.- A new endpoint for backward paging with an opaque cursor.
Pros and cons of the options
mekik/1 profile
- Good, because unmodified mekik/1 clients get exactly the mekik/1 frame set, and every other client is untouched.
- Good, because the selector is what a mekik/1 client already does (mekik/1 defines no subprotocol, its
hellohas noprotocol), while every Converger SDK already offersconverger.v1and sendshello.protocol. - Bad, because a hand-written client that offers no subprotocol and omits
hello.protocolnow losesack; it must send either to get the full protocol.
Fix it in chativa
- Bad, because it contradicts mekik/1's open rule, which chativa implements correctly, and every other mekik/1 client would hit the same problem.
Opt-in additions for everyone
- Bad, because it changes the default for the clients that already rely on
ackand receipts.
Vendored chativa in Node
- Good, because it runs chativa's real
MekikConnector(handshake, watermark, quick replies, interrupts, tool calls, GenUI routing) over a real socket in about a second per test, with no build step and no npm install. - Bad, because the vendored copy must be refreshed by hand when chativa changes (documented in its README).
Headless browser
- Bad, because it needs a chativa build, a browser and a sandbox server in CI, for no extra coverage of the wire.
Elixir port
- Bad, because a port tests our reading of chativa, not chativa.
Decision
Chosen: the mekik/1 profile, vendored chativa in Node, and backward paging on the existing list.
- mekik/1 profile. A native-endpoint connection that negotiated no WebSocket subprotocol and whose
hellohas noprotocol(or"mekik/1") does not receiveack,deliveryStatus,presence,tokenRefreshedorreplayTruncated; itsheartbeatis a WebSocket ping (the pong keeps it inside the idle timeout);token_expiredandtoken_revokedare reported with the mekik/1 codeunauthorized(number and close code unchanged), so a mekik/1 auth provider refreshes. Persistent frames are unchanged. Spec: Protocol v1 section 14.1. - Compatibility suite. chativa's
ChatFrame.ts,GenUIComponentCache.tsand connector-mekik'sindex.tsandauth.tsare vendored at a pinned commit undertest/protocol/fixtures/chativa/(only two import specifiers rewritten).test/protocol/chativa_compat_test.exs(tags:protocol,:chativa) drives the realMekikConnectorthrough a small Node harness against a Bandit-served endpoint and asserts what chativa's ChatEngine receives. It runs in the normal test run when Node >= 22.18 is on the PATH, is skipped otherwise, and fails instead whenCHATIVA_COMPAT=required, which CI sets. A dedicated CI job, "chativa reference client compatibility", runs it with Node 24, so a protocol change that breaks chativa fails a named check. - History cursor.
GET /api/v1/converger/conversations/:id/activitiesacceptsbefore=<seq>anddirection=backward: the page just beforebefore(or the newest page), still oldest first, withcursor(the oldest seq of the page, a string) for the next older page. Activities now carryseq.format=framesreturns the page as Protocol v1 frames (ConvergerWeb.Protocol.Frames.activity/3, relative to the token'suser_idor?userId=), which chativa routes like live frames. The seq is the cursor because it is already the protocol's public, gap-free position (ADR-0006); an opaque cursor would only hide it. - Conversation list.
GET /api/v1/converger/conversationslists the token's channel, most recently active first (keyset on(updated_at, id), new indexconversations (channel_id, updated_at, id)), withstatusandupdated_sincefilters and a preview of the latest message (one lateral query). It needs ascope: "channel"token: an end user's token must not enumerate other users' conversations. Field names follow chativa'sConversationwhere one exists.POST /conversationsaccepts an optionaltitle. - Upload result. The upload response adds
seq, the attachmentname, andframe: the attachment message as the socket delivers it. REST sends withoutfrom.id(activities and uploads) are stored with the token'suser_idas sender, so they render asfrom: "user"for that user, as socket sends do.
Consequences
Positive
- chativa's widget works against Converger with the published connector-mekik; the CI job keeps it working.
@chativa/connector-converger(chativa#86) has every REST endpoint it needs: history, conversation list, upload result, receipts on the full protocol.- Idle mekik/1 widgets stay connected.
Negative and trade-offs
- A client without subprotocol and without
hello.protocolno longer receivesackor receipts. All Converger SDKs and examples send both, and the spec now says how to opt in. - The vendored chativa copy can drift from chativa's main branch; it is refreshed deliberately, with its SHA in the README.
- Keyset paging on
updated_atcan show a conversation twice or skip it when it becomes active between pages; the list is a starting point, and live state comes from the socket.
Follow-ups
- #64: the channel-scoped native socket (
{conversationId, frame}envelopes) for a single agent-panel connection, theagentparticipant role, and the live relay of a mekik bot (run,genui_components,skills,resume), which the suite should then cover end to end. - AimTune/chativa#86:
@chativa/connector-convergeron top of these endpoints, and a chativa sandbox preset pointed at Converger.
Implementation
- Profile:
ConvergerWeb.Protocol.MekikProfile, applied inConvergerWeb.ProtocolSocket(which now receives the negotiated subprotocol fromConvergerWeb.ProtocolSocketController);ConvergerWeb.Protocol.Sessionrecordshello.protocol. Tests:test/converger_web/protocol_socket_test.exs("mekik/1 profile"). - History and list:
ConvergerWeb.ConvergerAPI.ActivityController.index/2,ActivityJSON,ConversationController.index/2,Converger.Activities.last_messages/1, theupdated_sincefilter ofConverger.Conversations.paginate_conversations/2, migration20261014210000. OpenAPI schemasActivitySet,ClientActivity,ClientConversationList,ClientConversationListItem,ClientConversationRequest,UploadResponse. Tests:test/converger_web/controllers/client_history_and_conversations_test.exs,api_contract_test.exs. - Compatibility suite:
test/protocol/chativa_compat_test.exs,test/protocol/fixtures/chativa/, CI jobchativain.github/workflows/ci.yml(and Node 24 withCHATIVA_COMPAT=requiredin the coverage job).
Links
- Connecting chat clients (chativa)
- Protocol v1, sections 2.1, 5.4, 8.5, 14.1 and 15
- Client API: list conversations, history paging backwards, upload
- chativa, mekik/1