Ghost media-key delivery latency: ~30s of silence before Discord speakers are audible in Matrix (stale-device bloat + OTK claim failures) #42
Labels
No labels
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
dark/nether-voicebridge#42
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Symptom
When a Matrix user joins a bridged call, Discord speakers (ghosts) appear in the
Element roster staggered ("one by one") and then remain silent for ~30 seconds
after appearing, before their audio becomes audible. Audio itself is correct once it
starts; the delay is entirely in per-device Olm media-key delivery (Discord→Matrix).
Live-observed 2026-06-24 on the VPS staging bridge (encrypted "Dark Voice" room).
Evidence (timings, user =
@dark)Ghost
jackalope:ghost publishing Discord speaker audioat 00:56:19.7ghost media key deliveredat 00:56:49.8 → ~30 s of silenceghost media key reached NO recipient devices(transient,unreachable=12)Olm session establishment to the receiving user:
00:56:48Tried to create new Olm sessions, but the signed one-time key was missing for some devices missing_devices_by_user={"@dark": {WwRz4rI5q7, fw6bHG5CYL, l4esNRFzn1, wk18E4QVw2, yVlHOeMTCZ}}→Established new Olm sessions sessions={}(claim returned nothing)00:56:54retry finally established a session to the live deviceRoot causes (compounding)
@darkhas ~12 devices; 5 are permanently dead (noone-time keys on the homeserver) —
WwRz4rI5q7, fw6bHG5CYL, l4esNRFzn1, wk18E4QVw2, yVlHOeMTCZ. Every ghost key-send wastes a failing/keys/claimon these 5(the constant
unreachable=5). Likely accumulated via per-store device minting(#30) across staging/prod/dev/wiped stores.
first claim returned no OTKs and had to be retried, adding seconds.
serialized, so the first key to the live device lands ~30 s after audio starts.
Proposed bridge-side optimizations
immediately rather than blocking the send behind new
/keys/claimround-trips forall devices (so the active device hears audio ASAP, slow/new devices catch up).
stop re-claiming them every burst (cuts wasted round-trips + the
NO recipientnoise).online (presence) instead of waiting for first speech, so the key is in place before
audio flows.
Environmental mitigation (separate from the bridge)
Tuwunel one-time-key replenishment.
Notes
Part of the deferred "crypto/sync latency" cluster surfaced alongside the puppet
DriverDisconnectrecovery work (2026-06-24). Thereached NO recipient devicesWARN and
/syncerrors observed in the same window are related symptoms of thisenvironment. Bridge correctness is fine; this is a latency/UX issue.
Update: device-scoped key targeting is the real fix (supersedes the failure-cache idea above)
A closer pass at the code changed the recommendation.
Also observed: the same root cause causes mid-call dropouts, not just join latency
@dark) had an 8sE2eeStateChanged: MissingKeywindow(
00:57:13→00:57:21) — bridge couldn't decrypt them → Discord side heard them drop —correlating exactly with a new participant joining (
Victoria Ladyat00:57:10). Plus~1s blips during a leave/rejoin. A Discord speaker reported as dropping (
Mustapha Mond)had continuous
ghost audio healthframes — capture never stalled; the loss wasreceiver-side (missing his rotated key). Every join/leave triggers an EC media-key
rotation that must re-propagate over the same slow Olm path.
Root cause is narrower than "stale devices" — it's over-broad fan-out
enumerate_key_recipients(ghost.rs:1437) readsm.call.memberbut dedups to user,discarding the device; the Olm transport (
key_transport.rs:219) then fans out to everydevice via
get_user_devices. Butm.call.memberis per-device (ruma-events 0.34active_memberships()exposesdevice_id; state key{user}_{device}), and the receiveside already works in device terms (
keys.rsclaimed_device_id). Evidence:delivered=7 unreachable=5for@dark= the bridge keys all ~12 of their devices when only one is inthe call. The 5 "unreachable" are verified-but-offline sessions (OTKs exhausted; fallback
only partial —
fallback_key_usedtrue 6× / false 13×), claimed and failed 45× in onesession. These users' extra sessions are legitimate, not junk — so "delete stale devices" is
not the answer.
Recommended fix
Send each ghost's media key only to the device(s) actually in the call (from
active_memberships()), not all of a user's devices:enumerate_key_recipients→ return(user, device_id)pairs.encryption().get_device(...)) instead ofget_user_devices(...).devices().For
@darkthis is ~12 devices → 1: it removes the doomed dead-device claims and theneedless deliveries at the root, shrinks the initial ~30s silence (1 session to establish,
not 12), kills the
NO recipientwindows, and scales with "many users × many devices." Thissupersedes the earlier failure-cache idea (dead devices simply aren't targeted anymore).
Out of scope / honest limits
@dark8s window = subscriber waiting for a remote client'srotated key) are a separate concern (subscriber sync responsiveness).
multi-device user) still hears all ghosts after narrowing; fall back to per-user fan-out
if a real client needs the key on a non-membership device.
Resolved in v0.2.4 by three changes to ghost media-key delivery, all live-verified on staging:
m.call.member), not all of a listener's sessions.@darkwent from 12 devices → 1 (delivered=7 unreachable=5→delivered=1 unreachable=0); the doomed OTK claims against offline/stale devices and the "nobody can decrypt" windows are gone. This makes the "stale device bloat" moot at the root — no device cleanup required./sync— filtered bootstrap (timeline/ephemeral/presence/account-data dropped, membership+encryption kept), ~halving each bootstrap and lightening homeserver load.Result: Discord speakers audible in Matrix in ~1–3s (was ~20–37s), and no more multi-second dropouts on every membership-driven key rotation. Correctness held throughout (every participant still hears all ghosts).
Out of scope (separate): receive-side rotation gaps (the bridge subscriber waiting for a remote human's rotated key) and Element Call's rotation cadence.