cultnet-ts removed a fragment buffer only on successful reassembly, with
no cap, no eviction and no timeout. A fragment set that loses one
fragment was stranded for the life of the session, so the map grew
without bound under any loss on a fragmenting channel. cultnet-rs at
least fails closed at 64 pending sets; TypeScript just consumes memory.
This is live, not theoretical. Ghostlight sends Heimdall sealed envelopes
over CultMesh RUDP with max_fragment_bytes = 2048
(Ghostlight/crates/ghostlight-dungeon/src/heimdall.rs:697), and Heimdall's
private command plane reassembles them through this path. Every lost
fragment on that link left a buffer nothing would ever collect, on a
long-running auth service.
Caps pending sets at 64 to match cultnet-rs's max_pending_fragment_sets,
and evicts the oldest stranded set rather than refusing the payload:
a receiver that stops accepting fragmented traffic after a bounded number
of losses is a denial of service delivered by the network. Map iteration
is insertion-ordered, so the first key is the oldest. fragmentSetsEvicted
and pendingFragmentSetCount make it observable.
Found by the Muninn media load test, which hit the Rust half of the same
defect; cultcache-py has the same unbounded map and is not touched here.
npm run test --workspace=packages/cultnet-ts -> pass 54, fail 0
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>