Files
amethyst/nestsClient
Claude 335a337283 feat(nestsclient): version-aware Lite-03/04 codec + ALPN negotiation — moq-lite Lite-04
Lite-03 audit L1: completes the moq-lite version surface so the
session speaks the wire-format version the server picks via
WebTransport ALPN negotiation. Pre-fix the codec hardcoded Lite-03;
advertising `moq-lite-04` was a footgun because a Lite-04-preferring
relay would desync on the very first Announce exchange.

Verified diff between Lite-03 and Lite-04 by WebFetching kixelated's
Rust reference (`rs/moq-lite/src/lite/{announce,subscribe,probe}.rs`,
`rs/moq-lite/src/version.rs`, `rs/moq-lite/src/model/origin.rs`):
exactly three fields differ; everything else is byte-identical.

  - `Announce.hops`: Lite-03 wire = single varint count (the spec
    explicitly fills with `Origin::UNKNOWN` placeholders on decode);
    Lite-04 wire = `varint(count) + count × varint(originId)` (the
    `OriginList`). MAX_HOPS = 32.
  - `AnnouncePlease.excludeHop`: Lite-03 absent; Lite-04 single
    varint after `prefix`. Sentinel `0` = no exclusion.
  - `Probe.rtt`: Lite-03 absent; Lite-04 single varint after
    `bitrate`. Sentinel `0` = unknown (decoded as null). Outgoing
    `Some(0)` is clamped to `Some(1)` to avoid colliding with the
    sentinel — mirrors kixelated's `encode_msg` clamp.

New surface:
  - `MoqLiteVersion` enum (LITE_03, LITE_04) with `fromAlpn` lookup.
  - `MoqLiteCodec.{encode,decode}{AnnouncePlease,Announce,Probe}`
    take `version: MoqLiteVersion = LITE_03` parameter, branch on
    it. Default LITE_03 keeps existing call sites compiling.
  - `MoqLiteSession.client(transport, scope, version)` carries the
    version through every codec invocation.
  - `MoqLiteAnnouncePlease.excludeHop: Long = 0L` (default).
  - `MoqLiteAnnounce.hops: List<Long>` (was `Long`) — list of
    origin IDs, bounded to MAX_HOPS=32. Existing call sites
    migrate `hops = 0L` → `hops = emptyList()`, `hops = 7L` →
    `hops = List(7) { 0L }`.
  - `MoqLiteProbe.rtt: Long? = null` (default).

Negotiation:
  - `WebTransportSession.negotiatedSubProtocol: String?` exposes
    the server's `wt-protocol` selection. `QuicWebTransportFactory`
    parses the response HEADERS, extracts the SF-string from
    `wt-protocol`, and threads it into `QuicWebTransportSession`.
    `FakeWebTransport.pair(negotiatedSubProtocol = …)` lets tests
    drive the same path.
  - Default advertised list now `[moq-lite-04, moq-lite-03]` (was
    `[moq-lite-03]`). Lite-04 sits first to match kixelated's
    preference; servers that don't support it fall back to
    Lite-03 cleanly.
  - `connectNestsListener` / `connectNestsSpeaker` resolve the
    version via `resolveMoqLiteVersion(webTransport.negotiatedSubProtocol)`
    and pass it to `MoqLiteSession.client(...)`. Falls back to
    Lite-03 when the server doesn't echo `wt-protocol` (older
    deployments) or echoes something unrecognised (forward-compat).
  - New `MOQ_LITE_04_VERSION = 0x6D71_6C04L` synthetic version
    code; `versionCode(version)` mapping function.

Regression tests (all green):
  - `MoqLiteCodecTest.announcePlease_lite04_round_trips_excludeHop`
  - `MoqLiteCodecTest.announcePlease_lite03_omits_excludeHop_on_wire`
  - `MoqLiteCodecTest.announce_lite04_round_trips_full_origin_list`
  - `MoqLiteCodecTest.announce_lite03_drops_origin_ids_keeps_count`
  - `MoqLiteCodecTest.announce_decoder_rejects_oversize_hop_count`
    (MAX_HOPS bounds check)
  - `MoqLiteCodecTest.probe_lite04_round_trips_rtt`
  - `MoqLiteCodecTest.probe_lite04_clamps_some_zero_to_one_to_avoid_unknown_sentinel`
  - `MoqLiteCodecTest.probe_lite04_decodes_zero_rtt_as_null`
  - `MoqLiteCodecTest.probe_lite03_wire_omits_rtt`
  - `MoqLiteSessionTest.lite04_announce_round_trips_full_origin_list_through_session`
    (end-to-end Lite-04 session)

Audit doc: nestsClient/plans/2026-05-09-moq-lite-rfc-compliance.md (L1).

https://claude.ai/code/session_012TGfo99Ugz7fcCPv85a8Tq
2026-05-09 15:05:54 +00:00
..