diff --git a/codex-rs/app-server-transport/src/transport/remote_control/mod.rs b/codex-rs/app-server-transport/src/transport/remote_control/mod.rs index f02a73b194b..54b33189c6d 100644 --- a/codex-rs/app-server-transport/src/transport/remote_control/mod.rs +++ b/codex-rs/app-server-transport/src/transport/remote_control/mod.rs @@ -52,6 +52,7 @@ use std::sync::atomic::Ordering; use tokio::sync::Semaphore; use tokio::sync::SemaphorePermit; use tokio::sync::mpsc; +use tokio::sync::mpsc::error::TrySendError; use tokio::sync::oneshot; use tokio::sync::watch; use tokio::task::JoinHandle; @@ -65,6 +66,8 @@ pub struct RemoteControlStartConfig { pub installation_id: String, } +const RECONNECT_CHANNEL_CAPACITY: usize = 1; + pub(super) struct QueuedServerEnvelope { pub(super) event: ServerEvent, pub(super) client_id: ClientId, @@ -75,7 +78,7 @@ pub(super) struct QueuedServerEnvelope { #[derive(Clone)] pub struct RemoteControlHandle { enabled_tx: Arc>, - reconnect_tx: mpsc::UnboundedSender, + reconnect_tx: mpsc::Sender, next_reconnect_generation: Arc, status_tx: Arc>, state_db_available: bool, @@ -263,33 +266,33 @@ impl RemoteControlHandle { response = Some(Err(RemoteControlReconnectUnavailable::Disabled)); return false; } - if self.reconnect_tx.is_closed() { - response = Some(Err(RemoteControlReconnectUnavailable::WorkerUnavailable)); - return false; - } - if status.status == RemoteControlConnectionStatus::Connecting { - response = Some(Ok(status.clone())); - return false; - } - + let reconnect_permit = match self.reconnect_tx.try_reserve() { + Ok(reconnect_permit) => reconnect_permit, + Err(TrySendError::Full(())) => { + response = Some(Ok(status.clone())); + return false; + } + Err(TrySendError::Closed(())) => { + response = Some(Err(RemoteControlReconnectUnavailable::WorkerUnavailable)); + return false; + } + }; let generation = self .next_reconnect_generation .fetch_add(1, Ordering::Relaxed) .wrapping_add(1); - if self.reconnect_tx.send(generation).is_err() { - response = Some(Err(RemoteControlReconnectUnavailable::WorkerUnavailable)); - return false; - } + reconnect_permit.send(generation); let next_status = remote_control_status_with_connection_status( status, RemoteControlConnectionStatus::Connecting, ); + let status_changed = next_status != *status; reconnect_generation = Some(generation); previous_status = Some(status.status); *status = next_status.clone(); response = Some(Ok(next_status)); - true + status_changed }); let response = response.expect("remote control reconnect must produce a response"); @@ -321,7 +324,7 @@ impl RemoteControlHandle { environment_id = ?status.environment_id, installation_id = %status.installation_id, server_name = %status.server_name, - "remote control reconnect coalesced with an existing connection attempt" + "remote control reconnect coalesced with a pending request" ); } response @@ -887,7 +890,7 @@ pub async fn start_remote_control( }; let (enabled_tx, enabled_rx) = watch::channel(initial_enabled); - let (reconnect_tx, reconnect_rx) = mpsc::unbounded_channel(); + let (reconnect_tx, reconnect_rx) = mpsc::channel(RECONNECT_CHANNEL_CAPACITY); let current_enrollment = Arc::new(RemoteControlEnrollmentState::new(/*enrollment*/ None)); let websocket_current_enrollment = current_enrollment.clone(); let pairing_persistence_key_required = app_server_client_name_rx.is_some(); diff --git a/codex-rs/app-server-transport/src/transport/remote_control/tests.rs b/codex-rs/app-server-transport/src/transport/remote_control/tests.rs index 6010b0896d6..c33bf611860 100644 --- a/codex-rs/app-server-transport/src/transport/remote_control/tests.rs +++ b/codex-rs/app-server-transport/src/transport/remote_control/tests.rs @@ -139,9 +139,9 @@ fn test_server_name() -> String { fn remote_control_handle_with_reconnect_receiver( remote_control_url: &str, auth_manager: Arc, -) -> (RemoteControlHandle, mpsc::UnboundedReceiver) { +) -> (RemoteControlHandle, mpsc::Receiver) { let (enabled_tx, _enabled_rx) = watch::channel(/*init*/ true); - let (reconnect_tx, reconnect_rx) = mpsc::unbounded_channel(); + let (reconnect_tx, reconnect_rx) = mpsc::channel(RECONNECT_CHANNEL_CAPACITY); let (status_tx, _status_rx) = watch::channel(RemoteControlStatusChangedNotification { status: RemoteControlConnectionStatus::Connecting, server_name: test_server_name(), @@ -203,7 +203,35 @@ fn remote_control_reconnect_rejects_unavailable_worker() { } #[test] -fn remote_control_reconnect_coalesces_while_connecting() { +fn remote_control_reconnect_rejects_unavailable_state_db() { + let (mut handle, _reconnect_rx) = remote_control_handle_with_reconnect_receiver( + "http://127.0.0.1:1/backend-api/", + remote_control_auth_manager(), + ); + handle.state_db_available = false; + + assert_eq!( + handle.reconnect(), + Err(RemoteControlReconnectUnavailable::StateDbUnavailable) + ); +} + +#[test] +fn remote_control_reconnect_rejects_disabled_remote_control() { + let (handle, _reconnect_rx) = remote_control_handle_with_reconnect_receiver( + "http://127.0.0.1:1/backend-api/", + remote_control_auth_manager(), + ); + handle.disable(); + + assert_eq!( + handle.reconnect(), + Err(RemoteControlReconnectUnavailable::Disabled) + ); +} + +#[test] +fn remote_control_reconnect_coalesces_pending_requests() { let (handle, mut reconnect_rx) = remote_control_handle_with_reconnect_receiver( "http://127.0.0.1:1/backend-api/", remote_control_auth_manager(), @@ -221,6 +249,26 @@ fn remote_control_reconnect_coalesces_while_connecting() { assert!(reconnect_rx.is_empty()); } +#[test] +fn remote_control_reconnect_queues_retry_after_worker_receives_request() { + let (handle, mut reconnect_rx) = remote_control_handle_with_reconnect_receiver( + "http://127.0.0.1:1/backend-api/", + remote_control_auth_manager(), + ); + handle.publish_status(RemoteControlConnectionStatus::Connected); + + let first = handle.reconnect().expect("first reconnect should queue"); + assert_eq!(reconnect_rx.try_recv(), Ok(1)); + let second = handle + .reconnect() + .expect("retry should queue after the worker receives the first request"); + + assert_eq!(first.status, RemoteControlConnectionStatus::Connecting); + assert_eq!(second, first); + assert_eq!(reconnect_rx.try_recv(), Ok(2)); + assert!(reconnect_rx.is_empty()); +} + fn remote_control_server_token_response( server_id: &str, environment_id: &str, @@ -718,6 +766,7 @@ async fn remote_control_handle_reconnects_without_disabling_or_reenrolling() { let client_id = ClientId("client-1".to_string()); let stream_id = StreamId("stream-1".to_string()); + let subscribe_cursor = "cursor-1".to_string(); let initialize_message = JSONRPCMessage::Request(codex_app_server_protocol::JSONRPCRequest { id: codex_app_server_protocol::RequestId::Integer(1), method: "initialize".to_string(), @@ -738,7 +787,7 @@ async fn remote_control_handle_reconnects_without_disabling_or_reenrolling() { client_id: client_id.clone(), stream_id: Some(stream_id.clone()), seq_id: Some(1), - cursor: None, + cursor: Some(subscribe_cursor.clone()), }, ) .await; @@ -814,12 +863,6 @@ async fn remote_control_handle_reconnects_without_disabling_or_reenrolling() { remote_handle.reconnect().expect("reconnect should succeed"), connecting_status ); - assert_eq!( - remote_handle - .reconnect() - .expect("duplicate reconnect should coalesce"), - connecting_status - ); expect_remote_control_status_snapshot(&mut status_rx, connecting_status).await; timeout(Duration::from_secs(1), first_websocket.next()) .await @@ -831,6 +874,12 @@ async fn remote_control_handle_reconnects_without_disabling_or_reenrolling() { second_handshake_request.headers.get("authorization"), Some(&format!("Bearer {TEST_REMOTE_CONTROL_SERVER_TOKEN}")) ); + assert_eq!( + second_handshake_request + .headers + .get("x-codex-subscribe-cursor"), + Some(&subscribe_cursor) + ); expect_remote_control_status_snapshot( &mut status_rx, RemoteControlStatusChangedNotification { diff --git a/codex-rs/app-server-transport/src/transport/remote_control/tests/clients_tests.rs b/codex-rs/app-server-transport/src/transport/remote_control/tests/clients_tests.rs index 8524a2a4be8..f1ab9875a25 100644 --- a/codex-rs/app-server-transport/src/transport/remote_control/tests/clients_tests.rs +++ b/codex-rs/app-server-transport/src/transport/remote_control/tests/clients_tests.rs @@ -14,7 +14,7 @@ fn client_management_handle( auth_manager: Arc, ) -> RemoteControlHandle { let (enabled_tx, _enabled_rx) = watch::channel(/*init*/ false); - let (reconnect_tx, _reconnect_rx) = mpsc::unbounded_channel(); + let (reconnect_tx, _reconnect_rx) = mpsc::channel(/*buffer*/ 1); let (status_tx, _status_rx) = watch::channel(RemoteControlStatusChangedNotification { status: RemoteControlConnectionStatus::Disabled, server_name: test_server_name(), diff --git a/codex-rs/app-server-transport/src/transport/remote_control/websocket.rs b/codex-rs/app-server-transport/src/transport/remote_control/websocket.rs index 1afed4c1f90..a65edec7fbf 100644 --- a/codex-rs/app-server-transport/src/transport/remote_control/websocket.rs +++ b/codex-rs/app-server-transport/src/transport/remote_control/websocket.rs @@ -282,7 +282,7 @@ pub(crate) struct RemoteControlWebsocket { server_event_rx: Arc>>, used_rx: watch::Receiver, enabled_rx: watch::Receiver, - reconnect_rx: mpsc::UnboundedReceiver, + reconnect_rx: mpsc::Receiver, } pub(crate) struct RemoteControlWebsocketConfig { @@ -370,6 +370,43 @@ impl RemoteControlStatusPublisher { } } + fn publish_status_if_no_pending_reconnect( + &self, + reconnect_rx: &mpsc::Receiver, + connection_status: RemoteControlConnectionStatus, + ) -> bool { + let mut no_pending_reconnect = false; + let mut status_change = None; + self.tx.send_if_modified(|status| { + no_pending_reconnect = reconnect_rx.is_empty(); + if !no_pending_reconnect { + return false; + } + + let next_status = + remote_control_status_with_connection_status(status, connection_status); + if *status == next_status { + return false; + } + + status_change = Some((status.clone(), next_status.clone())); + *status = next_status; + true + }); + if let Some((previous_status, next_status)) = status_change { + info!( + previous_status = ?previous_status.status, + next_status = ?next_status.status, + previous_environment_id = ?previous_status.environment_id, + next_environment_id = ?next_status.environment_id, + installation_id = %next_status.installation_id, + server_name = %next_status.server_name, + "remote control websocket status changed" + ); + } + no_pending_reconnect + } + fn publish_status_if_enabled( &self, enabled_rx: &watch::Receiver, @@ -457,7 +494,7 @@ impl RemoteControlWebsocket { channels: RemoteControlChannels, shutdown_token: CancellationToken, enabled_rx: watch::Receiver, - reconnect_rx: mpsc::UnboundedReceiver, + reconnect_rx: mpsc::Receiver, ) -> Self { let shutdown_token = shutdown_token.child_token(); let (server_event_tx, server_event_rx) = mpsc::channel(super::CHANNEL_CAPACITY); @@ -778,6 +815,13 @@ impl RemoteControlWebsocket { .await } => connect_result, }; + if let Ok(reconnect_generation) = self.reconnect_rx.try_recv() { + self.consume_reconnect_requests( + reconnect_generation, + "completed active connection attempt", + ); + continue; + } match connect_result { Ok((websocket_connection, response, active_control_auth)) => { @@ -786,8 +830,18 @@ impl RemoteControlWebsocket { } self.reconnect_attempt = 0; self.auth_recovery = self.auth_manager.unauthorized_recovery(); - self.status_publisher - .publish_status(RemoteControlConnectionStatus::Connected); + if !self + .status_publisher + .publish_status_if_no_pending_reconnect( + &self.reconnect_rx, + RemoteControlConnectionStatus::Connected, + ) + { + self.consume_pending_reconnect_requests( + "completed stale connection attempt", + ); + continue; + } let enrollment = self.current_enrollment.snapshot(); info!( websocket_url = %remote_control_target.websocket_url, @@ -813,8 +867,18 @@ impl RemoteControlWebsocket { let reconnect_delay = if err.kind() == ErrorKind::WouldBlock { REMOTE_CONTROL_ACCOUNT_ID_RETRY_INTERVAL } else { - self.status_publisher - .publish_status(RemoteControlConnectionStatus::Errored); + if !self + .status_publisher + .publish_status_if_no_pending_reconnect( + &self.reconnect_rx, + RemoteControlConnectionStatus::Errored, + ) + { + self.consume_pending_reconnect_requests( + "completed stale connection attempt", + ); + continue; + } let reconnect_attempt = self.reconnect_attempt.saturating_add(1); let (reconnect_delay, reconnect_backoff_reset) = next_reconnect_delay(&mut self.reconnect_attempt); @@ -2038,7 +2102,7 @@ mod tests { status_publisher: RemoteControlStatusPublisher, shutdown_token: CancellationToken, enabled_rx: watch::Receiver, - reconnect_rx: mpsc::UnboundedReceiver, + reconnect_rx: mpsc::Receiver, ) -> RemoteControlWebsocket { let remote_control_url = "http://localhost/backend-api/".to_string(); let remote_control_target = normalize_remote_control_url(&remote_control_url) @@ -2071,15 +2135,15 @@ mod tests { status_publisher.publish_status(RemoteControlConnectionStatus::Errored); let shutdown_token = CancellationToken::new(); let (_enabled_tx, enabled_rx) = watch::channel(true); - let (reconnect_tx, reconnect_rx) = mpsc::unbounded_channel(); + let (reconnect_tx, reconnect_rx) = mpsc::channel(/*buffer*/ 3); reconnect_tx - .send(7) + .try_send(7) .expect("reconnect command should queue"); reconnect_tx - .send(8) + .try_send(8) .expect("second reconnect command should queue"); reconnect_tx - .send(9) + .try_send(9) .expect("third reconnect command should queue"); let mut websocket = test_remote_control_websocket( transport_event_tx, @@ -2139,7 +2203,7 @@ mod tests { let state_db = remote_control_state_runtime(&codex_home).await; let shutdown_token = CancellationToken::new(); let (_enabled_tx, enabled_rx) = watch::channel(true); - let (reconnect_tx, reconnect_rx) = mpsc::unbounded_channel(); + let (reconnect_tx, reconnect_rx) = mpsc::channel(/*buffer*/ 1); let mut enrollment = remote_control_enrollment(Some(TEST_REMOTE_CONTROL_SERVER_TOKEN)); enrollment.remote_control_target = remote_control_target.clone(); let auth_manager = remote_control_auth_manager(); @@ -2181,7 +2245,7 @@ mod tests { .expect("connection failure status should arrive") .expect("status channel should remain open"); reconnect_tx - .send(10) + .try_send(10) .expect("reconnect command should send"); timeout(TEST_RECONNECT_WAKE_TIMEOUT, second_connection_rx.recv()) .await @@ -2194,6 +2258,110 @@ mod tests { server_task.await.expect("server task should join"); } + #[tokio::test] + async fn reconnect_queues_during_active_connection_attempt() { + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("listener should bind"); + let remote_control_url = remote_control_url_for_listener(&listener); + let remote_control_target = + normalize_remote_control_url(&remote_control_url).expect("target should normalize"); + let (connection_tx, mut connection_rx) = mpsc::channel(2); + let (finish_first_connection_tx, finish_first_connection_rx) = oneshot::channel(); + let server_task = tokio::spawn(async move { + let (first_stream, _) = accept_http_request(&listener).await; + connection_tx + .send(()) + .await + .expect("first connection signal should send"); + finish_first_connection_rx + .await + .expect("first connection completion signal should send"); + respond_with_status_and_headers( + first_stream, + "503 Service Unavailable", + &[], + "retry later", + ) + .await; + + let (second_stream, _) = accept_http_request(&listener).await; + connection_tx + .send(()) + .await + .expect("second connection signal should send"); + respond_with_status_and_headers( + second_stream, + "503 Service Unavailable", + &[], + "retry later", + ) + .await; + }); + + let (transport_event_tx, _transport_event_rx) = mpsc::channel(1); + let (status_publisher, _status_rx) = remote_control_status_channel(); + let codex_home = TempDir::new().expect("temp dir should create"); + let state_db = remote_control_state_runtime(&codex_home).await; + let shutdown_token = CancellationToken::new(); + let (_enabled_tx, enabled_rx) = watch::channel(true); + let (reconnect_tx, reconnect_rx) = mpsc::channel(/*buffer*/ 1); + let mut enrollment = remote_control_enrollment(Some(TEST_REMOTE_CONTROL_SERVER_TOKEN)); + enrollment.remote_control_target = remote_control_target.clone(); + let auth_manager = remote_control_auth_manager(); + let mut websocket = RemoteControlWebsocket::new( + RemoteControlWebsocketConfig { + remote_control_url, + installation_id: TEST_INSTALLATION_ID.to_string(), + remote_control_target: Some(remote_control_target), + server_name: "test-server".to_string(), + }, + Some(state_db), + auth_manager, + RemoteControlChannels { + transport_event_tx, + status_publisher, + current_enrollment: test_current_enrollment(Some(enrollment)), + pairing_persistence_key: watch::channel(None).0, + }, + shutdown_token.clone(), + enabled_rx, + reconnect_rx, + ); + let connect_shutdown_token = shutdown_token.child_token(); + let connect_task = tokio::spawn(async move { + websocket + .connect( + &connect_shutdown_token, + /*app_server_client_name*/ None, + ) + .await + }); + + timeout(TEST_HTTP_ACCEPT_TIMEOUT, connection_rx.recv()) + .await + .expect("first connection should arrive") + .expect("first connection signal should exist"); + reconnect_tx + .try_send(10) + .expect("reconnect command should send"); + timeout(Duration::from_millis(100), connection_rx.recv()) + .await + .expect_err("reconnect should not cancel the active connection attempt"); + finish_first_connection_tx + .send(()) + .expect("first connection should be allowed to finish"); + timeout(TEST_RECONNECT_WAKE_TIMEOUT, connection_rx.recv()) + .await + .expect("reconnect should bypass backoff after the active attempt finishes") + .expect("second connection signal should exist"); + + shutdown_token.cancel(); + let outcome = connect_task.await.expect("connect task should join"); + assert!(matches!(outcome, ConnectOutcome::Shutdown)); + server_task.await.expect("server task should join"); + } + #[test] fn pending_reconnect_drain_preserves_disabled_status() { let (transport_event_tx, _transport_event_rx) = mpsc::channel(1); @@ -2201,9 +2369,9 @@ mod tests { status_publisher.publish_status(RemoteControlConnectionStatus::Disabled); let shutdown_token = CancellationToken::new(); let (_enabled_tx, enabled_rx) = watch::channel(false); - let (reconnect_tx, reconnect_rx) = mpsc::unbounded_channel(); + let (reconnect_tx, reconnect_rx) = mpsc::channel(/*buffer*/ 1); reconnect_tx - .send(9) + .try_send(9) .expect("reconnect command should queue"); let mut websocket = test_remote_control_websocket( transport_event_tx, @@ -2244,9 +2412,9 @@ mod tests { status_publisher.publish_status(RemoteControlConnectionStatus::Connected); let shutdown_token = CancellationToken::new(); let (_enabled_tx, enabled_rx) = watch::channel(false); - let (reconnect_tx, reconnect_rx) = mpsc::unbounded_channel(); + let (reconnect_tx, reconnect_rx) = mpsc::channel(/*buffer*/ 1); reconnect_tx - .send(8) + .try_send(8) .expect("reconnect command should queue"); let mut websocket = test_remote_control_websocket( transport_event_tx, @@ -2811,7 +2979,7 @@ mod tests { let (status_publisher, _status_rx) = remote_control_status_channel(); let shutdown_token = CancellationToken::new(); let (_enabled_tx, enabled_rx) = watch::channel(true); - let (_reconnect_tx, reconnect_rx) = mpsc::unbounded_channel(); + let (_reconnect_tx, reconnect_rx) = mpsc::channel(/*buffer*/ 1); let websocket_task = tokio::spawn({ let shutdown_token = shutdown_token.clone(); async move { @@ -2935,6 +3103,24 @@ mod tests { ); } + #[test] + fn pending_reconnect_prevents_stale_connected_status() { + let (status_publisher, status_rx) = remote_control_status_channel(); + let (reconnect_tx, reconnect_rx) = mpsc::channel(/*buffer*/ 1); + reconnect_tx + .try_send(1) + .expect("reconnect command should queue"); + + assert!(!status_publisher.publish_status_if_no_pending_reconnect( + &reconnect_rx, + RemoteControlConnectionStatus::Connected, + )); + assert_eq!( + status_rx.borrow().status, + RemoteControlConnectionStatus::Connecting + ); + } + #[tokio::test] async fn run_server_writer_inner_sends_periodic_ping_frames() { let (client_stream, mut server_stream) = connected_websocket_pair().await; diff --git a/codex-rs/app-server-transport/src/transport/remote_control/websocket/tests/auth_change_tests.rs b/codex-rs/app-server-transport/src/transport/remote_control/websocket/tests/auth_change_tests.rs index acd119fd55d..814f99372b6 100644 --- a/codex-rs/app-server-transport/src/transport/remote_control/websocket/tests/auth_change_tests.rs +++ b/codex-rs/app-server-transport/src/transport/remote_control/websocket/tests/auth_change_tests.rs @@ -35,7 +35,7 @@ async fn active_remote_control_websocket( RemoteControlWebsocket, CancellationToken, watch::Sender, - mpsc::UnboundedSender, + mpsc::Sender, ) { let remote_control_url = remote_control_url_for_listener(listener); let remote_control_target = @@ -47,7 +47,7 @@ async fn active_remote_control_websocket( let (status_publisher, _status_rx) = remote_control_status_channel(); let shutdown_token = CancellationToken::new(); let (enabled_tx, enabled_rx) = watch::channel(/*init*/ true); - let (reconnect_tx, reconnect_rx) = mpsc::unbounded_channel(); + let (reconnect_tx, reconnect_rx) = mpsc::channel(/*buffer*/ 1); let websocket = RemoteControlWebsocket::new( RemoteControlWebsocketConfig { remote_control_url, diff --git a/codex-rs/app-server-transport/src/transport/remote_control/websocket/tests/outer_loop_tests.rs b/codex-rs/app-server-transport/src/transport/remote_control/websocket/tests/outer_loop_tests.rs index 5b18c238ce7..81ecd0dc6de 100644 --- a/codex-rs/app-server-transport/src/transport/remote_control/websocket/tests/outer_loop_tests.rs +++ b/codex-rs/app-server-transport/src/transport/remote_control/websocket/tests/outer_loop_tests.rs @@ -22,7 +22,7 @@ struct WorkerHandles { _enabled_tx: watch::Sender, // Must be kept alive: dropping it closes the reconnect channel and makes // the backoff select return Shutdown immediately. - _reconnect_tx: mpsc::UnboundedSender, + _reconnect_tx: mpsc::Sender, transport_event_rx: mpsc::Receiver, } @@ -43,7 +43,7 @@ async fn worker_with_listener( let (status_publisher, _status_rx) = remote_control_status_channel(); let shutdown_token = CancellationToken::new(); let (enabled_tx, enabled_rx) = watch::channel(/*init*/ true); - let (reconnect_tx, reconnect_rx) = mpsc::unbounded_channel(); + let (reconnect_tx, reconnect_rx) = mpsc::channel(/*buffer*/ 1); let initial_enrollment = pre_seeded_account.map(|(account_id, server_token)| { let mut enrollment = remote_control_enrollment(Some(server_token)); enrollment.remote_control_target = remote_control_target.clone(); diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 0dfc578ae06..760b2e94f65 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -294,7 +294,7 @@ Example with notification opt-out: - `app/list` — list available apps. - `remoteControl/enable` — experimental; enable remote control for the current app-server process and return the current remote-control status snapshot. The caller is responsible for persisting the desired setting outside app-server. - `remoteControl/disable` — experimental; disable remote control for the current app-server process and return the current remote-control status snapshot. This does not revoke already enrolled controller devices. -- `remoteControl/reconnect` — experimental; reconnect only the remote-control relay for the current app-server process. The daemon, enrollment, environment id, pairing authorization, virtual clients, threads, and account state remain intact. Concurrent requests coalesce while a connection attempt is already in progress. Returns the connecting status snapshot and rejects requests while remote control is disabled. +- `remoteControl/reconnect` — experimental; reconnect only the remote-control relay for the current app-server process. The daemon, enrollment, environment id, pairing authorization, virtual clients, threads, and account state remain intact. Pending requests coalesce; a request accepted after the worker begins a connection attempt schedules a fresh attempt after the current one finishes. Returns the connecting status snapshot and rejects requests while remote control is disabled. - `remoteControl/status/read` — experimental; read the current remote-control status snapshot. `status` is one of `disabled`, `connecting`, `connected`, or `errored`; `serverName` is the local machine name used by this app-server process; `environmentId` is a string when the app-server has a current enrollment and `null` when that enrollment is cleared, invalidated, or remote control is disabled. - `codeBridge/status/read` — experimental; read whether a local Code Bridge service is discoverable and responsive. This method first reads the Codex