Skip to main content

Module wire

Module wire 

Source
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§

AttachedWire
AttachSnapshot with a serialisable replay.
ClientReconnect
Who this connection used to be, so the daemon can restore ownership by reconnect order + was_owner.
ConversationDigest
ConversationSnapshot minus api_messages: what Conversation carries on the wire. messages_hash is 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.
WireEnvelope
WireLoadFailure

Enums§

Attach
ByeReason
ClientFrame
Client → daemon. First frame on a connection MUST be Hello.
DaemonFrame
RefuseReason
WireAgentEvent
WireLlmEvent
WireLoaderEvent
WireSessionEvent
WireSessionStreamEvent
WireStreamEvent

Constants§

CHECKPOINT_QUERY_ID
Checkpoint replies QueryResult{id: CHECKPOINT_QUERY_ID, {ok:true}} so reload.rs can await it per session.
CLOSE_QUERY_ID_BASE
synaps daemon close sends the engine command close under a fresh id at or above this one per request (daemon/close.rs) and awaits that id’s QueryResult, 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 (Attached ships the whole conversation; 64 MiB is well past any context window’s JSON).
DIGEST_RESYNC_QUERY_ID
SocketTransport re-fetches api_messages under this id on a digest miss.
IDLE_PROBE_QUERY_ID
The daemon’s idle monitor probes Status under 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): Set became a struct variant, Bye gained 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/Closed were added without a bump: an older daemon answers the unknown ClientFrame with Error{"malformed frame: unknown variant close…"} (the client maps that to “daemon too old”), whereas a bump would refuse every older TUI that reconnects after a daemon reload. Additive ClientFrame variants that only a new client sends follow this rule; Closed only ever answers Close, so old clients never see it. Pin/Pinned (synaps daemon pin) follow the same rule, and so does SessionMeta.pinned (#[serde(default)]: an older daemon omits it). ReloadExtension and 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’s Set (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_version this is, as set by set_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 caps can decode event once it is a wire frame. The enums nested under Stream have no #[serde(other)], so an older client does not skip an unknown nested variant: the whole frame fails to decode, SocketTransport::next_event treats that as a dead socket and the TUI drops into reconnect — whose Attached replay 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 (no owner_reason_input) on a shared session. 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 sees InputOwnerChanged. 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 a daemon reload onto 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 synaps binary calls it first thing in main with the id its build script baked in (the short commit, -dirty with uncommitted changes, or SYNAPS_BUILD_ID from 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.