fix: resolve sync-loop hang + auto-key-forwarding + UTD visibility #3
No reviewers
Labels
No labels
agent:felinus
agent:human
agent:maximus
priority:p0
priority:p1
priority:p2
priority:p3
status:blocked
status:needs-review
status:wip
type:chore
type:docs
type:feat
type:fix
type:perf
type:refactor
type:test
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
jmz/matrix-bridge!3
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "kitty/sync-wedge-resilience-fixes"
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?
Symptoms (observed 2026-05-01):
After ~3-6 hours of idle MCP traffic, the bridge would stop receiving
new messages from peers. Restarting the bridge process would briefly
appear to fix it, but reads still capped at the timestamp where the
loop originally wedged. Encrypted messages from peers during the
wedge window stayed permanently encrypted on the local device.
Root causes (three independent bugs, all fixed here):
Sync loop wedges silently on dropped long-poll connections.
client.rs::start_synccallsclient.sync_once(timeout=30s)in atight loop. matrix-rust-sdk uses HTTP long-poll for /sync. When the
upstream silently drops the TCP connection (NAT idle timeout, network
flapping), the future awaits bytes that never arrive — no Err raised,
the Err arm of the loop never fires, the loop hangs inside
awaitforever. Fix: wrap each iteration in
tokio::time::timeout(60s, ...).The 30s long-poll timeout the homeserver uses + 30s of slack means a
genuine slow response still completes, but a dead connection now
abandons after 60s and reissues — reqwest opens a fresh connection
automatically.
read_messagesreturns stale data when sync is mid-recovery.room.messages(MessagesOptions::backward())paginates from theroom's
prev_batchtoken in the local store, which is only as freshas the most recent successful sync. Fix: do a defensive
sync_once(timeout=0)(no long-poll, just "give me whatever thehomeserver has now") before each read, wrapped in a 15s outer
timeout so a wedged HTTP layer can't block reads. If the read-time
sync fails for any reason, fall through and serve local store
(degraded but not blocked).
automatic-room-key-forwardingfeature flag was off.Without this matrix-sdk feature, matrix-sdk-crypto's GossipMachine
defaults
room_key_requests_enabledANDroom_key_forwarding_enabledtofalse(gossiping/machine.rs:96-100). This means: (a) we never ask peer
devices for missing Megolm keys when we encounter a UTD, and (b) we
never respond to peers asking us for keys. Forward-direction key
sharing on send still works (that's unconditional), so new messages
decrypt fine — but recovery from any sync gap is impossible. Fix:
enable the feature in Cargo.toml.
UTD events were silently filtered out of
read_messages.mcp/tools.rs::read_messageshad.filter(|m| !m.decryption_failed)— events that failed to decrypt got dropped from the response,
making the room look like it had fewer messages than it actually
did. Without this filter we surface them as
[encrypted message — unable to decrypt]placeholders, which ishonest to the caller and makes the gap obvious.
Net effect:
After this patch, the bridge: (a) self-heals from network blips
within 60s, (b) always serves current homeserver state on read,
(c) properly participates in Megolm key gossip with peers, (d) never
silently swallows events.
Caveat for operators:
Other devices in the room (peer bridges, mobile/desktop clients) need
to ALSO have
automatic-room-key-forwardingenabled before our keyrequests get responses. Element clients have it on by default;
custom-built bridges (matrix-bridge-mcp on other hosts) need the
Cargo.toml change applied + binary rebuilt.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com