From c72afd03985d576ae8505e7c671f5f92be38b8b5 Mon Sep 17 00:00:00 2001 From: Gordon Date: Sun, 1 Nov 2026 17:33:43 +0800 Subject: [PATCH 1/2] fix(remote-connect): drop offline peer controllers from event fanout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `peer_host_invoke::retain_online_controllers` exists on the desktop host but nothing ever calls it — the CLI has the equivalent wiring through `peer_host::update_controller_presence`, the desktop `DevicePresence` handler only prunes the host-stream hub. A controller that goes offline therefore stays in `controllers` forever. Every peer event keeps fanning out to it and the relay rejects each one, which is what fills the log with: peer event fanout to failed: Relay event rejected Reconnecting does not help: the stale id is still there, so the failures continue alongside the new connection. Retain the controller set against account presence, mirroring the CLI, and fail closed permission requests whose last control surface vanished. --- src/apps/desktop/src/api/remote_connect_api.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/apps/desktop/src/api/remote_connect_api.rs b/src/apps/desktop/src/api/remote_connect_api.rs index bf4eb99b5e..19652053ef 100644 --- a/src/apps/desktop/src/api/remote_connect_api.rs +++ b/src/apps/desktop/src/api/remote_connect_api.rs @@ -28,7 +28,7 @@ use openbitfun_services_integrations::remote_connect::{ }; use regex::Regex; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::path::PathBuf; use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; use std::sync::{Arc, OnceLock}; @@ -2778,6 +2778,21 @@ async fn connect_route_devices(route: RelayRoute) -> Result = + devices.iter().map(|d| d.device_id.clone()).collect(); + let detached = + crate::api::peer_host_invoke::retain_online_controllers(&online_ids); + if !detached.is_empty() { + let _ = crate::api::peer_host_invoke::fail_closed_permission_requests( + detached, + "Peer controller offline", + ) + .await; + } // Offline presence does not revoke an account device or its // permission mailbox. Reconnect resumes the same ownership. if !device_routing_owner_is_current(&event_owner).await { From 0b00d02af9531d94a77e73151abbff47f5de7b2d Mon Sep 17 00:00:00 2001 From: Bob Lee Date: Mon, 2 Nov 2026 12:03:44 +0800 Subject: [PATCH 2/2] fix(peer): preserve permission mailbox during presence cleanup Scope subscriber removal to the relay reporting presence, including LAN. Offline presence is not permission revocation. Co-authored-by: OpenBitFun <318544290+bitfun-ai@users.noreply.github.com> --- src/apps/desktop/src/api/peer_host_invoke.rs | 68 +++++++++++++------ .../desktop/src/api/remote_connect_api.rs | 10 +-- 2 files changed, 49 insertions(+), 29 deletions(-) diff --git a/src/apps/desktop/src/api/peer_host_invoke.rs b/src/apps/desktop/src/api/peer_host_invoke.rs index 2e899e30a2..097d3bbecd 100644 --- a/src/apps/desktop/src/api/peer_host_invoke.rs +++ b/src/apps/desktop/src/api/peer_host_invoke.rs @@ -137,25 +137,21 @@ pub(crate) fn detach_route_controller(route: RelayRoute, device_id: &str) -> Vec fn retain_online_in_state( state: &mut PeerControlState, + route: RelayRoute, online_device_ids: &HashSet, -) -> Vec { - let had_controllers = !state.controllers.is_empty(); - state.controllers.retain(|(route, device_id)| { - *route != RelayRoute::Official || online_device_ids.contains(device_id) - }); - if had_controllers && state.controllers.is_empty() { - return state.permission_request_ids.drain().collect(); - } - Vec::new() +) { + state + .controllers + .retain(|(owner, device_id)| *owner != route || online_device_ids.contains(device_id)); } -/// Remove controllers missing from account presence and return requests that -/// lost their final control surface. -pub fn retain_online_controllers(online_device_ids: &HashSet) -> Vec { +/// Remove offline delivery subscribers only on the reporting route. Presence +/// loss does not revoke pending permissions; reconnect recovers the mailbox. +pub(crate) fn retain_online_controllers(route: RelayRoute, online_device_ids: &HashSet) { let Ok(mut state) = peer_control_state().lock() else { - return Vec::new(); + return; }; - retain_online_in_state(&mut state, online_device_ids) + retain_online_in_state(&mut state, route, online_device_ids); } /// Clear all attached controllers after the device-routing stream closes. @@ -749,14 +745,46 @@ mod tests { } #[test] - fn presence_loss_drains_requests_only_when_every_controller_is_offline() { + fn presence_loss_preserves_pending_permissions_after_the_last_controller_leaves() { let mut state = control_state(&["controller-a", "controller-b"], &["request-1"]); let online = HashSet::from(["controller-b".to_string()]); - assert!(retain_online_in_state(&mut state, &online).is_empty()); + retain_online_in_state(&mut state, RelayRoute::Official, &online); + assert_eq!(state.controllers.len(), 1); - assert_eq!( - retain_online_in_state(&mut state, &HashSet::new()), - vec!["request-1".to_string()] - ); + retain_online_in_state(&mut state, RelayRoute::Official, &HashSet::new()); + assert!(state.controllers.is_empty()); + assert!(state.permission_request_ids.contains("request-1")); + } + + #[test] + fn presence_updates_only_remove_subscribers_from_the_reporting_route() { + for route in [RelayRoute::Official, RelayRoute::Lan] { + let other = if route == RelayRoute::Official { + RelayRoute::Lan + } else { + RelayRoute::Official + }; + let mut state = PeerControlState::default(); + state.controllers.extend([ + (route, "shared-device".to_string()), + (other, "shared-device".to_string()), + (route, "online-device".to_string()), + ]); + state.permission_request_ids.insert("request-1".to_string()); + + retain_online_in_state( + &mut state, + route, + &HashSet::from(["online-device".to_string()]), + ); + assert_eq!(state.controllers.len(), 2); + assert!(state + .controllers + .contains(&(other, "shared-device".to_string()))); + assert!(state + .controllers + .contains(&(route, "online-device".to_string()))); + assert!(state.permission_request_ids.contains("request-1")); + } } } diff --git a/src/apps/desktop/src/api/remote_connect_api.rs b/src/apps/desktop/src/api/remote_connect_api.rs index 19652053ef..cb37494896 100644 --- a/src/apps/desktop/src/api/remote_connect_api.rs +++ b/src/apps/desktop/src/api/remote_connect_api.rs @@ -2784,15 +2784,7 @@ async fn connect_route_devices(route: RelayRoute) -> Result = devices.iter().map(|d| d.device_id.clone()).collect(); - let detached = - crate::api::peer_host_invoke::retain_online_controllers(&online_ids); - if !detached.is_empty() { - let _ = crate::api::peer_host_invoke::fail_closed_permission_requests( - detached, - "Peer controller offline", - ) - .await; - } + crate::api::peer_host_invoke::retain_online_controllers(route, &online_ids); // Offline presence does not revoke an account device or its // permission mailbox. Reconnect resumes the same ownership. if !device_routing_owner_is_current(&event_owner).await {