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
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
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