fix(nests): T11 drop bestEffort=true on moq-lite group uni streams

`MoqLiteSession.openGroupStream` was opening each group's QUIC uni
stream with `bestEffort = true`. `:quic`'s `SendBuffer.markLost` with
that flag drops lost STREAM ranges WITHOUT retransmit AND WITHOUT
`RESET_STREAM` (`quic/src/commonMain/kotlin/com/vitorpamplona/quic/
stream/SendBuffer.kt:300-309`). The peer's `ReceiveBuffer` ends up
with chunks at `[0, P)` and `[Q, end)` and a permanent hole at
`[P, Q)`; the application's `incoming` Flow parks on the hole forever.

Web watchers (`@moq/hang` `Container.Consumer`) park their
`Group.readFrame` until the relay's 30 s `MAX_GROUP_AGE` ages the
broadcast queue out — manifesting as a 30 s silent dropout per lost
packet on lossy networks (cellular, mobile WiFi, congested home
routers). This is a real-world bug that's invisible on a clean LAN.

The reference implementation (kixelated/moq-rs `Publisher::serve_group`,
`rs/moq-lite/src/lite/publisher.rs:347-406`) writes to reliable QUIC
streams with no `set_unreliable` call — `bestEffort` was Amethyst's
private optimisation with no peer-side support. Drop it; let `:quic`
retransmit lost ranges normally. A retransmit arriving 50–150 ms late
still falls inside hang's default ~200 ms jitter buffer, so the cost
is marginal extra bandwidth on retransmits and the win is no more
silent dropouts on lossy networks.

T11.2 — orthogonality check: stream-cliff fix is independent.
Re-read `nestsClient/plans/2026-05-01-quic-stream-cliff-investigation.md`
and grep'd both the plan and `NestMoqLiteBroadcaster.kt` for
`bestEffort` / `best_effort` — neither references it. The cliff fix
is `framesPerGroup = 5/50` (cadence reduction); load-bearing on
stream-creation RATE, not loss handling. Drop is safe.

T11.3 (stream priority — newer groups drain first under congestion)
deferred to a follow-up commit; hooking it into `:quic`'s
send-frame loop is bigger than a one-line change and warrants its
own task. The kixelated/moq-rs publisher uses
`stream.set_priority(priority.current())` to bias the writer; without
it, our drain order under congestion is FIFO across streams rather
than newest-first, which can mean the listener catches up on a stale
group when a fresh one is more useful. Doesn't block today —
production audio rarely hits transport congestion at 1 group/sec
cadence.

https://claude.ai/code/session_014JfZJHSTvyYYWJbC9VbB47
This commit is contained in:
Claude
2026-05-06 18:18:23 +00:00
parent 4714e3c723
commit 7e76ab1139
@@ -1023,14 +1023,21 @@ class MoqLiteSession internal constructor(
subscribeId: Long,
sequence: Long,
): com.vitorpamplona.nestsclient.transport.WebTransportWriteStream {
// Group streams carry a single Opus packet. They're real-time
// and best-effort — a STREAM frame arriving 200 ms late is
// worse than useless because the listener has already moved
// past that group's sequence number. Setting bestEffort=true
// tells the underlying QUIC SendBuffer to drop lost ranges
// instead of retransmitting them, bounding the bandwidth waste
// we'd otherwise incur on a lossy uplink.
val uni = transport.openUniStream(bestEffort = true)
// Group streams use reliable QUIC delivery to match the
// moq-lite reference (kixelated/moq-rs `serve_group` writes
// reliable streams; bestEffort is not a moq-lite concept).
// The previous shape opened with `bestEffort=true`, which
// caused `:quic`'s `SendBuffer.markLost` to drop lost ranges
// without retransmit AND without RESET_STREAM — leaving the
// peer's stream-reassembly buffer permanently wedged at the
// hole boundary. The watcher's `Group.readFrame` parks until
// the relay's 30 s `MAX_GROUP_AGE` ages the broadcast queue
// out, manifesting as a 30 s silent dropout per lost packet
// on lossy networks. Reliable delivery costs marginal extra
// bandwidth on retransmits (a lost STREAM range arriving 50
// 150 ms late still falls inside hang's default ~200 ms
// jitter buffer) and avoids the dropout entirely.
val uni = transport.openUniStream()
uni.write(Varint.encode(MoqLiteDataType.Group.code))
uni.write(MoqLiteCodec.encodeGroupHeader(MoqLiteGroupHeader(subscribeId, sequence)))
return uni