This document reflects the actual frontend behavior.
All protocol messages use this envelope:
{
"v": 1,
"type": "event_name",
"session_id": "string",
"agent_id": "optional",
"request_id": "optional",
"payload": {}
}Validation in src/lib/protocol/client.svelte.ts requires:
v === 1- known event
type - non-empty
session_id
Invalid envelopes are ignored.
pairing_requestuser_messageapproval_response
pairing_resultassistant_chunkassistant_finaltool_calltool_resultapproval_requesterror
- UI opens WebSocket endpoint.
- Once client reaches
pairing, UI sendspairing_requestwith:pairing_code(6 digits)client_pub(base64url X25519 public key)
- Core responds with
pairing_resultcontaining:access_token- optional
expires_in - optional
e2e.agent_pub
- UI derives shared key and enables E2E mode.
- Auth + shared key are persisted in local storage.
Implementation: src/lib/protocol/e2e.ts.
- Key exchange:
X25519via WebCrypto. - Key derivation:
SHA-256("webchannel-e2e-v1" || shared_secret). - Symmetric encryption:
ChaCha20-Poly1305. - Nonce: random 12-byte value.
E2E payload format:
{
"nonce": "base64url",
"ciphertext": "base64url"
}sendMessage(content) behavior:
- If E2E key exists: wrap plaintext object (
content,sender_id) and encrypt topayload.e2e. - If E2E key is not available: send
payload.contentin plaintext mode.
For assistant_* events, if payload.e2e is present, client attempts decryption.
If decryption fails, the event is still processed safely without crashing the UI.
For type = "error", expected payload shape:
{
"message": "string",
"code": "optional string"
}Malformed error payloads produce a local client-side error event.
When payload.code === "unauthorized":
- client clears in-memory auth (
accessToken, E2E key) - controller clears persisted auth
- session store is reset
Reconnect is attempted only when all conditions hold:
- socket closes unexpectedly
- previous state was
pairedorchatting - access token exists
- reconnect is enabled (
shouldReconnect = true)
Backoff strategy:
- base delay: 1000 ms
- exponential growth up to 30 s
- jitter: 50-100% of computed delay
request_id is used to correlate:
tool_call<->tool_resultapproval_request<->approval_response
If a tool_result arrives without request_id, store falls back to the latest unresolved tool call.