Control stream
One bidirectional stream, opened by the pilot immediately after the WebTransport session is established. Newline-delimited JSON, UTF-8, one object per line. The pilot speaks first: the agent learns the stream from the first inbound line, and nothing it writes before then is delivered.
| direction | type | fields |
|---|---|---|
| P→A | hello |
proto: 2, session_id (from the signal offer), client: {kind: "browser"|"native", name, version}, token? |
| A→P | welcome |
session_id, role: "driver"|"observer", channels: [{id, kind, name, codec, fps}], qos: {profile, deadline_delta_ms, deadline_key_ms, presentation_delay_ms, on_loss}, t_agent_us |
| A→P | denied |
reason — then the agent closes the session |
| P→A | ping |
t1 (pilot µs) |
| A→P | pong |
t1, t2 (agent µs at receipt) |
| P→A | loss |
ch, frame_id, key: bool — sent once per unrecoverable frame, immediately |
| P→A | request-keyframe |
ch — sent on hello and whenever the decoder needs a keyframe |
| P→A | set-qos |
profile |
| A→P | qos-ack |
profile, qos: {…} as in welcome, publisher: "requested"|"unavailable" |
| A→P | agent-stats |
1 Hz; frames_in, frames_sent, frames_dropped_backlog, frames_skipped_stale, keyframes_requested, chunks_sent, parity_sent, bytes_sent, rtt_ms, min_rtt_ms, cwnd, delivery_kbps |
| P→A | pilot-stats |
1 Hz; chunks_sent_seen, chunks_rx_seen, chunks_rx, chunks_missing, chunks_late, frames_clean, frames_recovered, frames_incomplete, frames_timed_out, frames_timed_out_late, keyframes_lost, keyframes_timed_out, kbps, fps, g2g_ms, decode_q. True loss comes from chunks_sent_seen/chunks_rx_seen: the agent’s chunks_sent and the pilot’s chunks_rx as the pilot read them together, at the instant an agent-stats arrived. The agent differences that pair across a ≥ 3 s window. Both ends must come from one clock — an earlier version compared the agent’s send count as of (now − RTT − 100 ms) against a chunks_rx taken at a different moment, which turned ordinary send-rate variation into percent-level phantom loss (see ADR 0006). A pilot that omits the pair leaves the agent’s loss signal unset rather than wrong. |
| P→A | bye |
— |
Anything unknown is ignored and counted. Commands and sensors do not travel
here — they are datagrams (chunks.md).
Clock offset: offset = t2 - (t1 + rtt/2) using the pilot’s local send/receive
times; glass-to-glass = t_decoded_pilot - (send_ts_agent + offset) where
send_ts comes from the chunk header (low 32 bits of agent µs, wrap-aware).