Protocol overview
This section narrates mekik/1, the wire protocol both implementations speak. It is the companion to PROTOCOL.md — the normative spec in the repo — not a replacement.
Where prose and the golden fixtures disagree, the fixtures win. These pages explain the protocol; the fixtures are the protocol. If you're porting mekik to a third language, read
PROTOCOL.mdand the fixtures, and treat this section as the tour guide.
The protocol in one screen
Frames are JSON objects with a type discriminator, exchanged over WebSocket, one frame per message, UTF-8. There are two axes that organize everything:
Direction.
client → server: hello · text · resume · genui_event · abort
server → client: welcome · text · tool_call · genui · interrupt · interrupt_resolved · run · error
· <rich message> — any client message-renderer name (image, card, carousel, …)
Persistence. This is the axis that makes reconnect work:
PERSISTENT (carry a per-conversation seq, stored, replayed):
text · tool_call · genui · interrupt · interrupt_resolved
· <rich message> — the one open entry: any renderer-named type
TRANSIENT (live-only, never stored, never replayed):
welcome · run · error
A persistent frame is the durable record of the conversation. A transient one is a live signal that's meaningless after the fact. On (re)connect the server sends welcome, replays every persistent frame with seq > watermark, then resumes live delivery.
Full shapes: Frames.
The version and the compatibility rule
PROTOCOL_VERSION = "mekik/1"
It's announced in welcome.data.protocol. The compatibility contract is two sentences:
- A major bump (
mekik/2) is breaking. - Within a major, a receiver must ignore unknown fields and unknown frame
types.
That second rule is what lets a newer server add a field or a frame without breaking an older client — additive changes are always safe. Both implementations enforce it: an unknown type is dropped, not an error.
Identity in four ids
userId permanent the user's cross-conversation store
conversationId until deleted the transcript AND the ilmek threadId
connectionId one socket a routing handle for one live connection
watermark per client the highest persistent seq this client has durably seen
conversationId is the ilmek threadId — that identity is the hinge that ties a conversation to a resumable graph thread. Anonymous connect is allowed: assert nothing and the server mints userId / conversationId and returns them in welcome. Full model, including the reset-to-zero rule: Identity & resume.
The turn lifecycle
One run per conversation at a time. A text while a run is in flight draws error{busy}; a text while the thread is parked draws error{interrupted} (you must resume). The full rulebook is Engine & turn lifecycle.
The event→frame contract
The server side of mekik is, at its core, one pure function: eventToFrames maps an ilmek run's typed event stream to mekik frames. It's turn-stateful (owns the GenUI stream id and chunk counter) but conversation-stateless (the engine hands it the seq allocator and id minter). That purity is what the golden fixtures pin. The full mapping table: Event mapping.
What travels: the shared payload types
Two payload shapes are shared verbatim with chativa, so the widget renders them unchanged:
AIChunk — the unit of generative UI:
- TypeScript
- .NET
type AIChunk =
| { type: "ui"; component: string; props?: Record<string, unknown>; id?: string | number }
| { type: "text"; content: string; id?: string | number }
| { type: "event"; name: string; payload?: unknown; id?: string | number };
// .NET represents frames and chunks as Dictionary<string, object?> (parity §2) —
// the same JSON on the wire, no typed AIChunk. A "ui" chunk, for example:
new Dictionary<string, object?>
{
["type"] = "ui",
["component"] = "hello-card",
["props"] = new Dictionary<string, object?> { ["name"] = "Ada" },
};
MessageAction — an interrupt/quick-reply chip:
- TypeScript
- .NET
type MessageAction = { label: string; value?: unknown };
// when value is omitted, the answer is the label string
// A dictionary again; omit "value" to make the answer the label string.
new Dictionary<string, object?>
{
["label"] = "Approve",
["value"] = new Dictionary<string, object?> { ["approved"] = true },
};
Sections in this reference
- Frames — every frame's shape, direction, and persistence, with examples.
- Identity & resume — the four ids, the watermark, multi-tab fan-out, and the reset rule.
- Event mapping — the canonical
IlmekEvent→ frame table, and interrupt payload wrapping.
For the authoring side — how you cause these frames from inside a node — see Authoring → Helpers.