fix: keep reconnecting across a long outage instead of evicting on NeighborDown

Field test of the long-outage path failed: ~31s into a Wi-Fi drop the peer
vanished with no "Reconnecting..." indicator and never came back. Logs showed
the chain `Gossip NeighborDown -> Removed peer -> Transport: stopped
supervising peer`.

Root cause: a transient gossip NeighborDown was routed to RoomEvent::PeerLeft,
same as a graceful leave, so core called disconnect_peer -> supervisor.abort().
That aborted the very reconnect supervisor that was meant to redial -- before
its own reconnect loop (which re-emits Connecting and re-dials the retained
09acefd address with backoff) ever ran. The supervisor + retained-address fix
were effectively dead code in the field, which is also why the loopback tests
(they drive the supervisor directly) never caught it.

Fix: decouple a transient drop from a graceful leave.
- gossip.rs: NeighborDown now emits the new RoomEvent::PeerConnectionLost
  instead of PeerLeft. A graceful GossipMessage::Leave still emits PeerLeft.
- core: on PeerConnectionLost, do NOT disconnect the peer. Keep its supervisor
  alive (it redials the retained address and drives the yellow indicator) and
  arm a per-peer reconnect grace timer (RECONNECT_GRACE = 45s, comfortably past
  the ~30s QUIC idle timeout). The peer is evicted only if the link hasn't
  recovered when the timer fires. A gossip rejoin (PeerJoined/PeerUpdated) or a
  transport reconnect (ConnEvent::Connected) cancels the timer first; session
  shutdown aborts all pending timers so none fire a stray eviction.

Builds clean, clippy clean, 3 transport tests pass. NOT yet field-verified --
re-test the long-outage path on a real call.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-05-31 23:29:26 -04:00
co-authored by Claude Opus 4.8
parent 039c34322c
commit be818467dd
3 changed files with 93 additions and 8 deletions
+8
View File
@@ -32,8 +32,16 @@ pub struct PeerState {
#[derive(Debug, Clone)]
pub enum RoomEvent {
PeerJoined(EndpointId, PeerState),
/// A peer left *gracefully* (it broadcast a `Leave`). Evict it immediately.
PeerLeft(EndpointId),
PeerUpdated(EndpointId, PeerState),
/// A peer's gossip neighbour link dropped without a graceful `Leave` (network
/// blip, crash, walked out of range). This is transient: the audio supervisor
/// keeps redialing, so the core marks the peer "reconnecting" and only evicts
/// it if the link hasn't recovered within the reconnect grace window. Distinct
/// from `PeerLeft` precisely so a momentary `NeighborDown` can't tear down the
/// reconnect path the way it used to.
PeerConnectionLost(EndpointId),
}
/// Transport-level link state for a peer, surfaced so the UI can show when a