Expand description
Wire frames for the daemon UDS (PLAN-phase2 §2.9, S289 D-2 amended).
Framing is synaps rpc’s: one JSON object per \n, at most
DAEMON_MAX_FRAME_BYTES (64 MiB — distinct from rpc’s 1 MiB because
Attached carries the whole api_messages). Enforced symmetrically:
encode_line refuses to build an oversize frame, decode_line and both
readers refuse to accept one. WireSessionEvent is a serde mirror of
SessionEventWire — StreamEvent/TurnError/ExtensionLoaderEvent are
not Serialize, so they are mirrored variant-for-variant here and
converted only at the socket boundary. In-process transports never touch
this module.
The one lossy mirror is Conversation: on the wire it is a
ConversationDigest (len + hash + tokens/cost), never the messages.
Full history travels only in Attached and QueryResult{Messages};
SocketTransport rebuilds the snapshot from its local mirror
(Attached + Stream(MessageHistory)) and re-queries on a hash miss.
Security: nothing in here carries a credential. Welcome/Attached are
summaries; ClientFrame’s Debug redacts Answer.value and
Submit.text so a frame can be traced by type without leaking bodies.
Structs§
- Attached
Wire AttachSnapshotwith a serialisable replay.- Client
Reconnect - Who this connection used to be, so the daemon can restore ownership by
reconnect order +
was_owner. - Conversation
Digest ConversationSnapshotminusapi_messages: whatConversationcarries on the wire.messages_hashis FNV-1a over each message’s JSON so a client can tell whether its local mirror is current.- Hello
- Welcome
- Auth-status summary only — never key material.
- Wire
Envelope - Wire
Load Failure
Enums§
- Attach
- ByeReason
- Client
Frame - Client → daemon. First frame on a connection MUST be
Hello. - Daemon
Frame - Refuse
Reason - Wire
Agent Event - Wire
LlmEvent - Wire
Loader Event - Wire
Session Event - Wire
Session Stream Event - Wire
Stream Event
Constants§
- CHECKPOINT_
QUERY_ ID CheckpointrepliesQueryResult{id: CHECKPOINT_QUERY_ID, {ok:true}}soreload.rscan await it per session.- CLOSE_
QUERY_ ID_ BASE synaps daemon closesends the engine commandcloseunder a fresh id at or above this one per request (daemon/close.rs) and awaits that id’sQueryResult, so concurrent closes never take each other’s reply. Attached clients never see these.- DAEMON_
MAX_ FRAME_ BYTES - Hard cap on one daemon frame, both directions (
Attachedships the whole conversation; 64 MiB is well past any context window’s JSON). - DIGEST_
RESYNC_ QUERY_ ID SocketTransportre-fetchesapi_messagesunder this id on a digest miss.- IDLE_
PROBE_ QUERY_ ID - The daemon’s idle monitor probes
Statusunder this id. - MAX_
FRAME_ BYTES - Alias used by the readers/writers of this protocol.
- PROTOCOL_
MAX - PROTOCOL_
MIN - Exact-match policy today (
min == max == PROTOCOL_VERSION). - PROTOCOL_
VERSION - Separate from
RPC_PROTOCOL_VERSION; bump on any non-additive change. v2 (phase 3):Setbecame a struct variant,Byegained a reason. v3 (E-P0): driver events (DriverArmed,DriverRevoked,DriverTurnOutcome); old clients skip unknown via#[serde(other)]. Protocol stays exact-match; only binary versions compare directionally (reload, §2.8).Close/Closedwere added without a bump: an older daemon answers the unknownClientFramewithError{"malformed frame: unknown variantclose…"}(the client maps that to “daemon too old”), whereas a bump would refuse every older TUI that reconnects after adaemon reload. AdditiveClientFramevariants that only a new client sends follow this rule;Closedonly ever answersClose, so old clients never see it.Pin/Pinned(synaps daemon pin) follow the same rule, and so doesSessionMeta.pinned(#[serde(default)]: an older daemon omits it).ReloadExtensionand its two replies (ExtensionReloaded,ExtensionReloadFailed) follow it too. - RESERVED_
QUERY_ ID_ BASE - Query ids at or above this are reserved for the transport/daemon and
never surfaced to a client as
QueryResult. - RPC_
MAX_ FRAME_ BYTES - rpc’s cap, kept for reference; daemon frames use
DAEMON_MAX_FRAME_BYTES. - VIEW_
CHANGED_ SETTING_ ID SettingChanged{id}broadcast when an engine command (/model,/thinking, …) changed the view: not a reply to any client’sSet(client ids never reach the reserved range), only the new view, so every attached client mirrors it, not just the one that typed the command.
Functions§
- binary_
version - The binary version the daemon/client report in the handshake.
- build_
id - Which build of
binary_versionthis is, as set byset_build_id; empty when unknown (never set: library embedders, tests, packaged builds without git). Two builds of one release share the version, not this. - client_
decodes - Whether a client that declared
capscan decodeeventonce it is a wire frame. The enums nested underStreamhave no#[serde(other)], so an older client does not skip an unknown nested variant: the whole frame fails to decode,SocketTransport::next_eventtreats that as a dead socket and the TUI drops into reconnect — whoseAttachedreplay may carry the very same event and fail again. The daemon therefore drops, per connection, every event the client did not opt into (daemon/conn.rs, live pump and attach replay alike). - decode_
line - Decode one line, enforcing
DAEMON_MAX_FRAME_BYTES. - encode_
line - Encode one frame as a single line (no embedded newline: serde_json
escapes them). Refuses to produce a line over
DAEMON_MAX_FRAME_BYTES. - frame_
limit_ msg - Human-readable form of the cap for error frames.
- legacy_
shared - A client that predates
client_input(noowner_reason_input) on asharedsession. It knows only the owner rule, so the daemon presents the truth in those terms: it owns input (its attach snapshot says so, unless it observes) and never seesInputOwnerChanged. Otherwise it would announce “input is owned by …” on attach and “input taken over” whenever another client typed, while it can type all along. Matters most after adaemon reloadonto a new build, when every open TUI is an older client. - messages_
hash - FNV-1a 64 over the canonical JSON of every message (stable across
binaries, unlike
DefaultHasher). - set_
build_ id - Record this process’s build id. The
synapsbinary calls it first thing inmainwith the id its build script baked in (the short commit,-dirtywith uncommitted changes, orSYNAPS_BUILD_IDfrom the build environment). It lives in the root crate, not here, so a new commit does not recompile the engine. First call wins; later calls are ignored.