Commit Graph

6 Commits

Author SHA1 Message Date
Claude fba0a5c952 feat(quic): bestEffort streams + park CC plan indefinitely
After drafting the congestion-control plan we concluded the audio-rooms
workload doesn't actually need CC — speakers push ~8 KB/sec, which
never fills any modern link's capacity. The one real concern that
surfaced — STREAM retransmit wasting bandwidth on stale Opus frames
on lossy uplinks — is much cheaper to fix directly than to bound via
a 14-test CC subsystem.

SendBuffer gains a `bestEffort: Boolean = false` constructor flag.
When true, markLost drops the lost ranges instead of moving them to
the retransmit queue and lets the underlying byte storage compact as
if the bytes had been ACK'd. The FIN flag (if covered) also stays
sent — best-effort skips FIN re-emission too. The peer may end up
with a truncated stream; moq-lite's per-stream timeouts handle that.

Plumbed through QuicStream → QuicConnection.openUniStream(bestEffort)
→ QuicWebTransportSessionState.openUniStream(bestEffort) →
WebTransportSession.openUniStream(bestEffort). Default is false
everywhere, so reliable streams (HTTP/3 control, moq-lite SUBSCRIBE
bidi, etc.) keep RFC 9000 §3.5 semantics.

MoqLiteSession.openGroupStream now passes `bestEffort = true` —
group streams carry a single Opus packet, are real-time, and don't
benefit from retransmit.

Internal cleanup: `removeOverlap`'s `ackedNotLost: Boolean` parameter
became `OverlapAction { ACK, RETRANSMIT, DROP }` so the third best-
effort disposition has a name. Same code paths, same tests, just
clearer at the call site.

CC plan (quic/plans/2026-05-05-congestion-control.md) is updated to
"parked indefinitely" with a note that this commit is the lighter-
weight alternative that addresses the only practical concern. The
plan is preserved as a reference if a future workload justifies CC.

New tests: SendBufferBestEffortTest (6 cases — reliable baseline,
best-effort drops, FIN drop in best-effort mode, partial overlap,
idempotent stale loss, ACK path still works).

https://claude.ai/code/session_01PYYez8a6sjiakyjAxsfCEQ
2026-05-05 02:06:08 +00:00
Claude 08bb3e14e1 docs(quic): plan congestion control (NewReno per RFC 9002 §7)
Open plan, not started. Conservative scope:
- NewReno reference algorithm (RFC 9002 §7).
- Bytes-in-flight tracking + send-side gating.
- Persistent-congestion handling.
- ~14 unit + 2 integration tests, ~3-5 days work.

Out of scope (deferred follow-ups): pacing, CUBIC, BBR, ECN.

The Why section is honest that this is "good citizen" work, not
"fix a bug" work — the audio cliff was a stream-id / forwarding
issue, not a rate-control issue, and the retransmit subsystem we
just shipped is what production actually needed. Plan stays open as
the natural next item but should not be prioritised over field
validation of the retransmit work.

Reference: neqo's cc/classic_cc.rs.

https://claude.ai/code/session_01PYYez8a6sjiakyjAxsfCEQ
2026-05-05 01:46:00 +00:00
Claude 2053f50f35 fix(quic): discard Initial/Handshake keys per RFC 9001 §4.9
Pre-fix `:quic` held Initial AND Handshake encryption-level state
indefinitely once derived. AEAD cipher state, per-level CRYPTO
buffers, and the per-level sent-packet map all stayed alive for the
lifetime of the connection — a real memory leak for long sessions
(audio rooms run for hours).

LevelState.discardKeys() (idempotent):
- Nulls sendProtection / receiveProtection (frees AEAD state).
- Replaces cryptoSend / cryptoReceive with empty instances.
- Replaces ackTracker with an empty instance.
- Clears sentPackets and resets largestAckedPn /
  largestAckedSentTimeMs.
- Latches keysDiscarded = true.

Hook locations:
- Initial discard (RFC 9001 §4.9.1, client side): in
  QuicConnectionWriter.drainOutbound, after a Handshake-level packet
  is built into the outbound datagram. The next drainOutbound MUST
  NOT touch the Initial level; any retransmitted Initial from the
  peer is silently dropped (receiveProtection == null), which is
  correct per the same RFC since the server has also moved up
  encryption levels by then.
- Handshake discard (RFC 9001 §4.9.2 + §4.1.2, client side): in
  QuicConnectionParser, on receipt of a HANDSHAKE_DONE frame.

Once a level's protection is null, parser-side decrypt at that level
returns null silently (existing receiveProtection == null check) and
writer-side build skips it (existing sendProtection == null check),
so no further code paths needed updating.

New test: KeyDiscardTest (4 cases — Initial keys discarded after
first Handshake packet, Handshake keys still live until
HANDSHAKE_DONE, Handshake keys discarded on HANDSHAKE_DONE,
discardKeys is idempotent).

Listed in the audit-summary deferred-work as item 3
(`No Initial / Handshake key discard`).

https://claude.ai/code/session_01PYYez8a6sjiakyjAxsfCEQ
2026-05-05 01:41:28 +00:00
Claude 70af1953dc docs(quic): record retransmit subsystem implementation status
Update the two affected plan docs now that RFC 9002 retransmit is
shipped on this branch.

quic/plans/2026-05-04-control-frame-retransmit.md:
- Mark plan as shipped 2026-05-05; list the 15 commits that landed it
  (plan + 9 steps + 5 follow-ups + perf + audit).
- Document what changed vs the original scope: the deferred follow-ups
  (STREAM data, CRYPTO, RESET_STREAM/STOP_SENDING/NEW_CONNECTION_ID)
  all shipped on top of the receive-flow-control core.
- Note the binary-search SendBuffer perf optimisation and the
  audit-driven first-call-wins fix.
- Update the cap-workaround status: initialMaxStreamsUni is back to
  10_000 (not the 1_000_000 mentioned in the original Why).

quic/plans/2026-04-26-quic-stack-status.md:
- Phase F downgraded from "partial" to "done (no CC)" — loss
  detection, RTT estimator, PTO, and per-frame retransmit shipped.
- Removed "no STREAM retransmit" / "SendBuffer doesn't retain bytes
  until ACK" from the deliberately-don't-do list (now do).
- Added congestion-control as the new deliberately-don't-do entry.
- Crossed out the corresponding deferred-work items; added congestion
  control as deferred item #8.
- Listed the new recovery test files in the test inventory.
- Linked to the retransmit plan + implementation log.

https://claude.ai/code/session_01PYYez8a6sjiakyjAxsfCEQ
2026-05-05 01:15:29 +00:00
Claude c24630513f docs(quic): plan control-frame retransmit subsystem mirroring neqo
The shipping fix raises initialMaxStreamsUni to 1M to dodge the
moq-rs cliff that fired when our :quic emitted its first
MAX_STREAMS_UNI extension. That sidesteps the symptom but leaves
every ack-eliciting control frame one wire-loss away from a silent
stall (MAX_DATA, MAX_STREAM_DATA, RESET_STREAM, etc.). Browser
QUIC stacks (Firefox neqo and Chrome quiche, both verified by
reading source) implement RFC 9002 §6 loss detection + per-frame
retransmit; :quic does not.

Plan documents:
  - the architecture, mirroring neqo's typed-token shape (chosen
    over quiche's monotonic-control-frame-id deque on code-fit
    grounds — better match for :quic's existing sealed-class
    Frame / MoqLiteControl idioms)
  - file-by-file implementation order in 9 steps that each compile
    and test independently
  - inventory of ~50 tests to port from neqo (9 from fc.rs, ~20
    from recovery/mod.rs, ~10 connection-level, plus codec round
    trips). Each row links to the upstream neqo test by file:line
    and the equivalent Kotlin test name we'll add
  - explicit out-of-scope list (STREAM data retransmit, CRYPTO
    retransmit, congestion control, 0-RTT, multipath) — separate
    follow-ups
  - effort estimate (4–7 days) and acceptance criteria

References upstream code at /tmp/quic-refs/neqo (mozilla/neqo) and
/tmp/quic-refs/quiche (google/quiche), sparse-cloned for source
verification.

https://claude.ai/code/session_01PYYez8a6sjiakyjAxsfCEQ
2026-05-04 22:25:34 +00:00
Claude 4338e5e6c4 docs(quic+nestsClient): post-implementation status + audio-rooms completion plan
Two new module-local plan docs (per CLAUDE.md's "plans live in the owning
module" rule) and a sweep of stale inline phase references.

quic/plans/2026-04-26-quic-stack-status.md:
  Post-mortem of the original docs/plans/2026-04-22 plan. Documents
  what shipped vs what was estimated, the actual package layout (~8.5k
  LoC, 39 test files, 5 audit rounds), the crypto delegation surface
  (Quartz only — no BouncyCastle, no JNI), interop verification status
  (aioquic + picoquic; nests not yet), and known deferred items
  (STREAM retransmit, Initial-key discard, etc.).

nestsClient/plans/2026-04-26-audio-rooms-completion.md:
  Punch list to ship audio rooms end-to-end:
    M1 Listener wire-up in Amethyst UI
    M2 Multi-speaker audience UX
    M3 Foreground service for backgrounded playback
    M4 Manual interop pass against nostrnests.com
    M5 MoQ publisher path (ANNOUNCE / TrackPublisher)
    M6 Capture → encode → publish pipeline
    M7 NestsSpeaker API
    M8 App polish (reconnect, leave cleanup)
    M9 Foreground service for speakers
  ~6 weeks for full audio rooms; ~2 weeks for listener-only MVP.

Inline doc cleanup:
  * Removed "Phase 3a/3c-1/3c-2/3c-3" / "Phase B/C/D-K/L" references
    from active code; replaced with "today" or pointers to the
    completion plan
  * Removed "Kwik-based stub" references; QuicWebTransportFactory and
    surrounding docs now describe :quic as the production path
  * TlsClient header reflects non-null certificateValidator + the
    JdkCertificateValidator / PermissiveCertificateValidator split
  * SendBuffer header documents the best-effort no-retransmit mode
    explicitly (was hidden behind a "Phase L will fix this" note)
  * MoqMessage / MoqObject / MoqSession reflect listener-side as
    shipped + publisher-side as Phase M5

CLAUDE.md:
  * Module list now includes :quic and :nestsClient (was 5 modules,
    now 7)
  * Architecture diagram + sharing philosophy explain what each new
    module owns

No production behaviour changes; doc + comment-only edits. Tests green.

https://claude.ai/code/session_01EC1tfXfap8k8GyKvrxkxZx
2026-04-26 01:38:48 +00:00