Files
rvbox/docs/protocol.md
T

5.0 KiB

RVBox v1 agent protocol

The authoritative schemas are ../protos/rvbox/v1/common.proto and ../protos/rvbox/v1/agent.proto. This document specifies their use over WSS.

Transport and compatibility

The nginx-terminated wss:// connection carries exactly one serialized rvbox.v1.AgentEnvelope in each binary WebSocket message. There is no extra length prefix. A decoded envelope may not exceed 1 MiB. A chunk's uncompressed payload may not exceed 64 KiB. Both peers validate declared and actual expanded sizes before allocation/decompression.

The protobuf package is rvbox.v1. Registration negotiates a major/minor protocol range: incompatible majors are rejected; the highest shared minor is chosen. New fields are append-only. A peer ignores unknown optional fields but must respond with PROTOCOL_ERROR to an unknown required envelope feature.

Heartbeat and reconnect

Both endpoints use the same algorithm. Any received valid WebSocket frame is inbound activity. After 10 seconds without inbound activity, send a WebSocket Ping. After 30 seconds without inbound activity, close the session and treat it as dead. Pong processing is normal WebSocket behavior; it is not an application message and never queues behind command traffic.

The client reconnects with full-jitter exponential backoff (initial 1 second, cap 60 seconds). A session stable for 60 seconds resets the backoff. The server does not reconnect; it waits for clients. A reconnect always registers again, receives a new fencing generation, resends unacknowledged delivery/output, and reconciles only commands the server still considers non-terminal.

Session fencing

ClientHello starts registration. ServerWelcome gives the selected version, random session_id, and session_generation. Except ClientHello, all envelopes carry those values. A new accepted registration fences and disconnects the prior one for the same client ID. The server accepts messages only from the current generation; command dispatches also identify their intended generation.

Reliable work flows

Dispatch and reconciliation

The server persistently creates a command before dispatching CommandDispatch. It redelivers until it receives CommandAccepted. Clients durably deduplicate on issue_uuid. A client whose queue is full sends a capacity rejection.

Following registration the server sends ReconcileRequest only for its non-terminal commands for that client. The client replies with a ReconcileSnapshot per requested known command, or unknown_to_client. It continues normal event retransmission from the server's last acknowledged event sequence. Terminal history already confirmed by the server is deliberately excluded.

Output and events

Client execution events use an increasing event_seq; retries reuse the same sequence and content. The server durably writes an event before sending EventAck. EventAck is cumulative through a sequence number. Output is Zstandard compressed with an explicit original-size field. Server storage can reuse the validated compressed bytes.

When rolling output removes old retained segments, the server writes OutputTruncation metadata containing the removed event/byte ranges. Queries must show that marker rather than silently presenting an apparently complete stream. An offline client that reaches a cap sends ClientOutputTruncated before replaying its retained tail on reconnect. The server records it, permits the named event-sequence gap, and exposes it in output queries. Truncation metadata is not an execution event and does not consume an event_seq.

Stdin and signals

StdinWrite is binary-safe and ordered by write_seq; the client durably deduplicates it and returns StdinAck. append_newline is true by default in the CLI but explicit on the wire. CloseStdin is a separate idempotent action. Signal and cancellation requests carry a command revision to settle start/kill races.

Scripts

For a script command, dispatch first contains a ScriptDescriptor; then the server sends ScriptChunk messages and a commit. The client checks offset, chunk order, full length, and SHA-256 before it reports upload complete or launches the process. A retransmitted chunk is idempotent by offset/content.

Flow control and failure containment

No receive loop runs an executor, database write, decompressor, or slow socket operation inline. Each side has bounded staging queues. Durable spools are the source of truth and are charged to the 10 MiB/50 MiB/1 GiB compressed retention budgets described in the architecture document. A full staging queue pauses the related read/dispatch path and drains from disk; it never grows without bound.

Malformed protobuf, over-size payload, invalid compressed data, impossible sequence, bad session token, or protocol-version violation yields a structured error where safe and closes that WebSocket session. It does not crash either daemon. Network loss is normal and is handled through idempotent resend.