Symptom
On iOS, after backgrounding the app briefly (app switch or screen lock), new channel messages no longer render on resume. The channel list and threads stay frozen until the app is force-killed and reopened. macOS desktop is unaffected (never suspends).
Root cause
RelaySessionNotifier.onAppResumed (mobile/lib/shared/relay/relay_session.dart) early-returns when the background stint was shorter than the grace window AND state.status == SessionStatus.connected. But the transport can be half-open after even a brief suspend: iOS drops the network on screen lock or rebinds NAT on a Wi-Fi/cellular switch, and no close frame ever reaches the client (the Dart VM is frozen, so the grace timer that would have disconnected cleanly never fired either). The status flag still says connected, the early return keeps the dead socket, live subscriptions stay silent, and nothing ever triggers the (existing, correct) reconnect+replay machinery.
The wall-clock guard added for long background stints does not cover this case: the stint is short, the socket is dead anyway.
Repro
- iOS device, open a channel.
- Background the app for a few seconds (or lock the screen), in conditions where the transport drops (screen lock on Wi-Fi is a reliable trigger).
- From another client, post to the channel.
- Resume the app: the message never appears. Force-kill + reopen: it appears.
Proposed fix
On the early-return path, verify liveness instead of trusting the flag: a minimal REQ that resolves on EOSE (3s timeout). On timeout/error, call reconnect(), which already replays live subscriptions with the since-skew. PR incoming.
Symptom
On iOS, after backgrounding the app briefly (app switch or screen lock), new channel messages no longer render on resume. The channel list and threads stay frozen until the app is force-killed and reopened. macOS desktop is unaffected (never suspends).
Root cause
RelaySessionNotifier.onAppResumed(mobile/lib/shared/relay/relay_session.dart) early-returns when the background stint was shorter than the grace window ANDstate.status == SessionStatus.connected. But the transport can be half-open after even a brief suspend: iOS drops the network on screen lock or rebinds NAT on a Wi-Fi/cellular switch, and no close frame ever reaches the client (the Dart VM is frozen, so the grace timer that would have disconnected cleanly never fired either). The status flag still says connected, the early return keeps the dead socket, live subscriptions stay silent, and nothing ever triggers the (existing, correct) reconnect+replay machinery.The wall-clock guard added for long background stints does not cover this case: the stint is short, the socket is dead anyway.
Repro
Proposed fix
On the early-return path, verify liveness instead of trusting the flag: a minimal REQ that resolves on EOSE (3s timeout). On timeout/error, call
reconnect(), which already replays live subscriptions with the since-skew. PR incoming.