Report selected environment connection transitions (#33251)

## What changed

- Track connected and disconnected states across initial remote exec-server connections and reconnection attempts.
- Emit experimental `thread/environment/connected` and `thread/environment/disconnected` app-server notifications for each thread selecting the environment. Each payload identifies the thread and environment; current state is not replayed when a thread starts.
- Stop forwarding connection events when an environment selection is removed or replaced.

## Testing

- Cover connection, disconnection, reconnection, shared-environment notifications, and replacement of a selected environment.

GitOrigin-RevId: 5dd767372363c4a2a8319fc16164be117d5bd20c
This commit is contained in:
sayan-oai
2026-07-15 05:21:13 +00:00
committed by copyberry
parent 8aae858958
commit 3afbd8dd45
28 changed files with 672 additions and 32 deletions
@@ -45,6 +45,9 @@ async fn deferred_environment_waits_before_connecting() -> anyhow::Result<()> {
let registration =
manager.register_deferred_noise_environment("tools".to_string(), provider.clone())?;
let environment = manager.get_environment("tools").expect("environment");
let connection_state = environment
.subscribe_connection_state()
.expect("remote environment connection state");
let mut readiness = Box::pin(environment.wait_until_ready());
assert!(poll!(&mut readiness).is_pending());
@@ -54,6 +57,7 @@ async fn deferred_environment_waits_before_connecting() -> anyhow::Result<()> {
let error = readiness.await.unwrap_err();
assert!(error.to_string().contains("test Noise provider called"));
assert_eq!(provider.calls(), 1);
assert!(!connection_state.has_changed()?);
Ok(())
}
+24
View File
@@ -15,6 +15,7 @@ use anyhow::Result;
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD;
use codex_api::AuthProvider;
use codex_exec_server::EnvironmentConnectionState;
use codex_exec_server::EnvironmentManager;
use codex_exec_server::ExecParams;
use codex_exec_server::ExecResponse;
@@ -42,6 +43,7 @@ use relay_proto::relay_message_frame;
use tempfile::TempDir;
use tokio::net::TcpListener;
use tokio::net::TcpStream;
use tokio::sync::watch;
use tokio::time::timeout;
use tokio_tungstenite::WebSocketStream;
use tokio_tungstenite::accept_async;
@@ -157,6 +159,9 @@ async fn deferred_noise_environment_connects_and_reconnects_with_fresh_bundle()
let environment = manager
.get_environment(ENVIRONMENT_ID)
.context("deferred Noise environment")?;
let mut connection_state = environment
.subscribe_connection_state()
.context("remote environment connection state")?;
assert_eq!(provider.calls(), 0);
registration.complete(Ok(()))?;
@@ -170,9 +175,17 @@ async fn deferred_noise_environment_connects_and_reconnects_with_fresh_bundle()
.await
.context("deferred Noise environment should become ready")??;
assert_eq!(provider.calls(), 1);
assert_eq!(
next_connection_state(&mut connection_state).await?,
EnvironmentConnectionState::Connected
);
first_relay.abort();
let _ = first_relay.await;
assert_eq!(
next_connection_state(&mut connection_state).await?,
EnvironmentConnectionState::Disconnected
);
let first_reconnected_websocket = accept_websocket(&listener, "reconnected peer").await?;
let second_reconnected_websocket = accept_websocket(&listener, "reconnected peer").await?;
let second_relay = tokio::spawn(proxy_relay_frames(
@@ -186,6 +199,10 @@ async fn deferred_noise_environment_connects_and_reconnects_with_fresh_bundle()
assert_eq!(recovered_info, initial_info);
assert_eq!(provider.calls(), 2);
assert_eq!(
next_connection_state(&mut connection_state).await?,
EnvironmentConnectionState::Connected
);
registry.verify().await;
second_relay.abort();
@@ -195,6 +212,13 @@ async fn deferred_noise_environment_connects_and_reconnects_with_fresh_bundle()
Ok(())
}
async fn next_connection_state(
state: &mut watch::Receiver<EnvironmentConnectionState>,
) -> Result<EnvironmentConnectionState> {
timeout(TEST_TIMEOUT, state.changed()).await??;
Ok(*state.borrow_and_update())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn remote_environment_routes_encrypted_exec_server_rpc() -> Result<()> {
let listener = TcpListener::bind("127.0.0.1:0").await?;