v0.7.0 4c16dd8

protocol.md RawHistoryFolder

Vev session protocol

This document defines the observable Protobuf session contract and its QUIC, Unix, and SSH stdio carriage rules. Each invariant maps to a current test or benchmark.

Message directions

internal/protocol/messages.go closes both unions. The daemon rejects a wrong-direction frame before any mutation (sessionwire.ErrWrongDirection → protocol.DecodeWrongDirection).

Client → server: Hello, Input, Resize, Detach, Ping, List, Kill, Theme, Ack, ImagePush, ClientNotice, CommandRequest, OutputResetRequest, SelectTab, TerminalFocus, RemotePreviewWatch, RouteAttentionSubscription, SamePeerSwitchRequest, RecentRouteSnapshot, RouteNavigationFailure, SessionCreationFailure, UIFence, NavigationInventoryRequest, NavigationInventoryPublication, NavigationInventoryFailure, PickerClose, PickerSelection.

Server → client: Welcome, ErrorMsg, Output, Detached, Pong, Sessions, CommandResult, AttachTarget, RemotePreview, CommittedRouteIdentity, RouteNavigationAction, RouteCreateSessionAction, RouteNavigationFailure, RoutePosition, RouteRetired, SamePeerSwitchFailure, UIReceipt, UIViewUpdate, NavigationInventoryResponse, NavigationInventoryDemand, NavigationInventorySelection, PickerOffer, PickerSnapshot, PickerClosed, PickerResult, PickerFailure, KillResult.

RouteNavigationFailure is the one payload that appears in both unions. sessionwire conversion switches (decodeProtoClient/decodeProtoServer, encodeProtoClient/encodeProtoServer) are exhaustive over the directional ClientEnvelope/ServerEnvelope oneof unions; unknown or opposite-direction payloads fail with DecodeUnknownType / DecodeMalformed before any mutation.

Handshake timeline

One absolute 15-second budget (protocol.HandshakeTimeout) covers dial through the first committed publication, on both ends. Every connection opens with exactly one preamble envelope (PreambleRequest client → server, PreambleResponse server → client) carrying magic, epoch, version (exact equality mandatory), role, and the immutable per-connection ceilings; the preamble is bounded to 4 KiB before allocation and is never replayed on resume. Hello.MaxOutputInFlight stays a per-attachment claim capped by the negotiated ceiling.

  • Client (internal/usecase/client/handshake.go): newHandshakeContext owns a single timer; boundedDial detaches the dial context so a successful carriage outlives the handshake; Close is the only interruption mechanism for blocked transport operations.
  • Daemon (internal/usecase/daemon/handshake.go, daemon.go:handleConn): newHandshakeContext + watchHandshakeTransport close the exact admitted connection on timeout; failHandshakeAttachment retires only that connection and purges a route-created session only when empty. An accepted connection that implements the optional ports.HandshakeDeadlineProvider seam supplies the absolute deadline it was admitted with, which the daemon adopts verbatim: queue delay is consumed instead of restarting the budget, and an already-elapsed deadline fails promptly. Connections without the seam keep a fresh budget from accept. Completion stays where the daemon commits its initial publication; the sessionwire preamble's own completion is not the handshake deadline.

First frame routing (daemon.go:handleConn): Hello → full attach; List, CommandRequest, NavigationInventoryRequest, and Kill are one-shot control connections answered and closed. RemotePreviewWatch streams RemotePreview frames until the client closes. Anything else, including any malformed first frame, gets a typed compatibility response and no session mutation:

First-frame failure kind Response Needs RequestID
DecodeMessageHello, version mismatch ErrorMsg{ErrVersionMismatch} no
DecodeMessageHello, malformed ErrorMsg{ErrInternal} no
DecodeMessageCommand CommandResult{Outcome, Code, Text} yes, else silent close
DecodeMessageKill ErrorMsg{ErrInternal} no — a malformed kill has no reliable RequestID to correlate a KillResult to
DecodeMessageRemotePreview RemotePreview{Status: Malformed} no
DecodeMessageNavigationInventory NavigationInventoryResponse{Invalid / VersionMismatch} yes, else silent close
other / unknown ErrorMsg{ErrInternal, "expected hello"} no

Live checks after decode: Hello.Version, CommandRequest.Version, NavigationInventoryRequest.Version, and the watch's RemotePreviewRequest.Version (independent catalogue-era schema version 1) must equal the negotiated constant or the request fails with the version-mismatch response above. There is no multi-version negotiation: equality is mandatory.

Hello.color (ColorCapabilities{mode, source}, field 26) is the only carrier of terminal color capability; Theme has no capability field. mode is a closed enum (TRUE_COLOR, ANSI256, ANSI16, MONOCHROME) and a missing message, an UNSPECIFIED mode, or any unknown enum value is a malformed Hello (sessionwire rejects it before any mutation). source UNSPECIFIED means "unknown": the daemon then ignores the claimed mode and re-detects from Hello.Env; DECLARED and FORCED claims are used verbatim. A HEURISTIC truecolor claim upgrades a weaker daemon-side detection; any other HEURISTIC claim falls back to detection. (internal/domain/terminalcap.Resolve; wire tests in sessionwire/proto_convert_test.go:TestProtoHelloColorWire.)

Command requests additionally carry a 10-second result deadline (daemon/command_tracker.go:CommandRequestTimeout), tracked per connection and correlated by RequestID. Every dispatched request produces one closed CommandResult outcome: succeeded, failed, or outcome unknown. Timeout or reply loss after dispatch is outcome unknown and is never replayed.

Broker negotiation

Broker connections are a separate conversation from the session protocol. internal/adapters/brokerwire owns it: client tags 101-111 and server tags 201-209 are disjoint from every session tag, and each direction is its own closed directional oneof union. A broker connection opens with exactly one broker preamble (PreambleRequest client → server, PreambleResponse server → client) using broker roles 3/4. The preamble carries the same magic (wire.PreambleMagic), epoch (wire.ProtocolEpoch), exact protocol.Version equality, and negotiated ceilings as the session handshake (envelope 1 MiB..16 MiB, stream chunk 1..64 KiB), and the serialized preamble is bounded to 4 KiB (wire.PreambleLimit, exposed as brokerwire.CheckPreambleSize) before scan or allocation.

Refusals use the shared PreambleRejectionCode taxonomy. There is no capability negotiation: nonzero capability bits are refused as code 7 (limit refused), and code 6 (out of order) is reserved for the connection dispatcher.

internal/adapters/brokeripc carries this conversation over broker.sock in the per-user runtime directory, with same-user peer-credential admission and bounded per-connection queues. The remote helpers _broker-mux-stdio, _broker-mux-quic-bootstrap, and _broker-mux-quic-proxy bridge a remote daemonmux socket to stdio or to one freshly authenticated QUIC stream.

Every daemonmux logical stream is credit flow controlled in both directions. Each side starts with the same per-stream byte window, derived from the negotiated ceilings (aggregate bytes / max streams, 512 KiB by default). A MuxData frame costs its payload plus 64 bytes of credit; the writer waits when credit runs out, and the receiver returns consumed credit with MuxWindowUpdate once half a window is drained. A slow link or a slow consumer therefore slows its own stream without affecting siblings. Data beyond the granted window, or credit beyond the window, is a peer violation and resets only that stream.

Setup is fully bounded: the dialer's one context/deadline covers the Unix dial, preamble, Register send, and wait for Registered, and the server requires Register within the same accept-time handshake budget that bounded the preamble and admission.

Output, ACK, and flow control

An Output state is (Epoch, Base, New, Echo, ViewRevision, Size, Full, Context?, Data). State-bearing output (New != 0) requires a validated ViewContext (non-zero publication, committed route identity, valid tab and pane IDs); side-effect output (New == 0) carries no context and no base. ValidateOutput enforces the epoch/base/new chain (New == Base+1 unless Full with Base == 0).

  • Ack{Epoch, State} is cumulative per epoch; epoch 0 is invalid.
  • At most MaxOutputWindow (8) states may be in flight per attachment (daemon.go:maxUnackedOutputStates); the daemon negotiates the window down from the transport capabilities (Hello.MaxOutputInFlight claim capped by the transport ceiling; datagram carriage reports 1).
  • The client coalesces ACKs (client.go:cumulativeAckQueue) and keeps applying + acknowledging daemon frames even while a picker lease owns the terminal (see below) — admission without physical write.
  • Echo is the newest InputSeq the daemon applied at least 50 ms ago (mosh's echo timeout), so the screen it ships with already holds that input's echo. When no frame is due, the daemon sends it in an empty side-effect Output (New == 0). Clients use it to confirm predicted echo. OutputResetRequest restarts the chain; UIFence{ActionID} orders UI operations and UIReceipt reports the processed boundary (Processed requires epoch/state/publication; Unavailable carries none).

Compression (sessionwire Output converters): only full snapshots at or above 1024 bytes are zlib candidates, and only when the compressed form plus header is smaller. Decoded length is validated against MaxOutputDataLen before allocation; decompression rejects length-excess, trailing bytes, and non-Full compressed states.

Byte ownership: Recv returns freshly allocated, right-sized payload copies the caller may retain; Send/SendAsync copies into the adapter's queue. IPC reuses one grow-once read buffer internally.

Picker admission without physical write

While a client picker overlay is open over a live attachment (session picker PickerOffer, or the first move PickerSnapshot), the client still applies and acknowledges every accepted daemon frame but does not write it. Closing the overlay sends OutputResetRequest, and the daemon answers with an authoritative full paint.

The daemon sends a palette-opened picker only after a frame composed after the palette closed has been emitted, so the erased palette is on screen before output suppression starts. An ACK-blocked paint or a transient freeze delays the picker; a detach or transport change drops it. The daemon side drops raw key/mouse input for the attachment while a move picker is open; only typed picker messages act.

Ordering, close, and admission semantics

  • Each connection is one ordered reliable stream. Concurrent Send calls are serialized by a single writer (IPC) or a mutex (SSH stdio); exactly one Recv caller at a time.
  • Close is concurrent-safe, idempotent, and unblocks in-flight Send/Recv with a terminal error (io.EOF on clean close).
  • Bounded admission, three preserved contracts (ports + wire.Transport): synchronous wire attempt (Send), bounded async admission (SendAsync, 8 queued frames / 32 MiB bytes, ErrBackpressure when full), owned bounded synchronous operation. Buffer ownership, cancellation, and error semantics of each return are unchanged. Synchronous sends rendezvous with the writer and carry no async byte-budget charge; async sends are charged on admission and released exactly once on dequeue.

Current state: the one-shot kill control paths have moved to the explicit result protocol. Kill carries a unique nonzero RequestID, and the daemon answers every normally decoded kill request with exactly one correlated KillResult before the control connection closes: KillSucceeded, KillFailed (with a bounded structured Failures list), or a definite KillOutcomeUnknown. The client consumes that result; a close without a result, a mismatched RequestID, or a wrong-typed reply is outcome unknown and is never replayed, so a control failure whose response cannot be delivered is never mistaken for success. CommandRequest follows the same correlated model through CommandResult: succeeded, failed, or outcome unknown. A timeout, close, cancellation, malformed reply, or mismatched RequestID after send is outcome unknown and is never replayed.

The remaining one-shot controls (List, remote preview, inventory, and picker control) do not treat close as success, but they have no closed outcome field. The daemon attempts one response and then closes; a client that receives no valid response reports a receive error. An undeliverable response is logged only where the handler has a send-failure path, so these controls remain weaker than KillResult and CommandResult.

Limits

Item Ceiling Owner
Preamble envelope 4 KiB (wire.PreambleLimit), exact magic/epoch/version/role protocol/wire/preamble.go, sessionwire/proto_handshake.go
Absolute envelope 16 MiB (wire.AbsoluteEnvelopeLimit), enforced before allocation protocol/wire/preamble.go, streamframe
Control (non-Output, non-image) envelope 1 MiB (wire.ControlEnvelopeLimit) protocol/wire/preamble.go
Output.Data MaxOutputDataLen = 16776809 (16 MiB minus worst-case Protobuf overhead) protocol/session.go
Image envelope 16 MiB absolute (image data rides the envelope ceiling) sessionwire image converters
Queued envelopes per connection 8 messages, 32 MiB bytes (streamframe defaults), then backpressure; sync sends uncharged, async charged once adapters/streamframe
Encoding buffer bounded per-connection framing buffers owned by streamframe adapters/streamframe
Output states in flight 8 (MaxOutputWindow) preferred for QUIC/IPC/SSH daemon, negotiated down
QUIC unauthenticated peers 4 connections, 3 s auth deadline, one bidirectional stream each; extra/unidirectional streams rejected adapters/quic
QUIC bootstrap 32-byte token + 16-byte nonce (base64), ≤4 KiB readiness (schema v1, host-independent port, SHA-256 pin, ≤15 s expiry), one bounded (512 B) auth record, atomic one-time consumption, token erasure on close adapters/quic bootstrap, app/_broker-mux-quic-bootstrap + _broker-mux-quic-proxy
Handshake, dial → first committed publication 15 s, single absolute deadline propagated unchanged, never reset both ends
Command result 10 s per request daemon/command_tracker.go
RemotePreview viewport 256×128 cells, 1 MiB protocol/preview.go
Route snapshot publication 32 entries protocol/routes.go
Route label 256 bytes, UTF-8, no controls protocol/routes.go
SSH shutdown wait 3 s (sshCloseTimeout) adapters/sshstdio

Framing on IPC/SSH stdio/QUIC stream: 4-byte big-endian length, then one complete serialized Protobuf envelope (no type byte). Zero-length envelopes and lengths above the ceiling fail without allocation. The strict scanner rejects short reads, trailing bytes, duplicate variants, and unknown fields before generated unmarshal.

Authentication boundary

No session bytes are accepted before authentication and version acceptance. Local carriage requires the preamble + Hello/Welcome inside the 15-second budget; a failed handshake closes the exact connection. Broker-owned remote carriage uses QUIC (internal/adapters/quic, TLS 1.3, ALPN vev/1, exact SHA-256 pin, one bidirectional stream, no 0-RTT); a host added with vev host add --transport ssh uses SSH stdio instead. For the QUIC route the broker starts one short-lived _broker-mux-quic-bootstrap --production over SSH which spawns a detached _broker-mux-quic-proxy (ephemeral listener + certificate, 32-byte token, ≤4 KiB readiness with host-independent port/pin/expiry), prints the single readiness line, and exits. The broker resolves the SSH target host, composes host:port, pins the certificate, and sends one bounded auth record before any preamble. The token is consumed atomically on first use; concurrent, replayed, or expired auth fails closed without private daemonmux access, and the proxy exits after session termination, expiry, or setup failure with token material erased.

Error taxonomy

  • Envelope level: short payload, trailing bytes, duplicate variants, unknown fields, invalid wire types, oversize — all fail closed before generated unmarshal, no partial values escape.
  • Typed boundary: protocol.DecodeFailure{Category, Kind, Version, RequestID?, Err} with categories Malformed | UnknownType | WrongDirection. The numeric Type field is retained only for log compatibility; direction is structural (closed oneof unions per direction). Payload bytes are never retained.
  • Transport level: io.EOF (close), ErrFrameTooLarge, ErrZeroLengthFrame, ErrBackpressure, process-exit errors for SSH. LinkState reports transport connectivity only, never semantic handshake acceptance.

Benchmarks

Run the protocol, framing, and typed-connection benchmarks from the repository root:

go test ./internal/protocol/wire -run '^$' -bench 'BenchmarkHelloEnvelope|BenchmarkInputEnvelope|BenchmarkImageEnvelope|BenchmarkOutput' -benchmem -count 5
go test ./internal/adapters/ipc -run '^$' -bench 'BenchmarkTransport' -benchmem -count 5
go test ./internal/adapters/sessionwire -run '^$' -bench 'BenchmarkTyped|BenchmarkOutputProductionRoundTrip' -benchmem -count 5

Scanner specifications are memoized by descriptor (specCache), so each message shape pays reflection once per process. Application and QUIC performance budgets and reproducible evidence commands live in performance.md.

Tooling

  • QUIC: github.com/quic-go/quic-go v0.62.0 (MIT). Verified API surface: TLS 1.3 (MinVersion: tls.VersionTLS13), ALPN-gated single bidirectional stream (AcceptStream server / OpenStreamSync client, MaxIncomingStreams: 1, MaxIncomingUniStreams: 0, extra streams rejected), stream cancellation (CancelRead/CancelWrite), stream deadlines (SetDeadline/SetReadDeadline/SetWriteDeadline), keepalive/idle limits (KeepAlivePeriod, MaxIdleTimeout, HandshakeIdleTimeout), listener ownership (Listener.Accept/Close), explicit close mapping (CloseWithError), 0-RTT disabled by using only DialAddr/ListenAddr (never *Early), resumption observable via ConnectionState.Used0RTT, tracing hook for observability. No QUIC types leak past internal/adapters/quic + internal/app composition; no datagrams, no 0-RTT mutations.
  • Protobuf: google.golang.org/protobuf v1.36.12 runtime + protoc-gen-go v1.36.12 pinned as a Go tool (tool google.golang.org/protobuf/cmd/protoc-gen-go in go.mod); generation via github.com/bufbuild/buf/cmd/buf v1.73.0 pinned as a Go tool (tool github.com/bufbuild/buf/cmd/buf). Sources live in internal/protocol/wire/schema/, generated Go lands directly in internal/protocol/wire (never edited); reproducibility command: go tool buf generate (config buf.gen.yaml), verified by Makefile:protocol-check. No gRPC.
  • Supporting dependencies are pinned in go.mod and go.sum; QUIC requires the corresponding x/crypto, x/net, and x/sys modules.
source
1# Vev session protocol
2
3This document defines the observable Protobuf session contract and its QUIC,
4Unix, and SSH stdio carriage rules. Each invariant maps to a current test or
5benchmark.
6
7## Message directions
8
9`internal/protocol/messages.go` closes both unions. The daemon rejects a
10wrong-direction frame before any mutation (`sessionwire.ErrWrongDirection`
11→ `protocol.DecodeWrongDirection`).
12
13Client → server: `Hello`, `Input`, `Resize`, `Detach`, `Ping`, `List`,
14`Kill`, `Theme`, `Ack`, `ImagePush`, `ClientNotice`, `CommandRequest`,
15`OutputResetRequest`, `SelectTab`, `TerminalFocus`, `RemotePreviewWatch`,
16`RouteAttentionSubscription`, `SamePeerSwitchRequest`,
17`RecentRouteSnapshot`, `RouteNavigationFailure`,
18`SessionCreationFailure`, `UIFence`, `NavigationInventoryRequest`,
19`NavigationInventoryPublication`, `NavigationInventoryFailure`,
20`PickerClose`, `PickerSelection`.
21
22Server → client: `Welcome`, `ErrorMsg`, `Output`, `Detached`, `Pong`,
23`Sessions`, `CommandResult`, `AttachTarget`, `RemotePreview`,
24`CommittedRouteIdentity`, `RouteNavigationAction`,
25`RouteCreateSessionAction`, `RouteNavigationFailure`, `RoutePosition`,
26`RouteRetired`, `SamePeerSwitchFailure`, `UIReceipt`, `UIViewUpdate`,
27`NavigationInventoryResponse`, `NavigationInventoryDemand`,
28`NavigationInventorySelection`, `PickerOffer`, `PickerSnapshot`,
29`PickerClosed`, `PickerResult`, `PickerFailure`, `KillResult`.
30
31`RouteNavigationFailure` is the one payload that appears in both unions.
32`sessionwire` conversion switches (`decodeProtoClient`/`decodeProtoServer`,
33`encodeProtoClient`/`encodeProtoServer`) are exhaustive over the
34directional `ClientEnvelope`/`ServerEnvelope` `oneof` unions; unknown
35or opposite-direction payloads fail with `DecodeUnknownType` /
36`DecodeMalformed` before any mutation.
37
38## Handshake timeline
39
40One absolute 15-second budget (`protocol.HandshakeTimeout`) covers dial
41through the first committed publication, on both ends. Every
42connection opens with exactly one preamble envelope
43(`PreambleRequest` client → server, `PreambleResponse` server →
44client) carrying magic, epoch, version (exact equality mandatory),
45role, and the immutable per-connection ceilings; the preamble is
46bounded to 4 KiB before allocation and is never replayed on resume.
47`Hello.MaxOutputInFlight` stays a per-attachment claim capped by the
48negotiated ceiling.
49
50- Client (`internal/usecase/client/handshake.go`): `newHandshakeContext`
51  owns a single timer; `boundedDial` detaches the dial context so a
52  successful carriage outlives the handshake; `Close` is the only
53  interruption mechanism for blocked transport operations.
54- Daemon (`internal/usecase/daemon/handshake.go`, `daemon.go:handleConn`):
55  `newHandshakeContext` + `watchHandshakeTransport` close the exact
56  admitted connection on timeout; `failHandshakeAttachment` retires only
57  that connection and purges a route-created session only when empty. An
58  accepted connection that implements the optional
59  `ports.HandshakeDeadlineProvider` seam supplies the absolute deadline it
60  was admitted with, which the daemon adopts verbatim: queue delay is
61  consumed instead of restarting the budget, and an already-elapsed
62  deadline fails promptly. Connections without the seam keep a fresh
63  budget from accept. Completion stays where the daemon commits its
64  initial publication; the sessionwire preamble's own completion is not
65  the handshake deadline.
66
67First frame routing (`daemon.go:handleConn`): `Hello` → full attach;
68`List`, `CommandRequest`, `NavigationInventoryRequest`, and `Kill` are
69one-shot control connections answered and closed. `RemotePreviewWatch`
70streams `RemotePreview` frames until the client closes. Anything else, including any
71malformed first frame, gets a typed compatibility response and no session
72mutation:
73
74| First-frame failure kind | Response | Needs RequestID |
75|---|---|---|
76| `DecodeMessageHello`, version mismatch | `ErrorMsg{ErrVersionMismatch}` | no |
77| `DecodeMessageHello`, malformed | `ErrorMsg{ErrInternal}` | no |
78| `DecodeMessageCommand` | `CommandResult{Outcome, Code, Text}` | yes, else silent close |
79| `DecodeMessageKill` | `ErrorMsg{ErrInternal}` | no — a malformed kill has no reliable RequestID to correlate a `KillResult` to |
80| `DecodeMessageRemotePreview` | `RemotePreview{Status: Malformed}` | no |
81| `DecodeMessageNavigationInventory` | `NavigationInventoryResponse{Invalid / VersionMismatch}` | yes, else silent close |
82| other / unknown | `ErrorMsg{ErrInternal, "expected hello"}` | no |
83
84Live checks after decode: `Hello.Version`, `CommandRequest.Version`,
85`NavigationInventoryRequest.Version`, and the watch's `RemotePreviewRequest.Version`
86(independent catalogue-era schema version `1`) must equal the negotiated
87constant or the request fails
88with the version-mismatch response above. There is no multi-version
89negotiation: equality is mandatory.
90
91`Hello.color` (`ColorCapabilities{mode, source}`, field 26) is the only carrier
92of terminal color capability; `Theme` has no capability field. `mode` is a
93closed enum (`TRUE_COLOR`, `ANSI256`, `ANSI16`, `MONOCHROME`) and a missing
94message, an `UNSPECIFIED` mode, or any unknown enum value is a malformed Hello
95(`sessionwire` rejects it before any mutation). `source` `UNSPECIFIED` means
96"unknown": the daemon then ignores the claimed mode and re-detects from
97`Hello.Env`; `DECLARED` and `FORCED` claims are used verbatim. A `HEURISTIC` truecolor claim
98upgrades a weaker daemon-side detection; any other `HEURISTIC` claim falls back
99to detection.
100(`internal/domain/terminalcap.Resolve`; wire tests in
101`sessionwire/proto_convert_test.go:TestProtoHelloColorWire`.)
102
103Command requests additionally carry a 10-second result deadline
104(`daemon/command_tracker.go:CommandRequestTimeout`), tracked per
105connection and correlated by `RequestID`. Every dispatched request produces
106one closed `CommandResult` outcome: succeeded, failed, or outcome unknown.
107Timeout or reply loss after dispatch is outcome unknown and is never replayed.
108
109## Broker negotiation
110
111Broker connections are a separate conversation from the session protocol.
112`internal/adapters/brokerwire` owns it: client tags 101-111 and server tags
113201-209 are disjoint from every session tag, and each direction is its own
114closed directional `oneof` union. A broker connection opens with exactly one
115broker preamble (`PreambleRequest` client → server, `PreambleResponse` server
116→ client) using broker roles 3/4. The preamble carries the same magic
117(`wire.PreambleMagic`), epoch (`wire.ProtocolEpoch`), exact `protocol.Version`
118equality, and negotiated ceilings as the session handshake (envelope
1191 MiB..16 MiB, stream chunk 1..64 KiB), and the serialized preamble is
120bounded to 4 KiB (`wire.PreambleLimit`, exposed as
121`brokerwire.CheckPreambleSize`) before scan or allocation.
122
123Refusals use the shared `PreambleRejectionCode` taxonomy. There is no
124capability negotiation: nonzero capability bits are refused as code 7
125(`limit refused`), and code 6 (`out of order`) is reserved for the
126connection dispatcher.
127
128`internal/adapters/brokeripc` carries this conversation over `broker.sock`
129in the per-user runtime directory, with same-user peer-credential admission
130and bounded per-connection queues. The remote helpers `_broker-mux-stdio`,
131`_broker-mux-quic-bootstrap`, and `_broker-mux-quic-proxy` bridge a remote
132daemonmux socket to stdio or to one freshly authenticated QUIC stream.
133
134Every daemonmux logical stream is credit flow controlled in both directions.
135Each side starts with the same per-stream byte window, derived from the
136negotiated ceilings (aggregate bytes / max streams, 512 KiB by default). A
137`MuxData` frame costs its payload plus 64 bytes of credit; the writer waits
138when credit runs out, and the receiver returns consumed credit with
139`MuxWindowUpdate` once half a window is drained. A slow link or a slow
140consumer therefore slows its own stream without affecting siblings. Data
141beyond the granted window, or credit beyond the window, is a peer violation
142and resets only that stream.
143
144Setup is fully bounded: the dialer's one context/deadline covers the Unix
145dial, preamble, Register send, and wait for Registered, and the server requires Register within
146the same accept-time handshake budget that bounded the preamble and admission.
147
148## Output, ACK, and flow control
149
150An `Output` state is `(Epoch, Base, New, Echo, ViewRevision, Size, Full,
151Context?, Data)`. State-bearing output (`New != 0`) requires a validated
152`ViewContext` (non-zero publication, committed route identity, valid tab
153and pane IDs); side-effect output (`New == 0`) carries no context and no
154base. `ValidateOutput` enforces the epoch/base/new chain
155(`New == Base+1` unless `Full` with `Base == 0`).
156
157- `Ack{Epoch, State}` is cumulative per epoch; epoch `0` is invalid.
158- At most `MaxOutputWindow` (8) states may be in flight per attachment
159  (`daemon.go:maxUnackedOutputStates`); the daemon negotiates the window
160  down from the transport capabilities (`Hello.MaxOutputInFlight` claim
161  capped by the transport ceiling; datagram carriage reports 1).
162- The client coalesces ACKs (`client.go:cumulativeAckQueue`) and keeps
163  applying + acknowledging daemon frames even while a picker lease owns
164  the terminal (see below) — admission without physical write.
165- `Echo` is the newest `InputSeq` the daemon applied at least 50 ms ago
166  (mosh's echo timeout), so the screen it ships with already holds that
167  input's echo. When no frame is due, the daemon sends it in an empty
168  side-effect `Output` (`New == 0`). Clients use it to confirm predicted
169  echo. `OutputResetRequest` restarts the chain; `UIFence{ActionID}` orders UI
170  operations and `UIReceipt` reports the processed boundary
171  (`Processed` requires epoch/state/publication; `Unavailable` carries
172  none).
173
174Compression (`sessionwire` Output converters): only full snapshots at or above
1751024 bytes are zlib candidates, and only when the compressed form plus
176header is smaller. Decoded length is validated against
177`MaxOutputDataLen` before allocation; decompression rejects
178length-excess, trailing bytes, and non-`Full` compressed states.
179
180Byte ownership: `Recv` returns freshly allocated, right-sized payload
181copies the caller may retain; `Send`/`SendAsync` copies into the
182adapter's queue. IPC reuses one grow-once read buffer internally.
183
184## Picker admission without physical write
185
186While a client picker overlay is open over a live attachment (session
187picker `PickerOffer`, or the first move `PickerSnapshot`), the client
188still applies and acknowledges every accepted daemon frame but does not
189write it. Closing the overlay sends `OutputResetRequest`, and the daemon
190answers with an authoritative full paint.
191
192The daemon sends a palette-opened picker only after a frame composed
193after the palette closed has been emitted, so the erased palette is on
194screen before output suppression starts. An ACK-blocked paint or a
195transient freeze delays the picker; a detach or transport change drops it. The daemon side drops raw
196key/mouse input for the attachment while a move picker is open; only
197typed picker messages act.
198
199## Ordering, close, and admission semantics
200
201- Each connection is one ordered reliable stream. Concurrent `Send` calls
202  are serialized by a single writer (IPC) or a mutex (SSH stdio);
203  exactly one `Recv` caller at a time.
204- `Close` is concurrent-safe, idempotent, and unblocks in-flight
205  `Send`/`Recv` with a terminal error (`io.EOF` on clean close).
206- Bounded admission, three preserved contracts (`ports` +
207  `wire.Transport`): synchronous wire attempt (`Send`), bounded async
208  admission (`SendAsync`, 8 queued frames / 32 MiB bytes,
209  `ErrBackpressure` when full), owned bounded synchronous operation.
210  Buffer ownership, cancellation, and error semantics of each
211  return are unchanged. Synchronous sends rendezvous with the writer
212  and carry no async byte-budget charge; async sends are charged on
213  admission and released exactly once on dequeue.
214
215Current state: the one-shot kill control paths have moved to the explicit
216result protocol. `Kill` carries a unique nonzero `RequestID`, and the daemon
217answers every normally decoded kill request with exactly one correlated
218`KillResult` before the control connection closes: `KillSucceeded`,
219`KillFailed` (with a bounded structured `Failures` list), or a definite
220`KillOutcomeUnknown`. The client consumes that result; a close without a
221result, a mismatched `RequestID`, or a wrong-typed reply is `outcome unknown`
222and is never replayed, so a control failure whose response cannot be delivered
223is never mistaken for success. `CommandRequest` follows the same correlated
224model through `CommandResult`: succeeded, failed, or outcome unknown. A timeout,
225close, cancellation, malformed reply, or mismatched `RequestID` after send is
226outcome unknown and is never replayed.
227
228The remaining one-shot controls (`List`, remote preview, inventory, and picker
229control) do not treat close as success, but they have no closed outcome field.
230The daemon attempts one response and then closes; a client that receives no
231valid response reports a receive error. An undeliverable response is logged
232only where the handler has a send-failure path, so these controls remain weaker
233than `KillResult` and `CommandResult`.
234
235## Limits
236
237| Item | Ceiling | Owner |
238|---|---|---|
239| Preamble envelope | 4 KiB (`wire.PreambleLimit`), exact magic/epoch/version/role | `protocol/wire/preamble.go`, `sessionwire/proto_handshake.go` |
240| Absolute envelope | 16 MiB (`wire.AbsoluteEnvelopeLimit`), enforced before allocation | `protocol/wire/preamble.go`, `streamframe` |
241| Control (non-Output, non-image) envelope | 1 MiB (`wire.ControlEnvelopeLimit`) | `protocol/wire/preamble.go` |
242| `Output.Data` | `MaxOutputDataLen` = 16776809 (16 MiB minus worst-case Protobuf overhead) | `protocol/session.go` |
243| Image envelope | 16 MiB absolute (image data rides the envelope ceiling) | `sessionwire` image converters |
244| Queued envelopes per connection | 8 messages, 32 MiB bytes (`streamframe` defaults), then backpressure; sync sends uncharged, async charged once | `adapters/streamframe` |
245| Encoding buffer | bounded per-connection framing buffers owned by `streamframe` | `adapters/streamframe` |
246| Output states in flight | 8 (`MaxOutputWindow`) preferred for QUIC/IPC/SSH | daemon, negotiated down |
247| QUIC unauthenticated peers | 4 connections, 3 s auth deadline, one bidirectional stream each; extra/unidirectional streams rejected | `adapters/quic` |
248| QUIC bootstrap | 32-byte token + 16-byte nonce (base64), ≤4 KiB readiness (schema v1, host-independent port, SHA-256 pin, ≤15 s expiry), one bounded (512 B) auth record, atomic one-time consumption, token erasure on close | `adapters/quic` bootstrap, `app/_broker-mux-quic-bootstrap` + `_broker-mux-quic-proxy` |
249| Handshake, dial → first committed publication | 15 s, single absolute deadline propagated unchanged, never reset | both ends |
250| Command result | 10 s per request | `daemon/command_tracker.go` |
251| `RemotePreview` viewport | 256×128 cells, 1 MiB | `protocol/preview.go` |
252| Route snapshot publication | 32 entries | `protocol/routes.go` |
253| Route label | 256 bytes, UTF-8, no controls | `protocol/routes.go` |
254| SSH shutdown wait | 3 s (`sshCloseTimeout`) | `adapters/sshstdio` |
255
256Framing on IPC/SSH stdio/QUIC stream: 4-byte big-endian length, then one complete
257serialized Protobuf envelope (no type byte). Zero-length envelopes and
258lengths above the ceiling fail without allocation. The strict scanner
259rejects short reads, trailing bytes, duplicate variants, and unknown
260fields before generated unmarshal.
261
262## Authentication boundary
263
264No session bytes are accepted before authentication and version
265acceptance. Local carriage requires the preamble + `Hello`/`Welcome`
266inside the 15-second budget; a failed handshake closes the exact
267connection. Broker-owned remote carriage uses QUIC (`internal/adapters/quic`,
268TLS 1.3, ALPN `vev/1`, exact SHA-256 pin, one bidirectional stream,
269no 0-RTT); a host added with `vev host add --transport ssh` uses SSH stdio instead.
270For the QUIC route the broker starts one short-lived `_broker-mux-quic-bootstrap --production` over SSH
271which spawns a detached `_broker-mux-quic-proxy` (ephemeral listener +
272certificate, 32-byte token, ≤4 KiB readiness with host-independent
273port/pin/expiry), prints the single readiness line, and exits. The
274broker resolves the SSH target host, composes `host:port`, pins the
275certificate, and sends one bounded auth record before any preamble.
276The token is consumed atomically on first use; concurrent, replayed,
277or expired auth fails closed without private daemonmux access, and the proxy exits
278after session termination, expiry, or setup failure with token
279material erased.
280
281## Error taxonomy
282
283- Envelope level: short payload, trailing bytes, duplicate variants,
284  unknown fields, invalid wire types, oversize — all fail closed before
285  generated unmarshal, no partial values escape.
286- Typed boundary: `protocol.DecodeFailure{Category, Kind, Version,
287  RequestID?, Err}` with categories
288  `Malformed | UnknownType | WrongDirection`. The numeric `Type` field is
289  retained only for log compatibility; direction is structural (closed
290  `oneof` unions per direction). Payload bytes are never retained.
291- Transport level: `io.EOF` (close), `ErrFrameTooLarge`,
292  `ErrZeroLengthFrame`, `ErrBackpressure`, process-exit errors for SSH.
293  `LinkState` reports transport connectivity only, never semantic
294  handshake acceptance.
295
296## Benchmarks
297
298Run the protocol, framing, and typed-connection benchmarks from the repository
299root:
300
301```sh
302go test ./internal/protocol/wire -run '^$' -bench 'BenchmarkHelloEnvelope|BenchmarkInputEnvelope|BenchmarkImageEnvelope|BenchmarkOutput' -benchmem -count 5
303go test ./internal/adapters/ipc -run '^$' -bench 'BenchmarkTransport' -benchmem -count 5
304go test ./internal/adapters/sessionwire -run '^$' -bench 'BenchmarkTyped|BenchmarkOutputProductionRoundTrip' -benchmem -count 5
305```
306
307Scanner specifications are memoized by descriptor (`specCache`), so each
308message shape pays reflection once per process. Application and QUIC performance
309budgets and reproducible evidence commands live in [performance.md](performance.md).
310
311## Tooling
312
313- QUIC: `github.com/quic-go/quic-go v0.62.0` (MIT). Verified API surface:
314  TLS 1.3 (`MinVersion: tls.VersionTLS13`), ALPN-gated single bidirectional
315  stream (`AcceptStream` server / `OpenStreamSync` client,
316  `MaxIncomingStreams: 1`, `MaxIncomingUniStreams: 0`, extra streams
317  rejected), stream cancellation (`CancelRead`/`CancelWrite`), stream
318  deadlines (`SetDeadline`/`SetReadDeadline`/`SetWriteDeadline`),
319  keepalive/idle limits (`KeepAlivePeriod`, `MaxIdleTimeout`,
320  `HandshakeIdleTimeout`), listener ownership (`Listener.Accept/Close`),
321  explicit close mapping (`CloseWithError`), 0-RTT disabled by using only
322  `DialAddr`/`ListenAddr` (never `*Early`), resumption observable via
323  `ConnectionState.Used0RTT`, tracing hook for observability.
324  No QUIC types leak past `internal/adapters/quic` + `internal/app`
325  composition; no datagrams, no 0-RTT mutations.
326- Protobuf: `google.golang.org/protobuf v1.36.12` runtime +
327  `protoc-gen-go v1.36.12` pinned as a Go tool
328  (`tool google.golang.org/protobuf/cmd/protoc-gen-go` in `go.mod`);
329  generation via `github.com/bufbuild/buf/cmd/buf v1.73.0` pinned as a Go
330  tool (`tool github.com/bufbuild/buf/cmd/buf`). Sources live in
331  `internal/protocol/wire/schema/`, generated Go lands directly in
332  `internal/protocol/wire` (never edited); reproducibility command:
333  `go tool buf generate` (config `buf.gen.yaml`), verified by
334  `Makefile:protocol-check`. No gRPC.
335- Supporting dependencies are pinned in `go.mod` and `go.sum`; QUIC requires
336  the corresponding `x/crypto`, `x/net`, and `x/sys` modules.

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close